mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Business
This commit is contained in:
parent
0ba75f81de
commit
729a260626
@ -850,6 +850,9 @@ public protocol TelegramRootControllerInterface: NavigationController {
|
||||
public protocol QuickReplySetupScreenInitialData: AnyObject {
|
||||
}
|
||||
|
||||
public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject {
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
var sharedContainerPath: String { get }
|
||||
var basePath: String { get }
|
||||
@ -940,7 +943,8 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeBusinessLocationSetupScreen(context: AccountContext, initialValue: TelegramBusinessLocation?, completion: @escaping (TelegramBusinessLocation?) -> Void) -> ViewController
|
||||
func makeBusinessHoursSetupScreen(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) -> ViewController
|
||||
func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController
|
||||
func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController
|
||||
func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError>
|
||||
func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController
|
||||
func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal<QuickReplySetupScreenInitialData, NoError>
|
||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||
|
@ -19,6 +19,7 @@ public enum AttachmentButtonType: Equatable {
|
||||
case gallery
|
||||
case file
|
||||
case location
|
||||
case quickReply
|
||||
case contact
|
||||
case poll
|
||||
case app(AttachMenuBot)
|
||||
@ -27,54 +28,60 @@ public enum AttachmentButtonType: Equatable {
|
||||
|
||||
public static func ==(lhs: AttachmentButtonType, rhs: AttachmentButtonType) -> Bool {
|
||||
switch lhs {
|
||||
case .gallery:
|
||||
if case .gallery = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .file:
|
||||
if case .file = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .location:
|
||||
if case .location = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .contact:
|
||||
if case .contact = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .poll:
|
||||
if case .poll = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .app(lhsBot):
|
||||
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .gift:
|
||||
if case .gift = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .standalone:
|
||||
if case .standalone = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .gallery:
|
||||
if case .gallery = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .file:
|
||||
if case .file = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .location:
|
||||
if case .location = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .quickReply:
|
||||
if case .quickReply = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .contact:
|
||||
if case .contact = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .poll:
|
||||
if case .poll = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .app(lhsBot):
|
||||
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .gift:
|
||||
if case .gift = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .standalone:
|
||||
if case .standalone = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -217,6 +217,10 @@ private final class AttachButtonComponent: CombinedComponent {
|
||||
name = ""
|
||||
imageName = ""
|
||||
imageFile = nil
|
||||
case .quickReply:
|
||||
//TODO:localize
|
||||
name = "Reply"
|
||||
imageName = "Chat/Attach Menu/Location"
|
||||
}
|
||||
|
||||
let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor
|
||||
@ -1183,6 +1187,9 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
accessibilityTitle = bot.shortName
|
||||
case .standalone:
|
||||
accessibilityTitle = ""
|
||||
case .quickReply:
|
||||
//TODO:localize
|
||||
accessibilityTitle = "Reply"
|
||||
}
|
||||
buttonView.isAccessibilityElement = true
|
||||
buttonView.accessibilityLabel = accessibilityTitle
|
||||
|
@ -1156,6 +1156,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
var avatarIconComponent: EmojiStatusComponent?
|
||||
var avatarVideoNode: AvatarVideoNode?
|
||||
var avatarTapRecognizer: UITapGestureRecognizer?
|
||||
var avatarMediaNode: AvatarVideoNode?
|
||||
|
||||
private var inlineNavigationMarkLayer: SimpleLayer?
|
||||
|
||||
|
@ -2132,9 +2132,23 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
|
||||
})
|
||||
case .greetings:
|
||||
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: false))
|
||||
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
||||
guard let accountContext else {
|
||||
return
|
||||
}
|
||||
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false))
|
||||
})
|
||||
case .awayMessages:
|
||||
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: true))
|
||||
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
||||
guard let accountContext else {
|
||||
return
|
||||
}
|
||||
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true))
|
||||
})
|
||||
case .chatbots:
|
||||
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext))
|
||||
}
|
||||
|
@ -876,7 +876,7 @@ public final class PendingMessageManager {
|
||||
|
||||
var quickReplyShortcut: Api.InputQuickReplyShortcut?
|
||||
if let quickReply {
|
||||
if let threadId = messages[0].0.threadId, !"".isEmpty {
|
||||
if let threadId = messages[0].0.threadId {
|
||||
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
|
||||
} else {
|
||||
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
|
||||
@ -1008,7 +1008,7 @@ public final class PendingMessageManager {
|
||||
|
||||
var quickReplyShortcut: Api.InputQuickReplyShortcut?
|
||||
if let quickReply {
|
||||
if let threadId = messages[0].0.threadId, !"".isEmpty {
|
||||
if let threadId = messages[0].0.threadId {
|
||||
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
|
||||
} else {
|
||||
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
|
||||
@ -1319,7 +1319,7 @@ public final class PendingMessageManager {
|
||||
|
||||
var quickReplyShortcut: Api.InputQuickReplyShortcut?
|
||||
if let quickReply {
|
||||
if let threadId = message.threadId, !"".isEmpty {
|
||||
if let threadId = message.threadId {
|
||||
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
|
||||
} else {
|
||||
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
|
||||
@ -1394,7 +1394,7 @@ public final class PendingMessageManager {
|
||||
|
||||
var quickReplyShortcut: Api.InputQuickReplyShortcut?
|
||||
if let quickReply {
|
||||
if let threadId = message.threadId, !"".isEmpty {
|
||||
if let threadId = message.threadId {
|
||||
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
|
||||
} else {
|
||||
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
|
||||
@ -1413,7 +1413,7 @@ public final class PendingMessageManager {
|
||||
|
||||
var quickReplyShortcut: Api.InputQuickReplyShortcut?
|
||||
if let quickReply {
|
||||
if let threadId = message.threadId, !"".isEmpty {
|
||||
if let threadId = message.threadId {
|
||||
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
|
||||
} else {
|
||||
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
|
||||
@ -1487,7 +1487,7 @@ public final class PendingMessageManager {
|
||||
|
||||
var quickReplyShortcut: Api.InputQuickReplyShortcut?
|
||||
if let quickReply {
|
||||
if let threadId = message.threadId, !"".isEmpty {
|
||||
if let threadId = message.threadId {
|
||||
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
|
||||
} else {
|
||||
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
|
||||
|
@ -755,7 +755,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
|
||||
|
||||
var displayTranscribe = false
|
||||
if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
|
||||
if Namespaces.Message.allNonRegular.contains(arguments.message.id.namespace) {
|
||||
displayTranscribe = false
|
||||
} else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||
if arguments.associatedData.isPremium {
|
||||
displayTranscribe = true
|
||||
|
@ -46,6 +46,7 @@ swift_library(
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/TelegramUI/Components/TimeSelectionActionSheet",
|
||||
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||
"//submodules/AttachmentUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -46,13 +46,16 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let initialData: AutomaticBusinessMessageSetupScreen.InitialData
|
||||
let mode: AutomaticBusinessMessageSetupScreen.Mode
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
initialData: AutomaticBusinessMessageSetupScreen.InitialData,
|
||||
mode: AutomaticBusinessMessageSetupScreen.Mode
|
||||
) {
|
||||
self.context = context
|
||||
self.initialData = initialData
|
||||
self.mode = mode
|
||||
}
|
||||
|
||||
@ -130,7 +133,8 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
|
||||
private var isOn: Bool = false
|
||||
private var accountPeer: EnginePeer?
|
||||
private var messages: [EngineMessage] = []
|
||||
private var currentShortcut: ShortcutMessageList.Item?
|
||||
private var currentShortcutDisposable: Disposable?
|
||||
|
||||
private var schedule: Schedule = .always
|
||||
private var customScheduleStart: Date?
|
||||
@ -144,8 +148,6 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
|
||||
private var replyToMessages: Bool = true
|
||||
|
||||
private var messagesDisposable: Disposable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
@ -172,7 +174,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.messagesDisposable?.dispose()
|
||||
self.currentShortcutDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
@ -350,10 +352,19 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let shortcutName: String
|
||||
switch component.mode {
|
||||
case .greeting:
|
||||
shortcutName = "hello"
|
||||
case .away:
|
||||
shortcutName = "away"
|
||||
}
|
||||
|
||||
let contents = AutomaticBusinessMessageSetupChatContents(
|
||||
context: component.context,
|
||||
kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput,
|
||||
shortcutId: nil
|
||||
kind: .quickReplyMessageInput(shortcut: shortcutName),
|
||||
shortcutId: self.currentShortcut?.id
|
||||
)
|
||||
let chatController = component.context.sharedContext.makeChatController(
|
||||
context: component.context,
|
||||
@ -364,7 +375,6 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
)
|
||||
chatController.navigationPresentation = .modal
|
||||
self.environment?.controller()?.push(chatController)
|
||||
self.messagesDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) {
|
||||
@ -459,14 +469,27 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
if self.component == nil {
|
||||
let _ = (component.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
self.accountPeer = component.initialData.accountPeer
|
||||
|
||||
let shortcutName: String
|
||||
switch component.mode {
|
||||
case .greeting:
|
||||
shortcutName = "hello"
|
||||
case .away:
|
||||
shortcutName = "away"
|
||||
}
|
||||
self.currentShortcut = component.initialData.shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
|
||||
|
||||
self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.accountPeer = peer
|
||||
let shortcut = shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
|
||||
if shortcut != self.currentShortcut {
|
||||
self.currentShortcut = shortcut
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -632,15 +655,15 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
|
||||
//TODO:localize
|
||||
var messagesSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
if let topMessage = self.messages.first {
|
||||
if let currentShortcut = self.currentShortcut {
|
||||
if let accountPeer = self.accountPeer {
|
||||
messagesSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(GreetingMessageListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
accountPeer: accountPeer,
|
||||
message: topMessage,
|
||||
count: self.messages.count,
|
||||
message: currentShortcut.topMessage,
|
||||
count: currentShortcut.totalCount,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -681,7 +704,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
theme: environment.theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: component.mode == .greeting ? (self.messages.count > 1 ? "GREETING MESSAGES" : "GREETING MESSAGE") : (self.messages.count > 1 ? "AWAY MESSAGES" : "AWAY MESSAGE"),
|
||||
string: component.mode == .greeting ? "GREETING MESSAGE" : "AWAY MESSAGE",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
@ -1244,6 +1267,19 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentContainer {
|
||||
public final class InitialData: AutomaticBusinessMessageSetupScreenInitialData {
|
||||
let accountPeer: EnginePeer?
|
||||
let shortcutMessageList: ShortcutMessageList
|
||||
|
||||
init(
|
||||
accountPeer: EnginePeer?,
|
||||
shortcutMessageList: ShortcutMessageList
|
||||
) {
|
||||
self.accountPeer = accountPeer
|
||||
self.shortcutMessageList = shortcutMessageList
|
||||
}
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
case greeting
|
||||
case away
|
||||
@ -1251,11 +1287,12 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
public init(context: AccountContext, mode: Mode) {
|
||||
public init(context: AccountContext, initialData: InitialData, mode: Mode) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: AutomaticBusinessMessageSetupScreenComponent(
|
||||
context: context,
|
||||
initialData: initialData,
|
||||
mode: mode
|
||||
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
@ -1293,4 +1330,20 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
public static func initialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> {
|
||||
return combineLatest(
|
||||
context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
|
||||
),
|
||||
context.engine.accountData.shortcutMessageList()
|
||||
|> take(1)
|
||||
)
|
||||
|> map { accountPeer, shortcutMessageList -> AutomaticBusinessMessageSetupScreenInitialData in
|
||||
return InitialData(
|
||||
accountPeer: accountPeer,
|
||||
shortcutMessageList: shortcutMessageList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,19 +21,23 @@ import QuickReplyNameAlertController
|
||||
import ChatListHeaderComponent
|
||||
import PlainButtonComponent
|
||||
import MultilineTextComponent
|
||||
import AttachmentUI
|
||||
|
||||
final class QuickReplySetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let initialData: QuickReplySetupScreen.InitialData
|
||||
|
||||
let mode: QuickReplySetupScreen.Mode
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
initialData: QuickReplySetupScreen.InitialData
|
||||
initialData: QuickReplySetupScreen.InitialData,
|
||||
mode: QuickReplySetupScreen.Mode
|
||||
) {
|
||||
self.context = context
|
||||
self.initialData = initialData
|
||||
self.mode = mode
|
||||
}
|
||||
|
||||
static func ==(lhs: QuickReplySetupScreenComponent, rhs: QuickReplySetupScreenComponent) -> Bool {
|
||||
@ -516,6 +520,13 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .select(completion) = component.mode {
|
||||
if let shortcutId {
|
||||
completion(shortcutId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let shortcut {
|
||||
let contents = AutomaticBusinessMessageSetupChatContents(
|
||||
context: component.context,
|
||||
@ -635,7 +646,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
//TODO:localize
|
||||
items.append(ActionSheetButtonItem(title: ids.count == 1 ? "Delete Shortcut" : "Delete Shortcuts", color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
items.append(ActionSheetButtonItem(title: ids.count == 1 ? "Delete Quick Reply" : "Delete Quick Replies", color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
@ -667,6 +678,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
size: CGSize,
|
||||
insets: UIEdgeInsets,
|
||||
statusBarHeight: CGFloat,
|
||||
isModal: Bool,
|
||||
transition: Transition,
|
||||
deferScrollApplication: Bool
|
||||
) -> CGFloat {
|
||||
@ -706,14 +718,31 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
titleText = "Quick Replies"
|
||||
}
|
||||
|
||||
let closeTitle: String
|
||||
switch component.mode {
|
||||
case .manage:
|
||||
closeTitle = strings.Common_Close
|
||||
case .select:
|
||||
closeTitle = strings.Common_Cancel
|
||||
}
|
||||
let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content(
|
||||
title: titleText,
|
||||
navigationBackTitle: nil,
|
||||
titleComponent: nil,
|
||||
chatListTitle: nil,
|
||||
leftButton: nil,
|
||||
leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .text(title: closeTitle, isBold: false),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.attemptNavigation(complete: {}) {
|
||||
self.environment?.controller()?.dismiss()
|
||||
}
|
||||
}
|
||||
))) : nil,
|
||||
rightButtons: rightButtons,
|
||||
backTitle: "Back",
|
||||
backTitle: isModal ? nil : "Back",
|
||||
backPressed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -896,6 +925,19 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var isModal = false
|
||||
if let controller = environment.controller(), controller.navigationPresentation == .modal {
|
||||
isModal = true
|
||||
}
|
||||
if case .select = component.mode {
|
||||
isModal = true
|
||||
}
|
||||
|
||||
var statusBarHeight = environment.statusBarHeight
|
||||
if isModal {
|
||||
statusBarHeight = max(statusBarHeight, 1.0)
|
||||
}
|
||||
|
||||
var listBottomInset = environment.safeInsets.bottom
|
||||
let navigationHeight = self.updateNavigationBar(
|
||||
component: component,
|
||||
@ -903,7 +945,8 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
strings: environment.strings,
|
||||
size: availableSize,
|
||||
insets: environment.safeInsets,
|
||||
statusBarHeight: environment.statusBarHeight,
|
||||
statusBarHeight: statusBarHeight,
|
||||
isModal: isModal,
|
||||
transition: transition,
|
||||
deferScrollApplication: true
|
||||
)
|
||||
@ -1013,7 +1056,12 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
|
||||
var entries: [ContentEntry] = []
|
||||
if let shortcutMessageList = self.shortcutMessageList, let accountPeer = self.accountPeer {
|
||||
entries.append(.add)
|
||||
switch component.mode {
|
||||
case .manage:
|
||||
entries.append(.add)
|
||||
case .select:
|
||||
break
|
||||
}
|
||||
for item in shortcutMessageList.items {
|
||||
entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: self.selectedIds.contains(item.id)))
|
||||
}
|
||||
@ -1046,7 +1094,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public final class QuickReplySetupScreen: ViewControllerComponentContainer {
|
||||
public final class QuickReplySetupScreen: ViewControllerComponentContainer, AttachmentContainable {
|
||||
public final class InitialData: QuickReplySetupScreenInitialData {
|
||||
let accountPeer: EnginePeer?
|
||||
let shortcutMessageList: ShortcutMessageList
|
||||
@ -1060,14 +1108,36 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
case manage
|
||||
case select(completion: (Int32) -> Void)
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
public init(context: AccountContext, initialData: InitialData) {
|
||||
public var requestAttachmentMenuExpansion: () -> Void = {
|
||||
}
|
||||
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in
|
||||
}
|
||||
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in
|
||||
}
|
||||
public var cancelPanGesture: () -> Void = {
|
||||
}
|
||||
public var isContainerPanning: () -> Bool = {
|
||||
return false
|
||||
}
|
||||
public var isContainerExpanded: () -> Bool = {
|
||||
return false
|
||||
}
|
||||
public var mediaPickerContext: AttachmentMediaPickerContext?
|
||||
|
||||
public init(context: AccountContext, initialData: InitialData, mode: Mode) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: QuickReplySetupScreenComponent(
|
||||
context: context,
|
||||
initialData: initialData
|
||||
initialData: initialData,
|
||||
mode: mode
|
||||
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
@ -1116,4 +1186,21 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func isContainerPanningUpdated(_ panning: Bool) {
|
||||
}
|
||||
|
||||
public func resetForReuse() {
|
||||
}
|
||||
|
||||
public func prepareForReuse() {
|
||||
}
|
||||
|
||||
public func requestDismiss(completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
public func shouldDismissImmediately() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/LocationUI",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Geocoding",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -20,6 +20,8 @@ import BundleIconComponent
|
||||
import LottieComponent
|
||||
import Markdown
|
||||
import LocationUI
|
||||
import CoreLocation
|
||||
import Geocoding
|
||||
|
||||
final class BusinessLocationSetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -74,7 +76,11 @@ final class BusinessLocationSetupScreenComponent: Component {
|
||||
private let textFieldTag = NSObject()
|
||||
private var resetAddressText: String?
|
||||
|
||||
private var isLoadingGeocodedAddress: Bool = false
|
||||
private var geocodeAddressState: (address: String, disposable: Disposable)?
|
||||
|
||||
private var mapCoordinates: TelegramBusinessLocation.Coordinates?
|
||||
private var mapCoordinatesManuallySet: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
@ -102,6 +108,7 @@ final class BusinessLocationSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.geocodeAddressState?.disposable.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
@ -183,12 +190,18 @@ final class BusinessLocationSetupScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, completion: { [weak self] location, _, _, address, _ in
|
||||
var initialLocation: CLLocationCoordinate2D?
|
||||
if let mapCoordinates = self.mapCoordinates {
|
||||
initialLocation = CLLocationCoordinate2D(latitude: mapCoordinates.latitude, longitude: mapCoordinates.longitude)
|
||||
}
|
||||
|
||||
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, initialLocation: initialLocation, completion: { [weak self] location, _, _, address, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.mapCoordinates = TelegramBusinessLocation.Coordinates(latitude: location.latitude, longitude: location.longitude)
|
||||
self.mapCoordinatesManuallySet = true
|
||||
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View, textView.currentText.isEmpty {
|
||||
self.resetAddressText = address
|
||||
}
|
||||
@ -198,6 +211,43 @@ final class BusinessLocationSetupScreenComponent: Component {
|
||||
self.environment?.controller()?.push(controller)
|
||||
}
|
||||
|
||||
private func updateGeocodedAddress(string: String) {
|
||||
let addressValue: String?
|
||||
if self.mapCoordinates != nil && self.mapCoordinatesManuallySet {
|
||||
addressValue = nil
|
||||
} else if string.count < 3 {
|
||||
addressValue = nil
|
||||
} else {
|
||||
addressValue = string
|
||||
}
|
||||
|
||||
if let current = self.geocodeAddressState, current.address == addressValue {
|
||||
} else {
|
||||
self.geocodeAddressState?.disposable.dispose()
|
||||
self.geocodeAddressState = nil
|
||||
|
||||
if let addressValue {
|
||||
let disposable = MetaDisposable()
|
||||
self.geocodeAddressState = (string, disposable)
|
||||
|
||||
disposable.set((
|
||||
geocodeLocation(address: addressValue, locale: Locale.current)
|
||||
|> delay(0.4, queue: .mainQueue())
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let location = result?.first?.location, !self.mapCoordinatesManuallySet {
|
||||
self.mapCoordinates = TelegramBusinessLocation.Coordinates(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -207,6 +257,9 @@ final class BusinessLocationSetupScreenComponent: Component {
|
||||
if self.component == nil {
|
||||
if let initialValue = component.initialValue {
|
||||
self.mapCoordinates = initialValue.coordinates
|
||||
if self.mapCoordinates != nil {
|
||||
self.mapCoordinatesManuallySet = true
|
||||
}
|
||||
self.resetAddressText = initialValue.address
|
||||
}
|
||||
}
|
||||
@ -333,12 +386,7 @@ final class BusinessLocationSetupScreenComponent: Component {
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
characterLimit: 64,
|
||||
updated: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
let _ = value
|
||||
updated: { _ in
|
||||
},
|
||||
tag: self.textFieldTag
|
||||
))))
|
||||
@ -390,6 +438,7 @@ final class BusinessLocationSetupScreenComponent: Component {
|
||||
self.openLocationPicker()
|
||||
} else {
|
||||
self.mapCoordinates = nil
|
||||
self.mapCoordinatesManuallySet = false
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
@ -1125,7 +1125,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
chatFilterTag = value
|
||||
}
|
||||
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: false, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
|
||||
var standalone = false
|
||||
if case .customChatContents = strongSelf.chatLocation {
|
||||
standalone = true
|
||||
}
|
||||
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
|
||||
self?.chatDisplayNode.dismissInput()
|
||||
}, present: { c, a in
|
||||
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||||
@ -9519,7 +9524,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
|
||||
self.push(self.context.sharedContext.makeQuickReplySetupScreen(context: self.context, initialData: initialData))
|
||||
let controller = self.context.sharedContext.makeQuickReplySetupScreen(context: self.context, initialData: initialData)
|
||||
controller.navigationPresentation = .modal
|
||||
self.push(controller)
|
||||
})
|
||||
}, sendBotStart: { [weak self] payload in
|
||||
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||||
@ -9542,9 +9549,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.dismissAllTooltips()
|
||||
|
||||
@ -9554,32 +9558,34 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
var bannedMediaInput = false
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
||||
bannedMediaInput = true
|
||||
} else if channel.hasBannedPermission(.banSendVoice) != nil {
|
||||
if !isVideo {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
||||
return
|
||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
||||
bannedMediaInput = true
|
||||
} else if channel.hasBannedPermission(.banSendVoice) != nil {
|
||||
if !isVideo {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
||||
return
|
||||
}
|
||||
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
||||
if isVideo {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
||||
if isVideo {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
|
||||
bannedMediaInput = true
|
||||
} else if group.hasBannedPermission(.banSendVoice) {
|
||||
if !isVideo {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
||||
return
|
||||
}
|
||||
} else if group.hasBannedPermission(.banSendInstantVideos) {
|
||||
if isVideo {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
||||
return
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
|
||||
bannedMediaInput = true
|
||||
} else if group.hasBannedPermission(.banSendVoice) {
|
||||
if !isVideo {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
||||
return
|
||||
}
|
||||
} else if group.hasBannedPermission(.banSendInstantVideos) {
|
||||
if isVideo {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import ChatEntityKeyboardInputNode
|
||||
import PremiumUI
|
||||
import PremiumGiftAttachmentScreen
|
||||
import TelegramCallsUI
|
||||
import AutomaticBusinessMessageSetupScreen
|
||||
|
||||
extension ChatControllerImpl {
|
||||
enum AttachMenuSubject {
|
||||
@ -131,8 +132,11 @@ extension ChatControllerImpl {
|
||||
|
||||
let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError>
|
||||
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted {
|
||||
buttons = self.context.engine.messages.attachMenuBots()
|
||||
|> map { attachMenuBots in
|
||||
buttons = combineLatest(
|
||||
self.context.engine.messages.attachMenuBots(),
|
||||
self.context.engine.accountData.shortcutMessageList() |> take(1)
|
||||
)
|
||||
|> map { attachMenuBots, shortcutMessageList in
|
||||
var buttons = availableButtons
|
||||
var allButtons = availableButtons
|
||||
var initialButton: AttachmentButtonType?
|
||||
@ -166,6 +170,19 @@ extension ChatControllerImpl {
|
||||
allButtons.insert(button, at: 1)
|
||||
}
|
||||
|
||||
if let user = peer as? TelegramUser, user.botInfo == nil {
|
||||
if let index = buttons.firstIndex(where: { $0 == .location }) {
|
||||
buttons.insert(.quickReply, at: index + 1)
|
||||
} else {
|
||||
buttons.append(.quickReply)
|
||||
}
|
||||
if let index = allButtons.firstIndex(where: { $0 == .location }) {
|
||||
allButtons.insert(.quickReply, at: index + 1)
|
||||
} else {
|
||||
allButtons.append(.quickReply)
|
||||
}
|
||||
}
|
||||
|
||||
return (buttons, allButtons, initialButton)
|
||||
}
|
||||
} else {
|
||||
@ -602,6 +619,24 @@ extension ChatControllerImpl {
|
||||
strongSelf.present(alertController, in: .window(.root))
|
||||
}
|
||||
}
|
||||
case .quickReply:
|
||||
let _ = (strongSelf.context.sharedContext.makeQuickReplySetupScreenInitialData(context: strongSelf.context)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak strongSelf] initialData in
|
||||
guard let strongSelf else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = QuickReplySetupScreen(context: strongSelf.context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .select(completion: { [weak strongSelf] shortcutId in
|
||||
guard let strongSelf else {
|
||||
return
|
||||
}
|
||||
strongSelf.attachmentController?.dismiss(animated: true)
|
||||
strongSelf.interfaceInteraction?.sendShortcut(shortcutId)
|
||||
}))
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -68,6 +68,8 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo
|
||||
} else {
|
||||
hasEditRights = true
|
||||
}
|
||||
} else if message.id.namespace == Namespaces.Message.QuickReplyCloud {
|
||||
hasEditRights = true
|
||||
} else if message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.id.namespace != Namespaces.Message.Cloud {
|
||||
hasEditRights = false
|
||||
} else if let author = message.author, author.id == accountPeerId, let peer = message.peers[message.id.peerId] {
|
||||
@ -601,68 +603,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
return .single(ContextController.Items(content: .list(actions)))
|
||||
}
|
||||
|
||||
if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
|
||||
var actions: [ContextMenuItem] = []
|
||||
|
||||
switch customChatContents.kind {
|
||||
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
var restrictedText: String?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
messageEntities = attribute.entities
|
||||
}
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
||||
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
if let restrictedText = restrictedText {
|
||||
storeMessageTextInPasteboard(restrictedText, entities: nil)
|
||||
} else {
|
||||
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
|
||||
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
|
||||
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
|
||||
} else {
|
||||
storeMessageTextInPasteboard(message.text, entities: messageEntities)
|
||||
}
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.2, {
|
||||
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
|
||||
controllerInteraction.displayUndo(content)
|
||||
})
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { c, f in
|
||||
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
|
||||
f(.custom(transition))
|
||||
})
|
||||
})))
|
||||
|
||||
actions.append(.separator)
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { [weak customChatContents] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let customChatContents else {
|
||||
return
|
||||
}
|
||||
customChatContents.deleteMessages(ids: messages.map(\.id))
|
||||
})))
|
||||
}
|
||||
|
||||
return .single(ContextController.Items(content: .list(actions)))
|
||||
}
|
||||
|
||||
var loadStickerSaveStatus: MediaId?
|
||||
var loadCopyMediaResource: MediaResource?
|
||||
var isAction = false
|
||||
@ -1140,8 +1080,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
})))
|
||||
}
|
||||
|
||||
|
||||
|
||||
var messageText: String = ""
|
||||
for message in messages {
|
||||
if !message.text.isEmpty {
|
||||
@ -1164,6 +1102,16 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}
|
||||
}
|
||||
|
||||
for attribute in message.attributes {
|
||||
if hasExpandedAudioTranscription, let attribute = attribute as? AudioTranscriptionMessageAttribute {
|
||||
if !messageText.isEmpty {
|
||||
messageText.append("\n")
|
||||
}
|
||||
messageText.append(attribute.text)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var isPoll = false
|
||||
if messageText.isEmpty {
|
||||
for media in message.media {
|
||||
@ -1937,6 +1885,74 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
actions.removeFirst()
|
||||
}
|
||||
|
||||
if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
|
||||
actions.removeAll()
|
||||
|
||||
switch customChatContents.kind {
|
||||
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
|
||||
if !messageText.isEmpty || (resourceAvailable && isImage) || diceEmoji != nil {
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
var restrictedText: String?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
messageEntities = attribute.entities
|
||||
}
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
||||
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
if let restrictedText = restrictedText {
|
||||
storeMessageTextInPasteboard(restrictedText, entities: nil)
|
||||
} else {
|
||||
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
|
||||
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
|
||||
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
|
||||
} else {
|
||||
storeMessageTextInPasteboard(message.text, entities: messageEntities)
|
||||
}
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.2, {
|
||||
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
|
||||
controllerInteraction.displayUndo(content)
|
||||
})
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
if message.id.namespace == Namespaces.Message.QuickReplyCloud {
|
||||
if data.canEdit {
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { c, f in
|
||||
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
|
||||
f(.custom(transition))
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
if !actions.isEmpty {
|
||||
actions.append(.separator)
|
||||
}
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { [weak customChatContents] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let customChatContents else {
|
||||
return
|
||||
}
|
||||
customChatContents.deleteMessages(ids: messages.map(\.id))
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ContextController.Items(content: .list(actions), tip: nil)
|
||||
}
|
||||
}
|
||||
|
@ -405,8 +405,9 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
options.insert(.PreferSynchronousResourceLoading)
|
||||
if firstTime {
|
||||
self.contentOffsetChangeTransition = .spring(duration: 0.4)
|
||||
self.contentOffsetChangeTransition = .immediate
|
||||
|
||||
self.listBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.listView.bounds.height), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0))
|
||||
} else {
|
||||
@ -419,7 +420,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
}
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.top = topInsetForLayout(size: validLayout.0, hasShortcuts: transition.hasShortcuts)
|
||||
insets.top = topInsetForLayout(size: validLayout.0)
|
||||
insets.left = validLayout.1
|
||||
insets.right = validLayout.2
|
||||
|
||||
@ -437,12 +438,8 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
if let topItemOffset = topItemOffset {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
|
||||
let position = strongSelf.listView.layer.position
|
||||
strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset))
|
||||
transition.animateView {
|
||||
strongSelf.listView.position = position
|
||||
}
|
||||
//transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
|
||||
transition.animatePositionAdditive(layer: strongSelf.listView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
|
||||
transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -451,10 +448,31 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func topInsetForLayout(size: CGSize, hasShortcuts: Bool) -> CGFloat {
|
||||
var minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
|
||||
if hasShortcuts {
|
||||
minimumItemHeights += VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round)
|
||||
private func topInsetForLayout(size: CGSize) -> CGFloat {
|
||||
var minimumItemHeights: CGFloat = 0.0
|
||||
if let currentEntries = self.currentEntries, !currentEntries.isEmpty {
|
||||
let indexLimit = min(4, currentEntries.count - 1)
|
||||
for i in 0 ... indexLimit {
|
||||
var itemHeight: CGFloat
|
||||
switch currentEntries[i].content {
|
||||
case .editShortcuts:
|
||||
itemHeight = VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round)
|
||||
case let .command(command):
|
||||
switch command.command {
|
||||
case .command:
|
||||
itemHeight = MentionChatInputPanelItemNode.itemHeight
|
||||
case .shortcut:
|
||||
itemHeight = 58.0
|
||||
}
|
||||
}
|
||||
if indexLimit >= 4 && i == indexLimit {
|
||||
minimumItemHeights += floor(itemHeight * 0.5)
|
||||
} else {
|
||||
minimumItemHeights += itemHeight
|
||||
}
|
||||
}
|
||||
} else {
|
||||
minimumItemHeights = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
|
||||
}
|
||||
|
||||
return max(size.height - minimumItemHeights, 0.0)
|
||||
@ -465,16 +483,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
self.validLayout = (size, leftInset, rightInset, bottomInset)
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
var hasShortcuts = false
|
||||
if let currentEntries = self.currentEntries {
|
||||
hasShortcuts = currentEntries.contains(where: { entry in
|
||||
if case .editShortcuts = entry.content {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
insets.top = self.topInsetForLayout(size: size, hasShortcuts: hasShortcuts)
|
||||
insets.top = self.topInsetForLayout(size: size)
|
||||
insets.left = leftInset
|
||||
insets.right = rightInset
|
||||
|
||||
|
@ -1902,12 +1902,16 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return BusinessHoursSetupScreen(context: context, initialValue: initialValue, completion: completion)
|
||||
}
|
||||
|
||||
public func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController {
|
||||
return AutomaticBusinessMessageSetupScreen(context: context, mode: isAwayMode ? .away : .greeting)
|
||||
public func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController {
|
||||
return AutomaticBusinessMessageSetupScreen(context: context, initialData: initialData as! AutomaticBusinessMessageSetupScreen.InitialData, mode: isAwayMode ? .away : .greeting)
|
||||
}
|
||||
|
||||
public func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> {
|
||||
return AutomaticBusinessMessageSetupScreen.initialData(context: context)
|
||||
}
|
||||
|
||||
public func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController {
|
||||
return QuickReplySetupScreen(context: context, initialData: initialData as! QuickReplySetupScreen.InitialData)
|
||||
return QuickReplySetupScreen(context: context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .manage)
|
||||
}
|
||||
|
||||
public func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal<QuickReplySetupScreenInitialData, NoError> {
|
||||
|
@ -158,19 +158,23 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem
|
||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
|
||||
let titleOffsetY: CGFloat
|
||||
switch item.style {
|
||||
case .regular:
|
||||
strongSelf.backgroundColor = item.theme.list.plainBackgroundColor
|
||||
strongSelf.topSeparatorNode.isHidden = mergedTop
|
||||
strongSelf.separatorNode.isHidden = !mergedBottom
|
||||
titleOffsetY = 2.0
|
||||
case .round:
|
||||
strongSelf.backgroundColor = nil
|
||||
strongSelf.topSeparatorNode.isHidden = true
|
||||
strongSelf.separatorNode.isHidden = !mergedBottom
|
||||
titleOffsetY = 1.0
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + 2.0), size: titleLayout.size)
|
||||
|
||||
strongSelf.topSeparatorNode.isHidden = mergedTop
|
||||
strongSelf.separatorNode.isHidden = !mergedBottom
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + titleOffsetY), size: titleLayout.size)
|
||||
|
||||
strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel))
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width, height: UIScreenPixel))
|
||||
|
Loading…
x
Reference in New Issue
Block a user