From 7966993955552739bb34b2c1d39ddccc009a2e95 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 5 Mar 2024 15:32:59 +0400 Subject: [PATCH 1/2] Business fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 8 + .../ChatListFilterPresetListController.swift | 32 ++++ .../ChatListFilterPresetListItem.swift | 26 ++- .../Sources/ItemListController.swift | 8 + .../Sources/ItemListControllerNode.swift | 3 + .../Sources/PremiumIntroScreen.swift | 4 + .../Sources/State/AccountViewTracker.swift | 29 ++++ .../TelegramEngineAccountData.swift | 4 +- .../Messages/QuickReplyMessages.swift | 64 ++++++- .../Peers/ChatListFiltering.swift | 16 ++ ...ChatInlineSearchResultsListComponent.swift | 20 ++- .../Sources/ListActionItemComponent.swift | 38 +++++ .../ListMultilineTextFieldItemComponent.swift | 7 + .../PeerInfoScreenBusinessHoursItem.swift | 11 +- ...aticBusinessMessageSetupChatContents.swift | 26 ++- .../AutomaticBusinessMessageSetupScreen.swift | 12 +- .../Sources/QuickReplySetupScreen.swift | 47 ++++-- .../Sources/BusinessDaySetupScreen.swift | 6 +- .../Sources/BusinessLocationSetupScreen.swift | 159 +++++++++++------- .../Sources/TextFieldComponent.swift | 13 ++ .../TelegramUI/Sources/ChatController.swift | 9 + .../Sources/ChatControllerEditChat.swift | 2 +- .../Sources/ChatControllerNode.swift | 50 ++++++ .../ChatControllerOpenAttachmentMenu.swift | 2 +- .../ChatInterfaceStateContextQueries.swift | 2 +- .../CommandChatInputContextPanelNode.swift | 10 +- 26 files changed, 503 insertions(+), 105 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 458fbaf998..8589e388c9 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11424,6 +11424,8 @@ to respond to messages faster."; "PeerInfo.BusinessHours.StatusOpensInHours_1" = "Opens in one hour"; "PeerInfo.BusinessHours.StatusOpensInHours_any" = "Opens in %d hours"; "PeerInfo.BusinessHours.StatusOpensOnDate" = "Opens %@"; +"PeerInfo.BusinessHours.StatusOpensTodayAt" = "Opens today at %@"; +"PeerInfo.BusinessHours.StatusOpensTomorrowAt" = "Opens tomorrow at %@"; "PeerInfo.BusinessHours.TimezoneSwitchMy" = "my time"; "PeerInfo.BusinessHours.TimezoneSwitchBusiness" = "local time"; "PeerInfo.BusinessHours.Label" = "business hours"; @@ -11567,6 +11569,9 @@ to respond to messages faster."; "BusinessLocationSetup.ErrorAddressEmpty.Text" = "Address can't be empty."; "BusinessLocationSetup.ErrorAddressEmpty.ResetAction" = "Delete"; +"BusinessLocationSetup.AlertUnsavedChanges.Text" = "You have unsaved changes."; +"BusinessLocationSetup.AlertUnsavedChanges.ResetAction" = "Revert"; + "ChatbotSetup.Title" = "Chatbots"; "ChatbotSetup.Text" = "Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More >]()"; "ChatbotSetup.TextLink" = "https://telegram.org"; @@ -11588,3 +11593,6 @@ to respond to messages faster."; "Chat.QuickReply.ServiceHeader1" = "To edit or delete your quick reply, tap an hold on it."; "Chat.QuickReply.ServiceHeader2" = "To use this quick reply in a chat, type / and select the shortcut from the list."; + +"Chat.QuickReplyMediaMessageLimitReachedText_1" = "There can be at most %d message in this chat."; +"Chat.QuickReplyMediaMessageLimitReachedText_any" = "There can be at most %d messages in this chat."; diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 6da4bd858c..c398146a21 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -340,6 +340,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let filtersWithCounts = Promise<[(ChatListFilter, Int)]>() filtersWithCounts.set(filtersWithCountsSignal) + let animateNextShowHideTagsTransition = Atomic(value: nil) + let arguments = ChatListFilterPresetListControllerArguments(context: context, addSuggestedPressed: { title, data in let _ = combineLatest( @@ -580,6 +582,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings]) + let previousDisplayTags = Atomic(value: nil) + let limits = context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) @@ -668,6 +672,11 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch rightNavigationButton = nil } + let previousDisplayTagsValue = previousDisplayTags.swap(displayTags) + if let previousDisplayTagsValue, previousDisplayTagsValue != displayTags { + let _ = animateNextShowHideTagsTransition.swap(displayTags) + } + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true) @@ -787,6 +796,29 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } }) }) + controller.afterTransactionCompleted = { [weak controller] in + guard let toggleDirection = animateNextShowHideTagsTransition.swap(nil) else { + return + } + + guard let controller else { + return + } + var presetItemNodes: [ChatListFilterPresetListItemNode] = [] + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatListFilterPresetListItemNode { + presetItemNodes.append(itemNode) + } + } + + var delay: Double = 0.0 + for itemNode in presetItemNodes.reversed() { + if toggleDirection { + itemNode.animateTagColorIn(delay: delay) + } + delay += 0.02 + } + } return controller } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index fc88aafcbb..4af495c8a9 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -112,7 +112,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { private let titleFont = Font.regular(17.0) -private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { +final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -415,7 +415,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN if item.tagColor != nil { sharedIconFrame.origin.x -= 34.0 } - strongSelf.sharedIconNode.frame = sharedIconFrame + if strongSelf.sharedIconNode.bounds.isEmpty { + strongSelf.sharedIconNode.frame = sharedIconFrame + } else { + transition.updateFrame(node: strongSelf.sharedIconNode, frame: sharedIconFrame) + } } var isShared = false if case let .filter(_, _, _, data) = item.preset, data.isShared { @@ -425,9 +429,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN if let tagColor = item.tagColor { let tagIconView: UIImageView + var tagIconTransition = transition if let current = strongSelf.tagIconView { tagIconView = current } else { + tagIconTransition = .immediate tagIconView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 24.0, color: .white)?.withRenderingMode(.alwaysTemplate)) strongSelf.tagIconView = tagIconView strongSelf.containerNode.view.addSubview(tagIconView) @@ -435,13 +441,16 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN tagIconView.tintColor = tagColor let tagIconFrame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX - 2.0 - 24.0, y: floorToScreenPixels((layout.contentSize.height - 24.0) / 2.0)), size: CGSize(width: 24.0, height: 24.0)) - tagIconView.frame = tagIconFrame - transition.updateAlpha(layer: tagIconView.layer, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) + tagIconTransition.updateAlpha(layer: tagIconView.layer, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) + tagIconTransition.updateFrame(view: tagIconView, frame: tagIconFrame) } else { if let tagIconView = strongSelf.tagIconView { strongSelf.tagIconView = nil - tagIconView.removeFromSuperview() + transition.updateAlpha(layer: tagIconView.layer, alpha: 0.0, completion: { [weak tagIconView] _ in + tagIconView?.removeFromSuperview() + }) + transition.updateTransformScale(layer: tagIconView.layer, scale: 0.001) } } @@ -458,6 +467,13 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } } + func animateTagColorIn(delay: Double) { + if let tagIconView = self.tagIconView { + tagIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12, delay: delay) + tagIconView.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: delay) + } + } + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index ea6a8cb633..e0bfc8910e 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -251,6 +251,13 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var willDisappear: ((Bool) -> Void)? public var didDisappear: ((Bool) -> Void)? + public var afterTransactionCompleted: (() -> Void)? { + didSet { + if self.isNodeLoaded { + (self.displayNode as! ItemListControllerNode).afterTransactionCompleted = self.afterTransactionCompleted + } + } + } public init(presentationData: ItemListPresentationData, updatedPresentationData: Signal, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal?) { self.state = state @@ -486,6 +493,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable displayNode.searchActivated = self.searchActivated displayNode.reorderEntry = self.reorderEntry displayNode.reorderCompleted = self.reorderCompleted + displayNode.afterTransactionCompleted = self.afterTransactionCompleted displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem displayNode.listNode.didScrollWithOffset = self.didScrollWithOffset displayNode.requestLayout = { [weak self] transition in diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 32654583a9..995c5a2d1f 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -285,6 +285,7 @@ open class ItemListControllerNode: ASDisplayNode { public var searchActivated: ((Bool) -> Void)? public var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal)? public var reorderCompleted: (([ItemListNodeAnyEntry]) -> Void)? + public var afterTransactionCompleted: (() -> Void)? public var requestLayout: ((ContainedViewLayoutTransition) -> Void)? public var enableInteractiveDismiss = false { @@ -920,6 +921,8 @@ open class ItemListControllerNode: ASDisplayNode { } } } + + strongSelf.afterTransactionCompleted?() } }) var updateEmptyStateItem = false diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 7af14917a3..1c0a6246d0 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -3617,6 +3617,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } navigationController.pushViewController(peerSelectionController) } + + if case .business = mode { + context.account.viewTracker.keepQuickRepliesApproximatelyUpdated() + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 8061d14d5d..b82d15f666 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -2016,6 +2016,35 @@ public final class AccountViewTracker { }) } + public func pendingQuickReplyMessagesViewForLocation(shortcut: String) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + guard let account = self.account else { + return .never() + } + let chatLocation: ChatLocationInput = .peer(peerId: account.peerId, threadId: nil) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just([Namespaces.Message.QuickReplyLocal]), orderStatistics: [], additionalData: []) + |> map { view, update, initialData in + var entries: [MessageHistoryEntry] = [] + for entry in view.entries { + var matches = false + inner: for attribute in entry.message.attributes { + if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + if attribute.shortcut == shortcut { + matches = true + } + break inner + } + } + if matches { + entries.append(entry) + } + } + let mappedView = MessageHistoryView(tag: nil, namespaces: .just([Namespaces.Message.QuickReplyLocal]), entries: entries, holeEarlier: false, holeLater: false, isLoading: false) + + return (mappedView, update, initialData) + } + return signal + } + public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, count: Int, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index 7d83a01d9b..7c50cc1387 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -162,8 +162,8 @@ public extension TelegramEngine { |> then(remoteApply) } - public func shortcutMessageList() -> Signal { - return _internal_shortcutMessageList(account: self.account) + public func shortcutMessageList(onlyRemote: Bool) -> Signal { + return _internal_shortcutMessageList(account: self.account, onlyRemote: onlyRemote) } public func keepShortcutMessageListUpdated() -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index 3863702b94..a7e321c872 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -48,12 +48,12 @@ struct QuickReplyMessageShortcutsState: Codable, Equatable { public final class ShortcutMessageList: Equatable { public final class Item: Equatable { - public let id: Int32 + public let id: Int32? public let shortcut: String public let topMessage: EngineMessage public let totalCount: Int - public init(id: Int32, shortcut: String, topMessage: EngineMessage, totalCount: Int) { + public init(id: Int32?, shortcut: String, topMessage: EngineMessage, totalCount: Int) { self.id = id self.shortcut = shortcut self.topMessage = topMessage @@ -117,12 +117,15 @@ func _internal_quickReplyMessageShortcutsState(account: Account) -> Signal Signal { - let updateSignal = _internal_shortcutMessageList(account: account) + let updateSignal = _internal_shortcutMessageList(account: account, onlyRemote: true) |> take(1) |> mapToSignal { list -> Signal in var acc: UInt64 = 0 for item in list.items { - combineInt64Hash(&acc, with: UInt64(item.id)) + guard let itemId = item.id else { + continue + } + combineInt64Hash(&acc, with: UInt64(itemId)) combineInt64Hash(&acc, with: md5StringHash(item.shortcut)) combineInt64Hash(&acc, with: UInt64(item.topMessage.id.id)) @@ -204,10 +207,42 @@ func _internal_keepShortcutMessagesUpdated(account: Account) -> Signal Signal { - return _internal_quickReplyMessageShortcutsState(account: account) - |> distinctUntilChanged - |> mapToSignal { state -> Signal in +func _internal_shortcutMessageList(account: Account, onlyRemote: Bool) -> Signal { + let pendingShortcuts: Signal<[String: EngineMessage], NoError> + if onlyRemote { + pendingShortcuts = .single([:]) + } else { + pendingShortcuts = account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 100, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Set([Namespaces.Message.QuickReplyLocal])), orderStatistics: []) + |> map { view , _, _ -> [String: EngineMessage] in + var topMessages: [String: EngineMessage] = [:] + for entry in view.entries { + var shortcut: String? + inner: for attribute in entry.message.attributes { + if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + shortcut = attribute.shortcut + break inner + } + } + if let shortcut { + if let currentTopMessage = topMessages[shortcut] { + if entry.message.index < currentTopMessage.index { + topMessages[shortcut] = EngineMessage(entry.message) + } + } else { + topMessages[shortcut] = EngineMessage(entry.message) + } + } + } + return topMessages + } + |> distinctUntilChanged + } + + return combineLatest(queue: .mainQueue(), + _internal_quickReplyMessageShortcutsState(account: account) |> distinctUntilChanged, + pendingShortcuts + ) + |> mapToSignal { state, pendingShortcuts -> Signal in guard let state else { return .single(ShortcutMessageList(items: [], isLoading: true)) } @@ -238,6 +273,7 @@ func _internal_shortcutMessageList(account: Account) -> Signal map { views -> ShortcutMessageList in var items: [ShortcutMessageList.Item] = [] + for shortcut in state.shortcuts { guard let historyViewKey = historyViewKeys[shortcut.id], let historyView = views.views[historyViewKey] as? MessageHistoryView else { continue @@ -254,6 +290,18 @@ func _internal_shortcutMessageList(account: Account) -> Signal distinctUntilChanged diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 2b6554bdee..af88b9dffa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -978,6 +978,22 @@ func _internal_updateChatListFiltersDisplayTagsInteractively(postbox: Postbox, d var state = entry?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default if displayTags != state.displayTags { state.displayTags = displayTags + + if state.displayTags { + for i in 0 ..< state.filters.count { + switch state.filters[i] { + case .allChats: + break + case let .filter(id, title, emoticon, data): + if data.color == nil { + var data = data + data.color = PeerNameColor(rawValue: Int32.random(in: 0 ... 7)) + state.filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: data) + } + } + } + } + hasUpdates = true } diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 4a8a9de071..0900afd2ae 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -81,6 +81,7 @@ public final class ChatInlineSearchResultsListComponent: Component { public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal? public let getSearchResult: () -> Signal? public let getSavedPeers: (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>? + public let loadMoreSearchResults: () -> Void public init( context: AccountContext, @@ -92,7 +93,8 @@ public final class ChatInlineSearchResultsListComponent: Component { peerSelected: @escaping (EnginePeer) -> Void, loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal?, getSearchResult: @escaping () -> Signal?, - getSavedPeers: @escaping (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>? + getSavedPeers: @escaping (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?, + loadMoreSearchResults: @escaping () -> Void ) { self.context = context self.presentation = presentation @@ -104,6 +106,7 @@ public final class ChatInlineSearchResultsListComponent: Component { self.loadTagMessages = loadTagMessages self.getSearchResult = getSearchResult self.getSavedPeers = getSavedPeers + self.loadMoreSearchResults = loadMoreSearchResults } public static func ==(lhs: ChatInlineSearchResultsListComponent, rhs: ChatInlineSearchResultsListComponent) -> Bool { @@ -377,6 +380,19 @@ public final class ChatInlineSearchResultsListComponent: Component { break } } + } else if let (currentIndex, disposable) = self.searchContents { + if let loadAroundIndex, loadAroundIndex != currentIndex { + switch component.contents { + case .empty: + break + case .tag: + break + case .search: + self.searchContents = (loadAroundIndex, disposable) + + component.loadMoreSearchResults() + } + } } } @@ -511,7 +527,7 @@ public final class ChatInlineSearchResultsListComponent: Component { contentId: .search(query), entries: entries, messages: messages, - hasEarlier: false, + hasEarlier: !(result?.completed ?? true), hasLater: false ) if !self.isUpdating { diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 66a8d57b8d..0b0fe45934 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -45,6 +45,7 @@ public final class ListActionItemComponent: Component { public enum Accessory: Equatable { case arrow case toggle(Toggle) + case activity } public enum IconInsets: Equatable { @@ -123,6 +124,7 @@ public final class ListActionItemComponent: Component { private var arrowView: UIImageView? private var switchNode: SwitchNode? private var iconSwitchNode: IconSwitchNode? + private var activityIndicatorView: UIActivityIndicatorView? private var component: ListActionItemComponent? @@ -191,6 +193,8 @@ public final class ListActionItemComponent: Component { contentRightInset = 30.0 case .toggle: contentRightInset = 76.0 + case .activity: + contentRightInset = 76.0 } var contentHeight: CGFloat = 0.0 @@ -428,6 +432,40 @@ public final class ListActionItemComponent: Component { } } + if case .activity = component.accessory { + let activityIndicatorView: UIActivityIndicatorView + var activityIndicatorTransition = transition + if let current = self.activityIndicatorView { + activityIndicatorView = current + } else { + activityIndicatorTransition = activityIndicatorTransition.withAnimation(.none) + if #available(iOS 13.0, *) { + activityIndicatorView = UIActivityIndicatorView(style: .medium) + } else { + activityIndicatorView = UIActivityIndicatorView(style: .gray) + } + self.activityIndicatorView = activityIndicatorView + self.addSubview(activityIndicatorView) + activityIndicatorView.sizeToFit() + } + + let activityIndicatorSize = activityIndicatorView.bounds.size + let activityIndicatorFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - activityIndicatorSize.width, y: floor((min(60.0, contentHeight) - activityIndicatorSize.height) * 0.5)), size: activityIndicatorSize) + + activityIndicatorView.tintColor = component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.5) + + activityIndicatorTransition.setFrame(view: activityIndicatorView, frame: activityIndicatorFrame) + + if !activityIndicatorView.isAnimating { + activityIndicatorView.startAnimating() + } + } else { + if let activityIndicatorView = self.activityIndicatorView { + self.activityIndicatorView = nil + activityIndicatorView.removeFromSuperview() + } + } + self.separatorInset = contentLeftInset return CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index f4476df226..d2c964545d 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -38,6 +38,7 @@ public final class ListMultilineTextFieldItemComponent: Component { public let autocapitalizationType: UITextAutocapitalizationType public let autocorrectionType: UITextAutocorrectionType public let characterLimit: Int? + public let allowEmptyLines: Bool public let updated: ((String) -> Void)? public let textUpdateTransition: Transition public let tag: AnyObject? @@ -53,6 +54,7 @@ public final class ListMultilineTextFieldItemComponent: Component { autocapitalizationType: UITextAutocapitalizationType = .sentences, autocorrectionType: UITextAutocorrectionType = .default, characterLimit: Int? = nil, + allowEmptyLines: Bool = true, updated: ((String) -> Void)?, textUpdateTransition: Transition = .immediate, tag: AnyObject? = nil @@ -67,6 +69,7 @@ public final class ListMultilineTextFieldItemComponent: Component { self.autocapitalizationType = autocapitalizationType self.autocorrectionType = autocorrectionType self.characterLimit = characterLimit + self.allowEmptyLines = allowEmptyLines self.updated = updated self.textUpdateTransition = textUpdateTransition self.tag = tag @@ -103,6 +106,9 @@ public final class ListMultilineTextFieldItemComponent: Component { if lhs.characterLimit != rhs.characterLimit { return false } + if lhs.allowEmptyLines != rhs.allowEmptyLines { + return false + } if (lhs.updated == nil) != (rhs.updated == nil) { return false } @@ -212,6 +218,7 @@ public final class ListMultilineTextFieldItemComponent: Component { }, isOneLineWhenUnfocused: false, characterLimit: component.characterLimit, + allowEmptyLines: component.allowEmptyLines, formatMenuAvailability: .none, lockedFormatAction: { }, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index 0bdaf44587..9e43592add 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -345,19 +345,20 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: openTimestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat( dateFormatString: { value in - return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: []) + let text = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: []) + return presentationData.strings.PeerInfo_BusinessHours_StatusOpensOnDate(text.string) }, tomorrowFormatString: { value in - return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: []) + return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTomorrowAt(value).string, ranges: []) }, todayFormatString: { value in - return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: []) + return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: []) }, yesterdayFormatString: { value in - return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_YesterdayAt(value).string, ranges: []) + return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: []) } )).string - currentDayStatusText = presentationData.strings.PeerInfo_BusinessHours_StatusOpensOnDate(dateText).string + currentDayStatusText = dateText } break } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift index 93dc1ed4d4..b002aa3cc5 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift @@ -26,6 +26,7 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco private var pendingMessages: [PendingMessageContext] = [] private var historyViewDisposable: Disposable? + private var pendingHistoryViewDisposable: Disposable? let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>() private var nextUpdateIsHoleFill: Bool = false @@ -43,10 +44,14 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco context.disposable.dispose() } self.historyViewDisposable?.dispose() + self.pendingHistoryViewDisposable?.dispose() } private func updateHistoryViewRequest(reload: Bool) { if let shortcutId = self.shortcutId { + self.pendingHistoryViewDisposable?.dispose() + self.pendingHistoryViewDisposable = nil + if self.historyViewDisposable == nil || reload { self.historyViewDisposable?.dispose() @@ -74,11 +79,28 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco }) } } else { - if self.sourceHistoryView == nil { + self.historyViewDisposable?.dispose() + self.historyViewDisposable = nil + + self.pendingHistoryViewDisposable = (self.context.account.viewTracker.pendingQuickReplyMessagesViewForLocation(shortcut: self.shortcut) + |> deliverOn(self.queue)).start(next: { [weak self] view, _, _ in + guard let self else { + return + } + + let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill + self.nextUpdateIsHoleFill = false + + self.sourceHistoryView = view + + self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic) + }) + + /*if self.sourceHistoryView == nil { let sourceHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: [], holeEarlier: false, holeLater: false, isLoading: false) self.sourceHistoryView = sourceHistoryView self.updateHistoryView(updateType: .Initial) - } + }*/ } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index 2b10c46039..7e9fe2c3f2 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -257,9 +257,9 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { switch component.mode { case .greeting: var greetingMessage: TelegramBusinessGreetingMessage? - if self.isOn, let currentShortcut = self.currentShortcut { + if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id { greetingMessage = TelegramBusinessGreetingMessage( - shortcutId: currentShortcut.id, + shortcutId: shortcutId, recipients: recipients, inactivityDays: self.inactivityDays ) @@ -267,7 +267,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { let _ = component.context.engine.accountData.updateBusinessGreetingMessage(greetingMessage: greetingMessage).startStandalone() case .away: var awayMessage: TelegramBusinessAwayMessage? - if self.isOn, let currentShortcut = self.currentShortcut { + if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id { let mappedSchedule: TelegramBusinessAwayMessage.Schedule switch self.schedule { case .always: @@ -282,7 +282,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { } } awayMessage = TelegramBusinessAwayMessage( - shortcutId: currentShortcut.id, + shortcutId: shortcutId, recipients: recipients, schedule: mappedSchedule, sendWhenOffline: self.sendWhenOffline @@ -654,7 +654,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { self.currentShortcut = component.initialData.shortcutMessageList.items.first(where: { $0.shortcut == shortcutName }) - self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList() + self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false) |> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in guard let self else { return @@ -1623,7 +1623,7 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC TelegramEngine.EngineData.Item.Peer.BusinessAwayMessage(id: context.account.peerId), TelegramEngine.EngineData.Item.Peer.BusinessHours(id: context.account.peerId) ), - context.engine.accountData.shortcutMessageList() + context.engine.accountData.shortcutMessageList(onlyRemote: true) |> take(1) ) |> mapToSignal { data, shortcutMessageList -> Signal in diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 473fe28dcb..22d8f7e0ca 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -54,6 +54,7 @@ final class QuickReplySetupScreenComponent: Component { enum Id: Hashable { case add case item(Int32) + case pendingItem(String) } var stableId: Id { @@ -61,7 +62,11 @@ final class QuickReplySetupScreenComponent: Component { case .add: return .add case let .item(item, _, _, _, _): - return .item(item.id) + if let itemId = item.id { + return .item(itemId) + } else { + return .pendingItem(item.shortcut) + } } } @@ -126,13 +131,17 @@ final class QuickReplySetupScreenComponent: Component { guard let listNode, let parentView = listNode.parentView else { return } - parentView.toggleShortcutSelection(id: item.id) + if let itemId = item.id { + parentView.toggleShortcutSelection(id: itemId) + } }, togglePeersSelection: { [weak listNode] _, _ in guard let listNode, let parentView = listNode.parentView else { return } - parentView.toggleShortcutSelection(id: item.id) + if let itemId = item.id { + parentView.toggleShortcutSelection(id: itemId) + } }, additionalCategorySelected: { _ in }, @@ -158,7 +167,9 @@ final class QuickReplySetupScreenComponent: Component { guard let listNode, let parentView = listNode.parentView else { return } - parentView.openDeleteShortcuts(ids: [item.id]) + if let itemId = item.id { + parentView.openDeleteShortcuts(ids: [itemId]) + } }, deletePeerThread: { _, _ in }, @@ -208,7 +219,9 @@ final class QuickReplySetupScreenComponent: Component { guard let listNode, let parentView = listNode.parentView else { return } - parentView.openEditShortcut(id: item.id, currentValue: item.shortcut) + if let itemId = item.id { + parentView.openEditShortcut(id: itemId, currentValue: item.shortcut) + } } ) @@ -406,6 +419,8 @@ final class QuickReplySetupScreenComponent: Component { return true case let .item(id): return !pendingRemoveItems.contains(id) + case .pendingItem: + return true } } } @@ -874,7 +889,7 @@ final class QuickReplySetupScreenComponent: Component { self.accountPeer = component.initialData.accountPeer self.shortcutMessageList = component.initialData.shortcutMessageList - self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList() + self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false) |> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in guard let self else { return @@ -937,7 +952,11 @@ final class QuickReplySetupScreenComponent: Component { ) if let emptyStateView = emptyState.view { if emptyStateView.superview == nil { - self.addSubview(emptyStateView) + if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(emptyStateView, belowSubview: navigationBarComponentView) + } else { + self.addSubview(emptyStateView) + } } emptyStateTransition.setFrame(view: emptyStateView, frame: emptyStateFrame) } @@ -1168,7 +1187,11 @@ final class QuickReplySetupScreenComponent: Component { continue } } - entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: self.selectedIds.contains(item.id))) + var isItemSelected = false + if let itemId = item.id { + isItemSelected = self.selectedIds.contains(itemId) + } + entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: isItemSelected)) } } contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate) @@ -1198,7 +1221,11 @@ final class QuickReplySetupScreenComponent: Component { let emptySearchStateFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - emptySearchStateSize.width) * 0.5), y: navigationHeight + floor((availableSize.height - emptySearchStateBottomInset - navigationHeight) * 0.5)), size: emptySearchStateSize) if let emptySearchStateView = emptySearchState.view { if emptySearchStateView.superview == nil { - self.addSubview(emptySearchStateView) + if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(emptySearchStateView, belowSubview: navigationBarComponentView) + } else { + self.addSubview(emptySearchStateView) + } } emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center) emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size) @@ -1327,7 +1354,7 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer, Atta context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) ), - context.engine.accountData.shortcutMessageList() + context.engine.accountData.shortcutMessageList(onlyRemote: false) |> take(1) ) |> map { accountPeer, shortcutMessageList -> QuickReplySetupScreenInitialData in diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index 1fc26a7615..e663082df3 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -121,8 +121,10 @@ final class BusinessDaySetupScreenComponent: Component { return true } - if self.intersectingRanges.isEmpty { - return true + if self.isOpen { + if self.intersectingRanges.isEmpty { + return true + } } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index 28f3218ad6..95f51e3f2c 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -79,11 +79,13 @@ final class BusinessLocationSetupScreenComponent: Component { private var resetAddressText: String? private var isLoadingGeocodedAddress: Bool = false - private var geocodeAddressState: (address: String, disposable: Disposable)? + private var geocodeDisposable: Disposable? private var mapCoordinates: TelegramBusinessLocation.Coordinates? private var mapCoordinatesManuallySet: Bool = false + private var applyButtonItem: UIBarButtonItem? + override init(frame: CGRect) { self.scrollView = ScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -110,7 +112,7 @@ final class BusinessLocationSetupScreenComponent: Component { } deinit { - self.geocodeAddressState?.disposable.dispose() + self.geocodeDisposable?.dispose() } func scrollToTop() { @@ -122,24 +124,14 @@ final class BusinessLocationSetupScreenComponent: Component { return true } - var address = "" - if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View { - address = textView.currentText - } + let businessLocation = self.currentBusinessLocation() - var businessLocation: TelegramBusinessLocation? - if !address.isEmpty || self.mapCoordinates != nil { - businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates) - } - - if businessLocation != nil && address.isEmpty { + if businessLocation != component.initialValue { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_Text, actions: [ + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_Text, actions: [ TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { }), - TextAlertAction(type: .destructiveAction, title: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_ResetAction, action: { - let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: nil).startStandalone() - + TextAlertAction(type: .destructiveAction, title: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_ResetAction, action: { complete() }) ]), in: .window(.root)) @@ -147,8 +139,6 @@ final class BusinessLocationSetupScreenComponent: Component { return false } - let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone() - return true } @@ -186,16 +176,51 @@ final class BusinessLocationSetupScreenComponent: Component { } } + private func currentBusinessLocation() -> TelegramBusinessLocation? { + var address = "" + if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View { + address = textView.currentText + } + + var businessLocation: TelegramBusinessLocation? + if !address.isEmpty || self.mapCoordinates != nil { + businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates) + } + return businessLocation + } + private func openLocationPicker() { + var initialLocation: CLLocationCoordinate2D? + var initialGeocodedLocation: String? + if let mapCoordinates = self.mapCoordinates { + initialLocation = CLLocationCoordinate2D(latitude: mapCoordinates.latitude, longitude: mapCoordinates.longitude) + } else if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View, textView.currentText.count >= 2 { + initialGeocodedLocation = textView.currentText + } + + if let initialGeocodedLocation { + self.isLoadingGeocodedAddress = true + self.state?.updated(transition: .immediate) + + self.geocodeDisposable?.dispose() + self.geocodeDisposable = (geocodeLocation(address: initialGeocodedLocation) + |> deliverOnMainQueue).startStrict(next: { [weak self] venues in + guard let self else { + return + } + self.isLoadingGeocodedAddress = false + self.state?.updated(transition: .immediate) + self.presentLocationPicker(initialLocation: venues?.first?.location?.coordinate) + }) + } else { + self.presentLocationPicker(initialLocation: initialLocation) + } + } + + private func presentLocationPicker(initialLocation: CLLocationCoordinate2D?) { guard let component = self.component else { return } - - 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 @@ -212,41 +237,30 @@ 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 + @objc private func savePressed() { + guard let component = self.component, let environment = self.environment else { + return } - 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) - } - })) - } + var address = "" + if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View { + address = textView.currentText } + + let businessLocation = self.currentBusinessLocation() + + if businessLocation != nil && address.isEmpty { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_Text, actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: { + }) + ]), in: .window(.root)) + + return + } + + let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone() + environment.controller()?.dismiss() } func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -391,7 +405,8 @@ final class BusinessLocationSetupScreenComponent: Component { placeholder: environment.strings.BusinessLocationSetup_AddressPlaceholder, autocapitalizationType: .none, autocorrectionType: .no, - characterLimit: 64, + characterLimit: 256, + allowEmptyLines: false, updated: { _ in }, textUpdateTransition: .spring(duration: 0.4), @@ -422,6 +437,14 @@ final class BusinessLocationSetupScreenComponent: Component { contentHeight += sectionSpacing var mapSectionItems: [AnyComponentWithIdentity] = [] + + let mapSelectionAccessory: ListActionItemComponent.Accessory? + if self.isLoadingGeocodedAddress { + mapSelectionAccessory = .activity + } else { + mapSelectionAccessory = .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.mapCoordinates != nil, isInteractive: self.mapCoordinates != nil)) + } + mapSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ @@ -434,7 +457,7 @@ final class BusinessLocationSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.mapCoordinates != nil, isInteractive: self.mapCoordinates != nil)), + accessory: mapSelectionAccessory, action: { [weak self] _ in guard let self else { return @@ -573,6 +596,26 @@ final class BusinessLocationSetupScreenComponent: Component { self.updateScrolling(transition: transition) + if let controller = environment.controller() as? BusinessLocationSetupScreen { + let businessLocation = self.currentBusinessLocation() + + if businessLocation == component.initialValue { + if controller.navigationItem.rightBarButtonItem != nil { + controller.navigationItem.setRightBarButton(nil, animated: true) + } + } else { + let applyButtonItem: UIBarButtonItem + if let current = self.applyButtonItem { + applyButtonItem = current + } else { + applyButtonItem = UIBarButtonItem(title: environment.strings.Common_Save, style: .done, target: self, action: #selector(self.savePressed)) + } + if controller.navigationItem.rightBarButtonItem !== applyButtonItem { + controller.navigationItem.setRightBarButton(applyButtonItem, animated: true) + } + } + } + return availableSize } } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 4ba8b431ac..7fa657812c 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -97,6 +97,7 @@ public final class TextFieldComponent: Component { public let resetText: NSAttributedString? public let isOneLineWhenUnfocused: Bool public let characterLimit: Int? + public let allowEmptyLines: Bool public let formatMenuAvailability: FormatMenuAvailability public let lockedFormatAction: () -> Void public let present: (ViewController) -> Void @@ -114,6 +115,7 @@ public final class TextFieldComponent: Component { resetText: NSAttributedString?, isOneLineWhenUnfocused: Bool, characterLimit: Int? = nil, + allowEmptyLines: Bool = true, formatMenuAvailability: FormatMenuAvailability, lockedFormatAction: @escaping () -> Void, present: @escaping (ViewController) -> Void, @@ -130,6 +132,7 @@ public final class TextFieldComponent: Component { self.resetText = resetText self.isOneLineWhenUnfocused = isOneLineWhenUnfocused self.characterLimit = characterLimit + self.allowEmptyLines = allowEmptyLines self.formatMenuAvailability = formatMenuAvailability self.lockedFormatAction = lockedFormatAction self.present = present @@ -167,6 +170,9 @@ public final class TextFieldComponent: Component { if lhs.characterLimit != rhs.characterLimit { return false } + if lhs.allowEmptyLines != rhs.allowEmptyLines { + return false + } if lhs.formatMenuAvailability != rhs.formatMenuAvailability { return false } @@ -553,6 +559,13 @@ public final class TextFieldComponent: Component { return false } } + if !component.allowEmptyLines { + let string = self.inputState.inputText.string as NSString + let updatedString = string.replacingCharacters(in: range, with: text) + if updatedString.range(of: "\n\n") != nil { + return false + } + } return true } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 8c7881996f..c0160b6d7c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -13984,6 +13984,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } + + if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, let messageLimit = customChatContents.messageLimit { + if let originalHistoryView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, originalHistoryView.entries.count + mappedMessages.count > messageLimit { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_QuickReplyMediaMessageLimitReachedText(Int32(messageLimit)), actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) + ]), in: .window(.root)) + return + } + } let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift index bacdf01df7..57a7666b2a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -34,7 +34,7 @@ extension ChatControllerImpl { return } - let _ = (self.context.engine.accountData.shortcutMessageList() + let _ = (self.context.engine.accountData.shortcutMessageList(onlyRemote: false) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in guard let self else { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 74e5ff6665..7de7df129b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -271,6 +271,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var derivedLayoutState: ChatControllerNodeDerivedLayoutState? + private var loadMoreSearchResultsDisposable: Disposable? + private var isLoadingValue: Bool = false private var isLoadingEarlier: Bool = false private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) { @@ -889,6 +891,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.displayVideoUnmuteTipDisposable?.dispose() self.inputMediaNodeDataDisposable?.dispose() self.inlineSearchResultsReadyDisposable?.dispose() + self.loadMoreSearchResultsDisposable?.dispose() } override func didLoad() { @@ -1636,6 +1639,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { isSelectionEnabled = false } else if self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { isSelectionEnabled = false + } else if case .customChatContents = self.chatLocation { + isSelectionEnabled = false } self.historyNode.isSelectionGestureEnabled = isSelectionEnabled @@ -2693,6 +2698,51 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } return foundLocalPeers + }, + loadMoreSearchResults: { [weak self] in + guard let self, let controller = self.controller else { + return + } + guard let currentSearchState = controller.searchState, let currentResultsState = controller.presentationInterfaceState.search?.resultsState else { + return + } + + self.loadMoreSearchResultsDisposable?.dispose() + self.loadMoreSearchResultsDisposable = (self.context.engine.messages.searchMessages(location: currentSearchState.location, query: currentSearchState.query, state: currentResultsState.state) + |> deliverOnMainQueue).startStrict(next: { [weak self] results, updatedState in + guard let self, let controller = self.controller else { + return + } + + controller.searchResult.set(.single((results, updatedState, currentSearchState.location))) + + var navigateIndex: MessageIndex? + controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search { + let messageIndices = results.messages.map({ $0.index }).sorted() + var currentIndex = messageIndices.last + if let previousResultId = data.resultsState?.currentId { + for index in messageIndices { + if index.id >= previousResultId { + currentIndex = index + break + } + } + } + navigateIndex = currentIndex + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: updatedState, totalCount: results.totalCount, completed: results.completed))) + } else { + return current + } + }) + if let navigateIndex = navigateIndex { + switch controller.chatLocation { + case .peer, .replyThread, .customChatContents: + controller.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true) + } + } + controller.updateItemNodesSearchTextHighlightStates() + }) } )), environment: {}, diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index f4aafd30c8..8772e663cc 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -135,7 +135,7 @@ extension ChatControllerImpl { if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted { buttons = combineLatest( self.context.engine.messages.attachMenuBots(), - self.context.engine.accountData.shortcutMessageList() |> take(1) + self.context.engine.accountData.shortcutMessageList(onlyRemote: true) |> take(1) ) |> map { attachMenuBots, shortcutMessageList in var buttons = availableButtons diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index d9ed4448c4..2572058c67 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -237,7 +237,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee if let user = peer as? TelegramUser, user.botInfo == nil { context.account.viewTracker.keepQuickRepliesApproximatelyUpdated() - shortcuts = context.engine.accountData.shortcutMessageList() + shortcuts = context.engine.accountData.shortcutMessageList(onlyRemote: true) |> map { shortcutMessageList -> [ShortcutMessageList.Item] in return shortcutMessageList.items.filter { item in return item.shortcut.hasPrefix(normalizedQuery) diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index da23ef1a9e..18b7c7bed8 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -66,7 +66,11 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { case let .command(command): return .command(command) case let .shortcut(shortcut): - return .shortcut(shortcut.id) + if let shortcutId = shortcut.id { + return .shortcut(shortcutId) + } else { + return .shortcut(0) + } } } } @@ -375,7 +379,9 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } } case let .shortcut(shortcut): - interfaceInteraction.sendShortcut(shortcut.id) + if let shortcutId = shortcut.id { + interfaceInteraction.sendShortcut(shortcutId) + } } }, openEditShortcuts: { [weak self] in guard let self, let interfaceInteraction = self.interfaceInteraction else { From ebd7459fe21cabe477522c57851886a186751086 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 5 Mar 2024 15:50:16 +0400 Subject: [PATCH 2/2] Fix text layout --- submodules/Display/Source/TextNode.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index c226bb6144..b469147b60 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1440,7 +1440,8 @@ open class TextNode: ASDisplayNode { let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount)) var lineAscent: CGFloat = 0.0 var lineDescent: CGFloat = 0.0 - let lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil) + var lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil) + lineWidth = min(lineWidth, constrainedSegmentWidth - additionalSegmentRightInset) var isRTL = false let glyphRuns = CTLineGetGlyphRuns(line) as NSArray