From 50339b3c4c62d9e5f08aa644fb459946fc0f7fd8 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 23 Feb 2024 23:12:28 +0400 Subject: [PATCH 1/3] [WIP] Business --- .../Sources/Node/ChatListItem.swift | 101 +++++++++++++++-- .../Sources/Node/ChatListNode.swift | 10 +- .../ChatPresentationInterfaceState.swift | 2 + .../ChatItemGalleryFooterContentNode.swift | 5 + .../SyncCore/SyncCore_Namespaces.swift | 7 ++ .../TelegramEngineAccountData.swift | 8 ++ .../Messages/QuickReplyMessages.swift | 2 +- .../TelegramEngine/Messages/TimeZones.swift | 106 ++++++++++++++++++ .../Sources/ChatListHeaderComponent.swift | 21 +++- .../Sources/PlainButtonComponent.swift | 7 +- ...aticBusinessMessageListItemComponent.swift | 3 +- .../QuickReplyEmptyStateComponent.swift | 2 +- .../Sources/QuickReplySetupScreen.swift | 10 +- .../Sources/BusinessHoursSetupScreen.swift | 79 +++++++++++-- .../Sources/TimezoneSelectionScreenNode.swift | 52 +++++---- .../ChatControllerNavigateToMessage.swift | 4 + .../Sources/ChatControllerNode.swift | 2 +- .../Sources/ChatHistoryEntriesForView.swift | 4 +- .../Sources/ChatHistoryListNode.swift | 25 ++++- .../ChatInterfaceStateAccessoryPanels.swift | 19 +++- .../ChatInterfaceStateContextMenus.swift | 6 +- .../CommandChatInputContextPanelNode.swift | 3 +- 22 files changed, 411 insertions(+), 67 deletions(-) create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Messages/TimeZones.swift diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 6d79803d62..6af430ca12 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -94,11 +94,13 @@ public enum ChatListItemContent { public var commandPrefix: String? public var searchQuery: String? public var messageCount: Int? + public var hideSeparator: Bool - public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?) { + public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?, hideSeparator: Bool) { self.commandPrefix = commandPrefix self.searchQuery = searchQuery self.messageCount = messageCount + self.hideSeparator = hideSeparator } } @@ -1166,6 +1168,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var currentItemHeight: CGFloat? let forwardedIconNode: ASImageNode let textNode: TextNodeWithEntities + var trailingTextBadgeNode: TextNode? + var trailingTextBadgeBackground: UIImageView? var dustNode: InvisibleInkDustNode? let inputActivitiesNode: ChatListInputActivitiesNode let dateNode: TextNode @@ -1437,6 +1441,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.textNode = TextNodeWithEntities() self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.displaysAsynchronously = true + self.textNode.textNode.layer.anchorPoint = CGPoint() self.inputActivitiesNode = ChatListInputActivitiesNode() self.inputActivitiesNode.isUserInteractionEnabled = false @@ -1823,6 +1828,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) + let makeTrailingTextBadgeLayout = TextNode.asyncLayout(self.trailingTextBadgeNode) let titleLayout = TextNode.asyncLayout(self.titleNode) let authorLayout = self.authorNode.asyncLayout() let makeMeasureLayout = TextNode.asyncLayout(self.measureNode) @@ -2073,7 +2079,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { avatarDiameter = 40.0 - avatarLeftInset = 18.0 + avatarDiameter + avatarLeftInset = 17.0 + avatarDiameter } else { if item.interaction.isInlineMode { avatarLeftInset = 12.0 @@ -2666,7 +2672,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { topIndex = peerData.messages.first?.index } if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { - if let messageCount = customMessageListData.messageCount { + if let messageCount = customMessageListData.messageCount, customMessageListData.commandPrefix == nil { dateText = "\(messageCount)" } else { dateText = " " @@ -2990,9 +2996,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let (authorLayout, authorApply) = authorLayout(item.context, rawContentWidth - badgeSize, item.presentationData.theme, effectiveAuthorTitle, forumThreads) + var textBottomRightCutout: CGFloat = 0.0 + + let trailingTextBadgeInsets = UIEdgeInsets(top: 2.0 - UIScreenPixel, left: 5.0, bottom: 2.0 - UIScreenPixel, right: 5.0) + var trailingTextBadgeLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil, let messageCount = customMessageListData.messageCount, messageCount > 1 { + let trailingText: String + //TODO:localize + trailingText = "+\(messageCount - 1) MORE" + let trailingAttributedText = NSAttributedString(string: trailingText, font: Font.regular(12.0), textColor: theme.messageTextColor) + let (layout, apply) = makeTrailingTextBadgeLayout(TextNodeLayoutArguments(attributedString: trailingAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + trailingTextBadgeLayoutAndApply = (layout, apply) + textBottomRightCutout += layout.size.width + 4.0 + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right + } + var textCutout: TextNodeCutout? - if !textLeftCutout.isZero { - textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil) + if !textLeftCutout.isZero || !textBottomRightCutout.isZero { + textCutout = TextNodeCutout(topLeft: textLeftCutout.isZero ? nil : CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: textBottomRightCutout.isZero ? nil : CGSize(width: textBottomRightCutout, height: 10.0)) } var textMaxWidth = rawContentWidth - badgeSize @@ -3552,6 +3572,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let _ = measureApply() let _ = dateApply() + var currentTextSnapshotView: UIView? + if transition.isAnimated, let currentItem, currentItem.editing != item.editing, strongSelf.textNode.textNode.cachedLayout?.linesRects() != textLayout.linesRects() { + if let textSnapshotView = strongSelf.textNode.textNode.view.snapshotContentTree() { + textSnapshotView.layer.anchorPoint = CGPoint() + currentTextSnapshotView = textSnapshotView + strongSelf.textNode.textNode.view.superview?.insertSubview(textSnapshotView, aboveSubview: strongSelf.textNode.textNode.view) + textSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSnapshotView] _ in + textSnapshotView?.removeFromSuperview() + }) + strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } + } + let _ = textApply(TextNodeWithEntities.Arguments( context: item.context, cache: item.interaction.animationCache, @@ -3572,7 +3605,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size) - if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil { + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil, customMessageListData.commandPrefix == nil { dateFrame.origin.x -= 10.0 let dateDisclosureIconView: UIImageView @@ -3818,6 +3851,60 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.authorNode.assignParentNode(parentNode: nil) } + if let currentTextSnapshotView { + transition.updatePosition(layer: currentTextSnapshotView.layer, position: textNodeFrame.origin) + } + + if let trailingTextBadgeLayoutAndApply { + let badgeSize = CGSize(width: trailingTextBadgeLayoutAndApply.0.size.width + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right, height: trailingTextBadgeLayoutAndApply.0.size.height + trailingTextBadgeInsets.top + trailingTextBadgeInsets.bottom - UIScreenPixel) + + var badgeFrame: CGRect + if textLayout.numberOfLines > 1 { + badgeFrame = CGRect(origin: CGPoint(x: textLayout.trailingLineWidth, y: textNodeFrame.height - 3.0 - badgeSize.height), size: badgeSize) + } else { + let firstLineFrame = textLayout.linesRects().first ?? CGRect(origin: CGPoint(), size: textNodeFrame.size) + badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: firstLineFrame.height + 5.0), size: badgeSize) + } + + if badgeFrame.origin.x + badgeFrame.width >= textNodeFrame.width - 2.0 - 10.0 { + badgeFrame.origin.x = textNodeFrame.width - 2.0 - badgeFrame.width + } + + let trailingTextBadgeBackground: UIImageView + if let current = strongSelf.trailingTextBadgeBackground { + trailingTextBadgeBackground = current + } else { + trailingTextBadgeBackground = UIImageView(image: tagBackgroundImage) + strongSelf.trailingTextBadgeBackground = trailingTextBadgeBackground + strongSelf.textNode.textNode.view.addSubview(trailingTextBadgeBackground) + } + trailingTextBadgeBackground.tintColor = theme.pinnedItemBackgroundColor.mixedWith(theme.unreadBadgeInactiveBackgroundColor, alpha: 0.1) + + trailingTextBadgeBackground.frame = badgeFrame + + let trailingTextBadgeFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + trailingTextBadgeInsets.left, y: badgeFrame.minY + trailingTextBadgeInsets.top), size: trailingTextBadgeLayoutAndApply.0.size) + let trailingTextBadgeNode = trailingTextBadgeLayoutAndApply.1() + if strongSelf.trailingTextBadgeNode !== trailingTextBadgeNode { + strongSelf.trailingTextBadgeNode?.removeFromSupernode() + strongSelf.trailingTextBadgeNode = trailingTextBadgeNode + + strongSelf.textNode.textNode.addSubnode(trailingTextBadgeNode) + + trailingTextBadgeNode.layer.anchorPoint = CGPoint() + } + + trailingTextBadgeNode.frame = trailingTextBadgeFrame + } else { + if let trailingTextBadgeNode = strongSelf.trailingTextBadgeNode { + strongSelf.trailingTextBadgeNode = nil + trailingTextBadgeNode.removeFromSupernode() + } + if let trailingTextBadgeBackground = strongSelf.trailingTextBadgeBackground { + strongSelf.trailingTextBadgeBackground = nil + trailingTextBadgeBackground.removeFromSuperview() + } + } + if !itemTags.isEmpty { let itemTagListFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.maxY - 12.0), size: CGSize(width: contentRect.width, height: 20.0)) @@ -4127,7 +4214,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { - if customMessageListData.messageCount != nil { + if customMessageListData.hideSeparator { strongSelf.separatorNode.isHidden = true } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 7306140ff1..82c0cfe1fc 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -433,7 +433,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL }, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, displayAsTopicList: peerEntry.displayAsTopicList, - tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) + tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -811,7 +811,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL }, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, displayAsTopicList: peerEntry.displayAsTopicList, - tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) + tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -4206,7 +4206,11 @@ func hideChatListContacts(context: AccountContext) { let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).startStandalone() } -func chatListItemTags(accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] { +func chatListItemTags(location: ChatListControllerLocation, accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] { + if case .chatList = location { + } else { + return [] + } guard let chatListFilters, !chatListFilters.isEmpty else { return [] } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 35ccabb4ea..75406cdcf6 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -1202,6 +1202,8 @@ public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bo } else { return false } + } else if case .customChatContents = state.chatLocation { + return true } else { return false } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index f1262a1777..62da3c1a45 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -816,6 +816,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) { self.currentMessage = message + var displayInfo = displayInfo + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + displayInfo = false + } + var canDelete: Bool var canShare = !message.containsSecretMedia diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 70d9a83496..958ac84482 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -285,6 +285,7 @@ private enum PreferencesKeyValues: Int32 { case didCacheSavedMessageTagsPrefix = 34 case displaySavedChatsAsTopics = 35 case shortcutMessages = 37 + case timezoneList = 38 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -474,6 +475,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.shortcutMessages.rawValue) return key } + + public static func timezoneList() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.timezoneList.rawValue) + return key + } } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index c2986ca224..46cfb507e1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -185,5 +185,13 @@ public extension TelegramEngine { public func sendMessageShortcut(peerId: EnginePeer.Id, id: Int32) { let _ = _internal_sendMessageShortcut(account: self.account, peerId: peerId, id: id).startStandalone() } + + public func cachedTimeZoneList() -> Signal { + return _internal_cachedTimeZoneList(account: self.account) + } + + public func keepCachedTimeZoneListUpdated() -> Signal { + return _internal_keepCachedTimeZoneListUpdated(account: self.account) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index e8d6cbab5e..a6b052fb01 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -229,7 +229,7 @@ func _internal_shortcutMessageList(account: Account) -> Signal Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.utcOffset != rhs.utcOffset { + return false + } + return true + } + } + + public let items: [Item] + public let hashValue: Int32 + + public init(items: [Item], hashValue: Int32) { + self.items = items + self.hashValue = hashValue + } + + public static func ==(lhs: TimeZoneList, rhs: TimeZoneList) -> Bool { + if lhs === rhs { + return true + } + if lhs.items != rhs.items { + return false + } + if lhs.hashValue != rhs.hashValue { + return false + } + return true + } +} + +func _internal_cachedTimeZoneList(account: Account) -> Signal { + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.timezoneList()])) + return account.postbox.combinedView(keys: [viewKey]) + |> map { views -> TimeZoneList? in + guard let view = views.views[viewKey] as? PreferencesView else { + return nil + } + guard let value = view.values[PreferencesKeys.timezoneList()]?.get(TimeZoneList.self) else { + return nil + } + return value + } +} + +func _internal_keepCachedTimeZoneListUpdated(account: Account) -> Signal { + let updateSignal = _internal_cachedTimeZoneList(account: account) + |> take(1) + |> mapToSignal { list -> Signal in + return account.network.request(Api.functions.help.getTimezonesList(hash: list?.hashValue ?? 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + + return account.postbox.transaction { transaction in + switch result { + case let .timezonesList(timezones, hash): + var items: [TimeZoneList.Item] = [] + for item in timezones { + switch item { + case let .timezone(id, name, utcOffset): + items.append(TimeZoneList.Item(id: id, title: name, utcOffset: utcOffset)) + } + } + transaction.setPreferencesEntry(key: PreferencesKeys.timezoneList(), value: PreferencesEntry(TimeZoneList(items: items, hashValue: hash))) + case .timezonesListNotModified: + break + } + } + |> ignoreValues + } + } + + return updateSignal +} diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index e41442d0ae..ae0f24162d 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -267,7 +267,9 @@ public final class ChatListHeaderComponent: Component { } func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: Transition) -> CGSize { - self.titleView.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + let titleText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + let titleTextUpdated = self.titleView.attributedText != titleText + self.titleView.attributedText = titleText let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0)) self.accessibilityLabel = title @@ -287,7 +289,12 @@ public final class ChatListHeaderComponent: Component { transition.setPosition(view: self.arrowView, position: arrowFrame.center) transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size)) - transition.setFrame(view: self.titleView, frame: CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)) + let titleFrame = CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + if titleTextUpdated { + self.titleView.frame = titleFrame + } else { + transition.setFrame(view: self.titleView, frame: titleFrame) + } return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height) } @@ -479,7 +486,9 @@ public final class ChatListHeaderComponent: Component { transition.setPosition(view: self.titleScaleContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setBounds(view: self.titleScaleContainer, bounds: CGRect(origin: self.titleScaleContainer.bounds.origin, size: size)) - self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + let titleText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + let titleTextUpdated = self.titleTextView.attributedText != titleText + self.titleTextView.attributedText = titleText let buttonSpacing: CGFloat = 8.0 @@ -616,7 +625,11 @@ public final class ChatListHeaderComponent: Component { let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height)) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleTextSize.width) / 2.0) + sideContentWidth, y: floor((size.height - titleTextSize.height) / 2.0)), size: titleTextSize) - transition.setFrame(view: self.titleTextView, frame: titleFrame) + if titleTextUpdated { + self.titleTextView.frame = titleFrame + } else { + transition.setFrame(view: self.titleTextView, frame: titleFrame) + } if let titleComponent = content.titleComponent { var titleContentTransition = transition diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index cb3ee96e76..f91488ec89 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -227,7 +227,12 @@ public final class PlainButtonComponent: Component { } let contentFrame = CGRect(origin: CGPoint(x: component.contentInsets.left + floor((size.width - component.contentInsets.left - component.contentInsets.right - contentSize.width) * 0.5), y: component.contentInsets.top + floor((size.height - component.contentInsets.top - component.contentInsets.bottom - contentSize.height) * 0.5)), size: contentSize) - contentTransition.setPosition(view: contentView, position: CGPoint(x: contentFrame.minX + contentFrame.width * contentView.layer.anchorPoint.x, y: contentFrame.minY + contentFrame.height * contentView.layer.anchorPoint.y)) + let contentPosition = CGPoint(x: contentFrame.minX + contentFrame.width * contentView.layer.anchorPoint.x, y: contentFrame.minY + contentFrame.height * contentView.layer.anchorPoint.y) + if !component.animateContents && (abs(contentView.center.x - contentPosition.x) <= 2.0 && abs(contentView.center.y - contentPosition.y) <= 2.0){ + contentView.center = contentPosition + } else { + contentTransition.setPosition(view: contentView, position: contentPosition) + } if component.animateContents { contentTransition.setBounds(view: contentView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size)) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 123a1d0087..ad53e21b11 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -238,7 +238,8 @@ final class GreetingMessageListItemComponent: Component { customMessageListData: ChatListItemContent.CustomMessageListData( commandPrefix: nil, searchQuery: nil, - messageCount: component.count + messageCount: component.count, + hideSeparator: true ) )), editing: false, diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift index a145ec7be2..f0a9d10fca 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift @@ -109,7 +109,7 @@ final class QuickReplyEmptyStateComponent: Component { transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "WriteEmoji"), - loop: false + loop: true )), environment: {}, containerSize: CGSize(width: 120.0, height: 120.0) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 010f4c6fca..595483c17d 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -250,7 +250,8 @@ final class QuickReplySetupScreenComponent: Component { customMessageListData: ChatListItemContent.CustomMessageListData( commandPrefix: "/\(item.shortcut)", searchQuery: nil, - messageCount: nil + messageCount: item.totalCount, + hideSeparator: false ) )), editing: isEditing, @@ -744,13 +745,14 @@ final class QuickReplySetupScreenComponent: Component { tabsNodeIsSearch: false, accessoryPanelContainer: nil, accessoryPanelContainerHeight: 0.0, - activateSearch: { [weak self] searchContentNode in + activateSearch: { [weak self] _ in guard let self else { return } - self.isSearchDisplayControllerActive = true - self.state?.updated(transition: .spring(duration: 0.4)) + let _ = self + //self.isSearchDisplayControllerActive = true + //self.state?.updated(transition: .spring(duration: 0.4)) }, openStatusSetup: { _ in }, diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift index fb5ffcfabc..5f8839c110 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift @@ -82,11 +82,14 @@ final class BusinessHoursSetupScreenComponent: Component { } var timezoneId: String - var days: [Day] + private(set) var days: [Day] + private(set) var intersectingDays = Set() init(timezoneId: String, days: [Day]) { self.timezoneId = timezoneId self.days = days + + self.validate() } init(businessHours: TelegramBusinessHours) { @@ -107,6 +110,19 @@ final class BusinessHoursSetupScreenComponent: Component { }) } } + + self.validate() + } + + mutating func validate() { + self.intersectingDays.removeAll() + + + } + + mutating func update(days: [Day]) { + self.days = days + self.validate() } func asBusinessHours() throws -> TelegramBusinessHours { @@ -165,6 +181,10 @@ final class BusinessHoursSetupScreenComponent: Component { private var showHours: Bool = false private var daysState = DaysState(timezoneId: "", days: []) + private var timeZoneList: TimeZoneList? + private var timezonesDisposable: Disposable? + private var keepTimezonesUpdatedDisposable: Disposable? + override init(frame: CGRect) { self.scrollView = ScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -191,6 +211,8 @@ final class BusinessHoursSetupScreenComponent: Component { } deinit { + self.timezonesDisposable?.dispose() + self.keepTimezonesUpdatedDisposable?.dispose() } func scrollToTop() { @@ -210,6 +232,21 @@ final class BusinessHoursSetupScreenComponent: Component { } catch let error { let _ = error //TODO:localize + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Business hours are intersecting. Reset?", actions: [ + TextAlertAction(type: .genericAction, title: "Cancel", action: { + }), + TextAlertAction(type: .defaultAction, title: "Reset", action: { [weak self] in + guard let self else { + return + } + let _ = self + complete() + }) + ]), in: .window(.root)) + return false } } else { @@ -271,10 +308,20 @@ final class BusinessHoursSetupScreenComponent: Component { } else { self.showHours = false self.daysState.timezoneId = TimeZone.current.identifier - self.daysState.days = (0 ..< 7).map { _ in + self.daysState.update(days: (0 ..< 7).map { _ in return Day(ranges: []) - } + }) } + + self.timezonesDisposable = (component.context.engine.accountData.cachedTimeZoneList() + |> deliverOnMainQueue).start(next: { [weak self] timeZoneList in + guard let self else { + return + } + self.timeZoneList = timeZoneList + self.state?.updated(transition: .immediate) + }) + self.keepTimezonesUpdatedDisposable = component.context.engine.accountData.keepCachedTimeZoneListUpdated().startStrict() } let environment = environment[EnvironmentType.self].value @@ -508,11 +555,13 @@ final class BusinessHoursSetupScreenComponent: Component { return } if dayIndex < self.daysState.days.count { - if self.daysState.days[dayIndex].ranges == nil { - self.daysState.days[dayIndex].ranges = [] + var days = self.daysState.days + if days[dayIndex].ranges == nil { + days[dayIndex].ranges = [] } else { - self.daysState.days[dayIndex].ranges = nil + days[dayIndex].ranges = nil } + self.daysState.update(days: days) } self.state?.updated(transition: .immediate) })), @@ -529,7 +578,9 @@ final class BusinessHoursSetupScreenComponent: Component { return } if self.daysState.days[dayIndex] != day { - self.daysState.days[dayIndex] = day + var days = self.daysState.days + days[dayIndex] = day + self.daysState.update(days: days) self.state?.updated(transition: .immediate) } } @@ -569,6 +620,18 @@ final class BusinessHoursSetupScreenComponent: Component { daysContentHeight += daysSectionSize.height daysContentHeight += sectionSpacing + let timezoneValueText: String + if let timeZoneList = self.timeZoneList { + if let item = timeZoneList.items.first(where: { $0.id == self.daysState.timezoneId }) { + timezoneValueText = item.title + } else { + timezoneValueText = TimeZone(identifier: self.daysState.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? " " + } + } else { + //TODO:localize + timezoneValueText = "Loading..." + } + let timezoneSectionSize = self.timezoneSection.update( transition: transition, component: AnyComponent(ListSectionComponent( @@ -588,7 +651,7 @@ final class BusinessHoursSetupScreenComponent: Component { )), icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: TimeZone(identifier: self.daysState.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? self.daysState.timezoneId, + string: timezoneValueText, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemSecondaryTextColor )), diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift index 8656ab32ea..07cc080138 100644 --- a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift @@ -58,8 +58,7 @@ private func preparedLanguageListSearchContainerTransition(presentationData: Pre } private final class TimezoneListSearchContainerNode: SearchDisplayControllerContentNode { - private let timezoneData: TimezoneData - + private let timeZoneList: TimeZoneList private let dimNode: ASDisplayNode private let listNode: ListView @@ -78,8 +77,8 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont return true } - init(context: AccountContext, timezoneData: TimezoneData, action: @escaping (String) -> Void) { - self.timezoneData = timezoneData + init(context: AccountContext, timeZoneList: TimeZoneList, action: @escaping (String) -> Void) { + self.timeZoneList = timeZoneList let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -102,16 +101,18 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont self.addSubnode(self.dimNode) self.addSubnode(self.listNode) + let querySplitCharacterSet: CharacterSet = CharacterSet(charactersIn: " /.+") + let foundItems = self.searchQuery.get() - |> mapToSignal { query -> Signal<[TimezoneData.Item]?, NoError> in + |> mapToSignal { query -> Signal<[TimeZoneList.Item]?, NoError> in if let query, !query.isEmpty { let query = query.lowercased() - return .single(timezoneData.items.filter { item in + return .single(timeZoneList.items.filter { item in if item.id.lowercased().hasPrefix(query) { return true } - if item.title.lowercased().split(separator: " ").contains(where: { $0.hasPrefix(query) }) { + if item.title.lowercased().components(separatedBy: querySplitCharacterSet).contains(where: { $0.hasPrefix(query) }) { return true } @@ -132,7 +133,7 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont for item in items { entries.append(TimezoneListEntry( id: item.id, - offset: item.offset, + offset: Int(item.utcOffset), title: item.title )) } @@ -301,7 +302,7 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode { private let requestDeactivateSearch: () -> Void private let present: (ViewController, Any?) -> Void private let push: (ViewController) -> Void - private let timezoneData: TimezoneData + private var timeZoneList: TimeZoneList? private var didSetReady = false let _ready = ValuePromise() @@ -331,28 +332,32 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode { return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } - let timezoneData = TimezoneData() - self.timezoneData = timezoneData - super.init() self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.addSubnode(self.listNode) let previousEntriesHolder = Atomic<([TimezoneListEntry], PresentationTheme, PresentationStrings)?>(value: nil) - self.listDisposable = (self.presentationDataValue.get() - |> deliverOnMainQueue).start(next: { [weak self] presentationData in + self.listDisposable = (combineLatest(queue: .mainQueue(), + self.presentationDataValue.get(), + context.engine.accountData.cachedTimeZoneList() + ) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, timeZoneList in guard let strongSelf = self else { return } - + + strongSelf.timeZoneList = timeZoneList + var entries: [TimezoneListEntry] = [] - for item in timezoneData.items { - entries.append(TimezoneListEntry( - id: item.id, - offset: item.offset, - title: item.title - )) + if let timeZoneList { + for item in timeZoneList.items { + entries.append(TimezoneListEntry( + id: item.id, + offset: Int(item.utcOffset), + title: item.title + )) + } } entries.sort() @@ -447,8 +452,11 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode { guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else { return } + guard let timeZoneList = self.timeZoneList else { + return + } - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: TimezoneListSearchContainerNode(context: self.context, timezoneData: self.timezoneData, action: self.action), inline: true, cancel: { [weak self] in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: TimezoneListSearchContainerNode(context: self.context, timeZoneList: timeZoneList, action: self.action), inline: true, cancel: { [weak self] in self?.requestDeactivateSearch() }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index ef430bc3d9..092847181d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -104,6 +104,9 @@ extension ChatControllerImpl { if case let .peer(peerId) = self.chatLocation, messageLocation.peerId == peerId, !isPinnedMessages, !isScheduledMessages { forceInCurrentChat = true } + if case .customChatContents = self.chatLocation { + forceInCurrentChat = true + } if isPinnedMessages, let messageId = messageLocation.messageId { let _ = (combineLatest( @@ -205,6 +208,7 @@ extension ChatControllerImpl { if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) } } + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition) if delayCompletion { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 50e8551bb6..74e5ff6665 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1883,7 +1883,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { overlayNavigationBar.updateLayout(size: barFrame.size, transition: transition) } - var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) + var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top + 6.0, right: containerInsets.left) let listScrollIndicatorInsets = UIEdgeInsets(top: containerInsets.bottom + inputPanelsHeight, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) var childContentInsets: UIEdgeInsets = containerInsets diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 332d725dd7..060de3e3ec 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -546,12 +546,12 @@ func chatHistoryEntriesForView( let message = Message( stableId: UInt32.max - 1001 - UInt32(i), stableVersion: 0, - id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: 123 - Int32(i)), + id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: Int32.max - 100 - Int32(i)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, - timestamp: Int32(i), + timestamp: -Int32(i), flags: [.Incoming], tags: [], globalTags: [], diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index bbd4200418..055439b3f5 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1283,10 +1283,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } return true }) - |> mapToSignal { _ in + |> mapToSignal { location, _ -> Signal<((MessageHistoryView, ViewUpdateType), ChatHistoryLocationInput?), NoError> in return historyView + |> map { historyView in + return (historyView, location) + } } - |> map { view, update in + |> map { viewAndUpdate, location in + let (view, update) = viewAndUpdate + let version = currentViewVersion.modify({ value in if let value = value { return value + 1 @@ -1295,11 +1300,21 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } })! + var scrollPositionValue: ChatHistoryViewScrollPosition? + if let location { + switch location.content { + case let .Scroll(subject, _, _, scrollPosition, animated, highlight): + scrollPositionValue = .index(subject: subject, position: scrollPosition, directionHint: .Up, animated: animated, highlight: highlight, displayLink: false) + default: + break + } + } + return ( ChatHistoryViewUpdate.HistoryView( view: view, type: .Generic(type: update), - scrollPosition: nil, + scrollPosition: scrollPositionValue, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData( @@ -1309,10 +1324,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto cachedDataMessages: nil, readStateData: nil ), - id: 0 + id: location?.id ?? 0 ), version, - nil, + location, nil ) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index fe7d320454..ac201532fe 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -76,12 +76,21 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS replyPanelNode.interfaceInteraction = interfaceInteraction replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return replyPanelNode - } else if let peerId = chatPresentationInterfaceState.chatLocation.peerId { - let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: peerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) - panelNode.interfaceInteraction = interfaceInteraction - return panelNode } else { - return nil + var chatPeerId: EnginePeer.Id? + if let peerId = chatPresentationInterfaceState.chatLocation.peerId { + chatPeerId = peerId + } else if case .customChatContents = chatPresentationInterfaceState.chatLocation { + chatPeerId = context.account.peerId + } + + if let chatPeerId { + let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: chatPeerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } else { + return nil + } } } else { return nil diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 4f03d1f2c4..510ae5cca2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -271,6 +271,10 @@ private func canViewReadStats(message: Message, participantCount: Int?, isMessag } func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, accountPeerId: PeerId) -> Bool { + if case .customChatContents = chatPresentationInterfaceState.chatLocation { + return true + } + guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return false } @@ -338,7 +342,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS case .replyThread: canReply = true case .customChatContents: - canReply = false + canReply = true } return canReply } diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 6ecb5d7693..4b6334f13e 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -220,7 +220,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { customMessageListData: ChatListItemContent.CustomMessageListData( commandPrefix: "/\(shortcut.shortcut)", searchQuery: command.searchQuery.flatMap { "/\($0)"}, - messageCount: nil + messageCount: shortcut.totalCount, + hideSeparator: false ) )), editing: false, From 0ba75f81dece0a3dcf8d6ee213e970a5a05b7580 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 26 Feb 2024 18:43:15 +0400 Subject: [PATCH 2/3] [WIP] Business --- .../Sources/Node/ChatListItem.swift | 2 +- .../SyncCore/SyncCore_CachedUserData.swift | 38 ++-- .../Messages/TelegramEngineMessages.swift | 2 +- .../Sources/ListActionItemComponent.swift | 14 +- .../ListMultilineTextFieldItemComponent.swift | 2 +- .../Components/PeerInfo/PeerInfoScreen/BUILD | 1 + .../PeerInfoScreenBusinessHoursItem.swift | 94 +++++++++- .../Sources/PeerInfoScreen.swift | 4 +- .../Sources/BusinessDaySetupScreen.swift | 163 +++++++++--------- .../Sources/BusinessHoursSetupScreen.swift | 155 +++++++++++++---- .../Sources/BusinessLocationSetupScreen.swift | 50 ++++-- .../Sources/TimezoneSelectionScreenNode.swift | 17 +- .../Sources/StoryChatContent.swift | 6 +- .../Sources/StoryItemContentComponent.swift | 2 +- .../StoryItemSetContainerComponent.swift | 2 +- .../AddTimeIcon.imageset/Contents.json | 12 ++ .../AddTimeIcon.imageset/addclock_30.pdf | 117 +++++++++++++ .../Animations/BusinessHoursEmoji.tgs | Bin 0 -> 33321 bytes 18 files changed, 520 insertions(+), 161 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/addclock_30.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/BusinessHoursEmoji.tgs diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 6af430ca12..f0a3ca6946 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1441,7 +1441,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.textNode = TextNodeWithEntities() self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.displaysAsynchronously = true - self.textNode.textNode.layer.anchorPoint = CGPoint() + self.textNode.textNode.anchorPoint = CGPoint() self.inputActivitiesNode = ChatListInputActivitiesNode() self.inputActivitiesNode.isUserInteractionEnabled = false diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 49837a1318..d33c041fa2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -314,19 +314,37 @@ public final class TelegramBusinessHours: Equatable, Codable { public func splitIntoWeekDays() -> [WeekDay] { var mappedDays: [[WorkingTimeInterval]] = Array(repeating: [], count: 7) + var weekMinutes = IndexSet() for interval in self.weeklyTimeIntervals { - let startDayIndex = interval.startMinute / (24 * 60) - if startDayIndex < 0 || startDayIndex >= 7 { - continue + weekMinutes.insert(integersIn: interval.startMinute ..< interval.endMinute) + } + + for i in 0 ..< mappedDays.count { + let dayRange = i * 24 * 60 ..< (i + 1) * 24 * 60 + var removeMinutes = IndexSet() + inner: for range in weekMinutes.rangeView { + if range.lowerBound >= dayRange.upperBound { + break inner + } else { + let clippedRange: Range + if range.lowerBound == dayRange.lowerBound { + clippedRange = range.lowerBound ..< min(range.upperBound, dayRange.upperBound) + } else { + clippedRange = range.lowerBound ..< min(range.upperBound, dayRange.upperBound + 12 * 60) + } + + let startTimeInsideDay = clippedRange.lowerBound - i * (24 * 60) + let endTimeInsideDay = clippedRange.upperBound - i * (24 * 60) + + mappedDays[i].append(WorkingTimeInterval( + startMinute: startTimeInsideDay, + endMinute: endTimeInsideDay + )) + removeMinutes.insert(integersIn: clippedRange) + } } - let startTimeInsideDay = interval.startMinute - startDayIndex * (24 * 60) - let endTimeInsideDay = interval.endMinute - startDayIndex * (24 * 60) - - mappedDays[startDayIndex].append(WorkingTimeInterval( - startMinute: startTimeInsideDay, - endMinute: endTimeInsideDay - )) + weekMinutes.subtract(removeMinutes) } return mappedDays.map { day -> WeekDay in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 72ae2f0dbe..d325726da8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1191,7 +1191,7 @@ public extension TelegramEngine { } var selectedMedia: EngineMedia - if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), !preferHighQuality { + if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = EngineMedia(media) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 5e8720c93f..c7072d35a6 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -66,6 +66,7 @@ public final class ListActionItemComponent: Component { public let theme: PresentationTheme public let title: AnyComponent + public let contentInsets: UIEdgeInsets public let leftIcon: AnyComponentWithIdentity? public let icon: Icon? public let accessory: Accessory? @@ -74,6 +75,7 @@ public final class ListActionItemComponent: Component { public init( theme: PresentationTheme, title: AnyComponent, + contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0), leftIcon: AnyComponentWithIdentity? = nil, icon: Icon? = nil, accessory: Accessory? = .arrow, @@ -81,6 +83,7 @@ public final class ListActionItemComponent: Component { ) { self.theme = theme self.title = title + self.contentInsets = contentInsets self.leftIcon = leftIcon self.icon = icon self.accessory = accessory @@ -94,6 +97,9 @@ public final class ListActionItemComponent: Component { if lhs.title != rhs.title { return false } + if lhs.contentInsets != rhs.contentInsets { + return false + } if lhs.leftIcon != rhs.leftIcon { return false } @@ -172,8 +178,6 @@ public final class ListActionItemComponent: Component { let themeUpdated = component.theme !== previousComponent?.theme - let verticalInset: CGFloat = 12.0 - var contentLeftInset: CGFloat = 16.0 let contentRightInset: CGFloat switch component.accessory { @@ -186,7 +190,7 @@ public final class ListActionItemComponent: Component { } var contentHeight: CGFloat = 0.0 - contentHeight += verticalInset + contentHeight += component.contentInsets.top if component.leftIcon != nil { contentLeftInset += 46.0 @@ -198,7 +202,7 @@ public final class ListActionItemComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - contentLeftInset - contentRightInset, height: availableSize.height) ) - let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: verticalInset), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: contentHeight), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false @@ -208,7 +212,7 @@ public final class ListActionItemComponent: Component { } contentHeight += titleSize.height - contentHeight += verticalInset + contentHeight += component.contentInsets.bottom if let iconValue = component.icon { if previousComponent?.icon?.component.id != iconValue.component.id, let icon = self.icon { diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 6a7e63e973..9dadf28dc5 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -208,7 +208,7 @@ public final class ListMultilineTextFieldItemComponent: Component { containerSize: availableSize ) - let size = textFieldSize + let size = CGSize(width: textFieldSize.width, height: textFieldSize.height - 1.0) let textFieldFrame = CGRect(origin: CGPoint(), size: textFieldSize) if let textFieldView = self.textField.view { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index f7f139d03c..9b1fd4efd9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -139,6 +139,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent", "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index 6ee2fe4f8f..efdb3850fa 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -11,8 +11,9 @@ import TelegramCore import ComponentFlow import MultilineTextComponent import BundleIconComponent +import PlainButtonComponent -private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay) -> String { +private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay, offsetMinutes: Int) -> String { var businessHoursText: String = "" switch day { case .open: @@ -26,6 +27,8 @@ private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay) -> Strin var resultText: String = "" for range in intervals { + let range = TelegramBusinessHours.WorkingTimeInterval(startMinute: range.startMinute + offsetMinutes, endMinute: range.endMinute + offsetMinutes) + if !resultText.isEmpty { resultText.append("\n") } @@ -47,13 +50,13 @@ final class PeerInfoScreenBusinessHoursItem: PeerInfoScreenItem { let id: AnyHashable let label: String let businessHours: TelegramBusinessHours - let requestLayout: () -> Void + let requestLayout: (Bool) -> Void init( id: AnyHashable, label: String, businessHours: TelegramBusinessHours, - requestLayout: @escaping () -> Void + requestLayout: @escaping (Bool) -> Void ) { self.id = id self.label = label @@ -79,6 +82,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode private let labelNode: ImmediateTextNode private let currentStatusText = ComponentView() private let currentDayText = ComponentView() + private var timezoneSwitchButton: ComponentView? private var dayTitles: [ComponentView] = [] private var dayValues: [ComponentView] = [] private let arrowIcon = ComponentView() @@ -90,6 +94,8 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode private var item: PeerInfoScreenBusinessHoursItem? private var theme: PresentationTheme? + private var currentTimezone: TimeZone + private var displayLocalTimezone: Bool = false private var cachedDays: [TelegramBusinessHours.WeekDay] = [] private var cachedWeekMinuteSet = IndexSet() @@ -115,6 +121,8 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode self.activateArea = AccessibilityAreaNode() + self.currentTimezone = TimeZone.current + super.init() self.addSubnode(self.bottomSeparatorNode) @@ -179,7 +187,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode switch gesture { case .tap, .longTap: self.isExpanded = !self.isExpanded - self.item?.requestLayout() + self.item?.requestLayout(true) default: break } @@ -255,11 +263,16 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode let currentHour = currentCalendar.component(.hour, from: currentDate) let currentWeekMinute = currentDayIndex * 24 * 60 + currentHour * 60 + currentMinute + var timezoneOffsetMinutes: Int = 0 + if self.displayLocalTimezone { + timezoneOffsetMinutes = (self.currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60 + } + let isOpen = self.cachedWeekMinuteSet.contains(currentWeekMinute) //TODO:localize let openStatusText = isOpen ? "Open" : "Closed" - var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex]) : " " + var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex], offsetMinutes: timezoneOffsetMinutes) : " " if !isOpen { for range in self.cachedWeekMinuteSet.rangeView { @@ -325,6 +338,61 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode environment: {}, containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0) ) + + var timezoneSwitchButtonSize: CGSize? + if item.businessHours.timezoneId != self.currentTimezone.identifier { + let timezoneSwitchButton: ComponentView + if let current = self.timezoneSwitchButton { + timezoneSwitchButton = current + } else { + timezoneSwitchButton = ComponentView() + self.timezoneSwitchButton = timezoneSwitchButton + } + let timezoneSwitchTitle: String + //TODO:localize + if self.displayLocalTimezone { + timezoneSwitchTitle = "my time" + } else { + timezoneSwitchTitle = "local time" + } + timezoneSwitchButtonSize = timezoneSwitchButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: timezoneSwitchTitle, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor)) + )), + background: AnyComponent(RoundedRectangle( + color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + cornerRadius: nil + )), + effectAlignment: .center, + contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 2.0, right: 7.0), + action: { [weak self] in + guard let self else { + return + } + self.displayLocalTimezone = !self.displayLocalTimezone + self.item?.requestLayout(false) + }, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } else { + if let timezoneSwitchButton = self.timezoneSwitchButton { + self.timezoneSwitchButton = nil + timezoneSwitchButton.view?.removeFromSuperview() + } + } + + let timezoneSwitchButtonSpacing: CGFloat = 3.0 + if timezoneSwitchButtonSize != nil { + topOffset -= 20.0 + } + let currentDayTextFrame = CGRect(origin: CGPoint(x: width - dayRightInset - currentDayTextSize.width, y: topOffset), size: currentDayTextSize) if let currentDayTextView = self.currentDayText.view { if currentDayTextView.superview == nil { @@ -337,6 +405,20 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode topOffset += max(currentStatusTextSize.height, currentDayTextSize.height) + if let timezoneSwitchButtonView = self.timezoneSwitchButton?.view, let timezoneSwitchButtonSize { + topOffset += timezoneSwitchButtonSpacing + + var timezoneSwitchButtonTransition = transition + if timezoneSwitchButtonView.superview == nil { + timezoneSwitchButtonTransition = .immediate + self.contextSourceNode.contentNode.view.addSubview(timezoneSwitchButtonView) + } + let timezoneSwitchButtonFrame = CGRect(origin: CGPoint(x: width - dayRightInset - timezoneSwitchButtonSize.width, y: topOffset), size: timezoneSwitchButtonSize) + timezoneSwitchButtonTransition.updateFrame(view: timezoneSwitchButtonView, frame: timezoneSwitchButtonFrame) + + topOffset += timezoneSwitchButtonSize.height + } + let daySpacing: CGFloat = 15.0 var dayHeights: CGFloat = 0.0 @@ -383,7 +465,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode dayTitleValue = " " } - let businessHoursText = dayBusinessHoursText(businessDays[i]) + let businessHoursText = dayBusinessHoursText(businessDays[i], offsetMinutes: timezoneOffsetMinutes) let dayTitleSize = dayTitle.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index cdd84b941b..c9ddbd4551 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1166,8 +1166,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let businessHours = cachedData.businessHours { //TODO:localize - items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: "business hours", businessHours: businessHours, requestLayout: { - interaction.requestLayout(true) + items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: "business hours", businessHours: businessHours, requestLayout: { animated in + interaction.requestLayout(animated) })) } diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index a147d2b61f..0e3d87e39e 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -81,6 +81,7 @@ final class BusinessDaySetupScreenComponent: Component { private(set) var isOpen: Bool = false private(set) var ranges: [BusinessHoursSetupScreenComponent.WorkingHourRange] = [] + private var intersectingRanges = Set() private var nextRangeId: Int = 0 override init(frame: CGRect) { @@ -116,7 +117,25 @@ final class BusinessDaySetupScreenComponent: Component { } func attemptNavigation(complete: @escaping () -> Void) -> Bool { - return true + guard let component = self.component else { + return true + } + + if self.intersectingRanges.isEmpty { + return true + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Business hours are intersecting. Reset?", actions: [ + TextAlertAction(type: .genericAction, title: "Cancel", action: { + }), + TextAlertAction(type: .defaultAction, title: "Reset", action: { + complete() + }) + ]), in: .window(.root)) + + return false } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -191,6 +210,26 @@ final class BusinessDaySetupScreenComponent: Component { private func validateRanges() { self.ranges.sort(by: { $0.startMinute < $1.startMinute }) + + self.intersectingRanges.removeAll() + for i in 0 ..< self.ranges.count { + var minuteSet = IndexSet() + inner: for j in 0 ..< self.ranges.count { + if i == j { + continue inner + } + let range = self.ranges[j] + let rangeMinutes = range.startMinute ..< range.endMinute + minuteSet.insert(integersIn: rangeMinutes) + } + + let range = self.ranges[i] + let rangeMinutes = range.startMinute ..< range.endMinute + + if minuteSet.intersects(integersIn: rangeMinutes) { + self.intersectingRanges.insert(range.id) + } + } } func update(component: BusinessDaySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -260,7 +299,7 @@ final class BusinessDaySetupScreenComponent: Component { let bottomContentInset: CGFloat = 24.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let sectionSpacing: CGFloat = 32.0 + let sectionSpacing: CGFloat = 24.0 let _ = bottomContentInset let _ = sectionSpacing @@ -335,80 +374,47 @@ final class BusinessDaySetupScreenComponent: Component { let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat()) var rangeSectionItems: [AnyComponentWithIdentity] = [] - rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: "Opening time", - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + for i in 0 ..< 2 { + let isOpenTime = i == 0 + rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: isOpenTime ? "Opening time" : "Closing Time", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: isOpenTime ? startText : endText, font: Font.regular(17.0), textColor: self.intersectingRanges.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemPrimaryTextColor)) )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( - content: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: startText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) - )), - background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)), - effectAlignment: .center, - minSize: nil, - contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0), - action: { [weak self] in + background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)), + effectAlignment: .center, + minSize: nil, + contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0), + action: { [weak self] in + guard let self else { + return + } + self.openRangeDateSetup(rangeId: rangeId, isStartTime: isOpenTime) + }, + animateAlpha: true, + animateScale: false + ))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true), + accessory: nil, + action: { [weak self] _ in guard let self else { return } - self.openRangeDateSetup(rangeId: rangeId, isStartTime: true) - }, - animateAlpha: true, - animateScale: false - ))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true), - accessory: nil, - action: { [weak self] _ in - guard let self else { - return + self.openRangeDateSetup(rangeId: rangeId, isStartTime: isOpenTime) } - self.openRangeDateSetup(rangeId: rangeId, isStartTime: true) - } - )))) - rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: "Closing time", - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( - content: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: endText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) - )), - background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)), - effectAlignment: .center, - minSize: nil, - contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0), - action: { [weak self] in - guard let self else { - return - } - self.openRangeDateSetup(rangeId: rangeId, isStartTime: false) - }, - animateAlpha: true, - animateScale: false - ))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true), - accessory: nil, - action: { [weak self] _ in - guard let self else { - return - } - self.openRangeDateSetup(rangeId: rangeId, isStartTime: false) - } - )))) + )))) + } + rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ @@ -519,13 +525,13 @@ final class BusinessDaySetupScreenComponent: Component { text: .plain(NSAttributedString( string: "Add a Set of Hours", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + textColor: environment.theme.list.itemAccentColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( - name: "Chat List/AddIcon", + name: "Item List/AddTimeIcon", tintColor: environment.theme.list.itemAccentColor ))), accessory: nil, @@ -619,7 +625,7 @@ final class BusinessDaySetupScreenComponent: Component { final class BusinessDaySetupScreen: ViewControllerComponentContainer { private let context: AccountContext - private let updateDay: (BusinessHoursSetupScreenComponent.Day) -> Void + fileprivate let updateDay: (BusinessHoursSetupScreenComponent.Day) -> Void init(context: AccountContext, dayIndex: Int, day: BusinessHoursSetupScreenComponent.Day, updateDay: @escaping (BusinessHoursSetupScreenComponent.Day) -> Void) { self.context = context @@ -647,9 +653,12 @@ final class BusinessDaySetupScreen: ViewControllerComponentContainer { return true } - self.updateDay(BusinessHoursSetupScreenComponent.Day(ranges: componentView.isOpen ? componentView.ranges : nil)) - - return componentView.attemptNavigation(complete: complete) + if componentView.attemptNavigation(complete: complete) { + self.updateDay(BusinessHoursSetupScreenComponent.Day(ranges: componentView.isOpen ? componentView.ranges : nil)) + return true + } else { + return false + } } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift index 5f8839c110..3e8acde476 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift @@ -22,6 +22,32 @@ import LocationUI import TelegramStringFormatting import TimezoneSelectionScreen +private func wrappedMinuteRange(range: Range, dayIndexOffset: Int = 0) -> IndexSet { + let mappedRange = (range.lowerBound + dayIndexOffset * 24 * 60) ..< (range.upperBound + dayIndexOffset * 24 * 60) + + var result = IndexSet() + if mappedRange.upperBound > 7 * 24 * 60 { + result.insert(integersIn: mappedRange.lowerBound ..< 7 * 24 * 60) + result.insert(integersIn: 0 ..< (mappedRange.upperBound - 7 * 24 * 60)) + } else { + result.insert(integersIn: mappedRange) + } + return result +} + +private func getDayRanges(days: [BusinessHoursSetupScreenComponent.Day], index: Int) -> [BusinessHoursSetupScreenComponent.WorkingHourRange] { + let day = days[index] + if let ranges = day.ranges { + if ranges.isEmpty { + return [BusinessHoursSetupScreenComponent.WorkingHourRange(id: 0, startMinute: 0, endMinute: 24 * 60)] + } else { + return ranges + } + } else { + return [] + } +} + final class BusinessHoursSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -68,6 +94,16 @@ final class BusinessHoursSetupScreenComponent: Component { } } + struct DayRangeIndex: Hashable { + var day: Int + var id: Int + + init(day: Int, id: Int) { + self.day = day + self.id = id + } + } + struct Day: Equatable { var ranges: [WorkingHourRange]? @@ -83,7 +119,7 @@ final class BusinessHoursSetupScreenComponent: Component { var timezoneId: String private(set) var days: [Day] - private(set) var intersectingDays = Set() + private(set) var intersectingRanges = Set() init(timezoneId: String, days: [Day]) { self.timezoneId = timezoneId @@ -111,13 +147,45 @@ final class BusinessHoursSetupScreenComponent: Component { } } + if let value = try? self.asBusinessHours() { + if value != businessHours { + assertionFailure("Inconsistent representation") + } + } + self.validate() } mutating func validate() { - self.intersectingDays.removeAll() - + self.intersectingRanges.removeAll() + for dayIndex in 0 ..< self.days.count { + var otherDaysMinutes = IndexSet() + inner: for otherDayIndex in 0 ..< self.days.count { + if dayIndex == otherDayIndex { + continue inner + } + for range in getDayRanges(days: self.days, index: otherDayIndex) { + otherDaysMinutes.formUnion(wrappedMinuteRange(range: range.startMinute ..< range.endMinute, dayIndexOffset: otherDayIndex)) + } + } + + let dayRanges = getDayRanges(days: self.days, index: dayIndex) + for i in 0 ..< dayRanges.count { + var currentDayOtherMinutes = IndexSet() + inner: for j in 0 ..< dayRanges.count { + if i == j { + continue inner + } + currentDayOtherMinutes.formUnion(wrappedMinuteRange(range: dayRanges[j].startMinute ..< dayRanges[j].endMinute, dayIndexOffset: dayIndex)) + } + + let currentDayIndices = wrappedMinuteRange(range: dayRanges[i].startMinute ..< dayRanges[i].endMinute, dayIndexOffset: dayIndex) + if !otherDaysMinutes.intersection(currentDayIndices).isEmpty || !currentDayOtherMinutes.intersection(currentDayIndices).isEmpty { + self.intersectingRanges.insert(DayRangeIndex(day: dayIndex, id: dayRanges[i].id)) + } + } + } } mutating func update(days: [Day]) { @@ -140,13 +208,7 @@ final class BusinessHoursSetupScreenComponent: Component { for range in effectiveRanges { let minuteRange: Range = (dayStartMinute + range.startMinute) ..< (dayStartMinute + range.endMinute) - var wrappedMinutes = IndexSet() - if minuteRange.upperBound > 7 * 24 * 60 { - wrappedMinutes.insert(integersIn: minuteRange.lowerBound ..< 7 * 24 * 60) - wrappedMinutes.insert(integersIn: 0 ..< (7 * 24 * 60 - minuteRange.upperBound)) - } else { - wrappedMinutes.insert(integersIn: minuteRange) - } + let wrappedMinutes = wrappedMinuteRange(range: minuteRange) if !filledMinutes.intersection(wrappedMinutes).isEmpty { throw ValidationError.intersectingRanges @@ -156,7 +218,20 @@ final class BusinessHoursSetupScreenComponent: Component { } } - return TelegramBusinessHours(timezoneId: self.timezoneId, weeklyTimeIntervals: mappedIntervals) + var mergedIntervals: [TelegramBusinessHours.WorkingTimeInterval] = [] + for interval in mappedIntervals { + if mergedIntervals.isEmpty { + mergedIntervals.append(interval) + } else { + if mergedIntervals[mergedIntervals.count - 1].endMinute >= interval.startMinute { + mergedIntervals[mergedIntervals.count - 1] = TelegramBusinessHours.WorkingTimeInterval(startMinute: mergedIntervals[mergedIntervals.count - 1].startMinute, endMinute: interval.endMinute) + } else { + mergedIntervals.append(interval) + } + } + } + + return TelegramBusinessHours(timezoneId: self.timezoneId, weeklyTimeIntervals: mergedIntervals) } } @@ -231,7 +306,6 @@ final class BusinessHoursSetupScreenComponent: Component { return true } catch let error { let _ = error - //TODO:localize let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } //TODO:localize @@ -359,7 +433,7 @@ final class BusinessHoursSetupScreenComponent: Component { let bottomContentInset: CGFloat = 24.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let sectionSpacing: CGFloat = 32.0 + let sectionSpacing: CGFloat = 30.0 let _ = bottomContentInset let _ = sectionSpacing @@ -370,14 +444,14 @@ final class BusinessHoursSetupScreenComponent: Component { let iconSize = self.icon.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "⏰", font: Font.semibold(90.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "BusinessHoursEmoji"), + loop: true )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 10.0), size: iconSize) if let iconView = self.icon.view { if iconView.superview == nil { self.scrollView.addSubview(iconView) @@ -386,7 +460,7 @@ final class BusinessHoursSetupScreenComponent: Component { iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) } - contentHeight += 129.0 + contentHeight += 126.0 //TODO:localize let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Turn this on to show your opening hours schedule to your customers.", attributes: MarkdownAttributes( @@ -506,28 +580,42 @@ final class BusinessHoursSetupScreenComponent: Component { title = " " } - let subtitle: String + let subtitle = NSMutableAttributedString() + + var invalidIndices: [Int] = [] + let effectiveDayRanges = getDayRanges(days: self.daysState.days, index: dayIndex) + for range in effectiveDayRanges { + if self.daysState.intersectingRanges.contains(DayRangeIndex(day: dayIndex, id: range.id)) { + invalidIndices.append(range.id) + } + } + + let subtitleFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)) + if let ranges = self.daysState.days[dayIndex].ranges { if ranges.isEmpty { - subtitle = "Open 24 Hours" + subtitle.append(NSAttributedString(string: "Open 24 Hours", font: subtitleFont, textColor: invalidIndices.contains(0) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor)) } else { - var resultText: String = "" - for range in ranges { - if !resultText.isEmpty { - resultText.append(", ") - } + for i in 0 ..< ranges.count { + let range = ranges[i] + let startHours = clipMinutes(range.startMinute) / 60 let startMinutes = clipMinutes(range.startMinute) % 60 let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: PresentationDateTimeFormat()) let endHours = clipMinutes(range.endMinute) / 60 let endMinutes = clipMinutes(range.endMinute) % 60 let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat()) - resultText.append("\(startText)\u{00a0}- \(endText)") + + var rangeString = "\(startText)\u{00a0}- \(endText)" + if i != ranges.count - 1 { + rangeString.append(", ") + } + + subtitle.append(NSAttributedString(string: rangeString, font: subtitleFont, textColor: invalidIndices.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor)) } - subtitle = resultText } } else { - subtitle = "Closed" + subtitle.append(NSAttributedString(string: "Closed", font: subtitleFont, textColor: environment.theme.list.itemAccentColor)) } daysSectionItems.append(AnyComponentWithIdentity(id: dayIndex, component: AnyComponent(ListActionItemComponent( @@ -542,14 +630,11 @@ final class BusinessHoursSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: subtitle, - font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)), - textColor: environment.theme.list.itemAccentColor - )), - maximumNumberOfLines: 5 + text: .plain(subtitle), + maximumNumberOfLines: 20 ))) - ], alignment: .left, spacing: 2.0)), + ], alignment: .left, spacing: 3.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 10.0, right: 0.0), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: day.ranges != nil, action: { [weak self] _ in guard let self else { return diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index 6c2727f32c..1b35f9fe0e 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -109,20 +109,38 @@ final class BusinessLocationSetupScreenComponent: Component { } func attemptNavigation(complete: @escaping () -> Void) -> Bool { - if let component = self.component { - 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) - } - - let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone() + guard let component = self.component else { + return true } + 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) + } + + if businessLocation != nil && address.isEmpty { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Address can't be empty.", actions: [ + TextAlertAction(type: .genericAction, title: "Cancel", action: { + }), + TextAlertAction(type: .destructiveAction, title: "Delete", action: { + let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: nil).startStandalone() + + complete() + }) + ]), in: .window(.root)) + + return false + } + + let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone() + return true } @@ -228,10 +246,7 @@ final class BusinessLocationSetupScreenComponent: Component { let bottomContentInset: CGFloat = 24.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let sectionSpacing: CGFloat = 32.0 - - let _ = bottomContentInset - let _ = sectionSpacing + let sectionSpacing: CGFloat = 24.0 var contentHeight: CGFloat = 0.0 @@ -246,7 +261,7 @@ final class BusinessLocationSetupScreenComponent: Component { environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 11.0), size: iconSize) if let iconView = self.icon.view { if iconView.superview == nil { self.scrollView.addSubview(iconView) @@ -304,6 +319,7 @@ final class BusinessLocationSetupScreenComponent: Component { contentHeight += subtitleSize.height contentHeight += 27.0 + //TODO:localize var addressSectionItems: [AnyComponentWithIdentity] = [] addressSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent( context: component.context, diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift index 07cc080138..b6630b46c2 100644 --- a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift @@ -34,7 +34,22 @@ private struct TimezoneListEntry: Comparable, Identifiable { } func item(presentationData: PresentationData, searchMode: Bool, action: @escaping (String) -> Void) -> ListViewItem { - return ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: self.title, kind: .neutral, alignment: .natural, sectionId: 0, style: .plain, action: { + let hours = abs(self.offset / (60 * 60)) + let minutes = abs(self.offset % (60 * 60)) / 60 + let offsetString: String + if minutes == 0 { + offsetString = "UTC \(self.offset >= 0 ? "+" : "-")\(hours)" + } else { + let minutesString: String + if minutes < 10 { + minutesString = "0\(minutes)" + } else { + minutesString = "\(minutes)" + } + offsetString = "UTC \(self.offset >= 0 ? "+" : "-")\(hours):\(minutesString)" + } + + return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), title: self.title, label: offsetString, sectionId: 0, style: .plain, disclosureStyle: .none, action: { action(self.id) }) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 1146bf3101..dc8f056aab 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -990,7 +990,7 @@ public final class StoryContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories { + if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = item.media @@ -1642,7 +1642,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let alternativeMedia = item.alternativeMedia, !preferHighQualityStories { + if let alternativeMedia = item.alternativeMedia, (!preferHighQualityStories && !item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = item.media @@ -2880,7 +2880,7 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories { + if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = item.media diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 76fb1778b6..fed8cb1220 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -593,7 +593,7 @@ final class StoryItemContentComponent: Component { let selectedMedia: EngineMedia var messageMedia: EngineMedia? - if !component.preferHighQuality, let alternativeMedia = component.item.alternativeMedia { + if !component.preferHighQuality, !component.item.isMy, let alternativeMedia = component.item.alternativeMedia { selectedMedia = alternativeMedia switch alternativeMedia { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 14aea4a93b..c25fb3cb10 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -6857,7 +6857,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if case let .file(file) = component.slice.item.storyItem.media, file.isVideo { + if !component.slice.item.storyItem.isMy, case let .file(file) = component.slice.item.storyItem.media, file.isVideo { let isHq = component.slice.additionalPeerData.preferHighQualityStories items.append(.action(ContextMenuActionItem(text: isHq ? component.strings.Story_ContextMenuSD : component.strings.Story_ContextMenuHD, icon: { theme in if isHq { diff --git a/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/Contents.json new file mode 100644 index 0000000000..fbc5573e6d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addclock_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/addclock_30.pdf b/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/addclock_30.pdf new file mode 100644 index 0000000000..9ba3b68f93 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/addclock_30.pdf @@ -0,0 +1,117 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.334961 4.334961 cm +0.000000 0.000000 0.000000 scn +1.330000 10.665078 m +1.330000 15.820656 5.509422 20.000078 10.665000 20.000078 c +15.820579 20.000078 20.000000 15.820656 20.000000 10.665078 c +20.000000 5.509500 15.820579 1.330078 10.665000 1.330078 c +10.024749 1.330078 9.400214 1.394436 8.797290 1.516823 c +8.437361 1.589886 8.086353 1.357334 8.013291 0.997406 c +7.940229 0.637478 8.172781 0.286469 8.532710 0.213406 c +9.222226 0.073441 9.935388 0.000076 10.665000 0.000076 c +16.555117 0.000076 21.330002 4.774961 21.330002 10.665078 c +21.330002 16.555195 16.555117 21.330078 10.665000 21.330078 c +4.774883 21.330078 0.000000 16.555195 0.000000 10.665078 c +0.000000 9.935467 0.073363 9.222304 0.213327 8.532788 c +0.286389 8.172859 0.637397 7.940308 0.997326 8.013370 c +1.357255 8.086432 1.589807 8.437439 1.516745 8.797368 c +1.394358 9.400291 1.330000 10.024826 1.330000 10.665078 c +h +11.330000 17.165077 m +11.330000 17.532347 11.032269 17.830078 10.665000 17.830078 c +10.297730 17.830078 10.000000 17.532347 10.000000 17.165077 c +10.000000 11.114144 l +10.000000 10.638557 10.203376 10.185671 10.558834 9.869707 c +14.723198 6.168051 l +14.997698 5.924050 15.418027 5.948775 15.662027 6.223276 c +15.906028 6.497777 15.881302 6.918105 15.606802 7.162106 c +11.442438 10.863762 l +11.370919 10.927334 11.330000 11.018456 11.330000 11.114144 c +11.330000 17.165077 l +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 3.339844 3.334961 cm +0.000000 0.000000 0.000000 scn +5.330000 8.665077 m +5.330000 9.032347 5.032269 9.330078 4.665000 9.330078 c +4.297730 9.330078 4.000000 9.032347 4.000000 8.665077 c +4.000000 5.330077 l +0.665000 5.330077 l +0.297731 5.330077 0.000000 5.032347 0.000000 4.665077 c +0.000000 4.297808 0.297731 4.000077 0.665000 4.000077 c +4.000000 4.000077 l +4.000000 0.665077 l +4.000000 0.297808 4.297730 0.000077 4.665000 0.000077 c +5.032269 0.000077 5.330000 0.297808 5.330000 0.665077 c +5.330000 4.000077 l +8.665000 4.000077 l +9.032269 4.000077 9.330000 4.297808 9.330000 4.665077 c +9.330000 5.032347 9.032269 5.330077 8.665000 5.330077 c +5.330000 5.330077 l +5.330000 8.665077 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2167 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002257 00000 n +0000002280 00000 n +0000002453 00000 n +0000002527 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2586 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/BusinessHoursEmoji.tgs b/submodules/TelegramUI/Resources/Animations/BusinessHoursEmoji.tgs new file mode 100644 index 0000000000000000000000000000000000000000..9fbdb0c152b95eec9450b2801c3fabccb1982958 GIT binary patch literal 33321 zcmV(=K-s?^iwFP!000021MIzPk1WTLCHPl}`#Ce=?}xrgYW0jTnCU@Fz+xfr19wPC z+@?f<bAKv{cnZvtp|MBiu@}KYJKeos}{=av>iq;+8{k8q~?|+jo{pMeP z|GR(w?XUmvyD$FxuYdnPfgVSJN)CTAHMnfPw#$J4)1>afgk!e`R;$d`yM~O(hI!Tx47{C z%RkMjcrVZSkLlk(AN1q;SDW(mSNvQ4D)+ZR9*nn2My~uPFGZ`DBTk38CN z@-7@~NT*oi0pG))7oTybc(4gS<3{Q=68SjL5BM6Pdbplt5Eod zAOHHUk9cpeMsAFu*kOnq6|eGNeRxL4!teg+PwrG)J(yD!N&U$jtT~zF2zq*JK*;Owc|f*X5(O1|QA z##%lpoxF{QqjUJy|9bbUKY#VzkKg<}OW!ZvSf1x|mKRH|%Gd2F1$oVQDi$wsbminz zIMh=p^J_bQ3h8hxCujH}`4XNe-*S9)xu>%nR

{_g+f^Zxc7x8?uyH@*?a_J^N-`2OF%IWxMS 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 J#004Z0|3kCuF?Pi literal 0 HcmV?d00001 From 729a260626fb759e2f82838becf7b56c74d7e9f0 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 27 Feb 2024 00:52:17 +0400 Subject: [PATCH 3/3] [WIP] Business --- .../Sources/AccountContext.swift | 6 +- .../Sources/AttachmentController.swift | 103 +++++++------ .../Sources/AttachmentPanel.swift | 7 + .../Sources/Node/ChatListItem.swift | 1 + .../Sources/PremiumIntroScreen.swift | 18 ++- .../Sources/State/PendingMessageManager.swift | 12 +- .../ChatMessageInteractiveFileNode.swift | 4 +- .../AutomaticBusinessMessageSetupScreen/BUILD | 1 + .../AutomaticBusinessMessageSetupScreen.swift | 87 ++++++++--- .../Sources/QuickReplySetupScreen.swift | 107 +++++++++++-- .../BusinessLocationSetupScreen/BUILD | 1 + .../Sources/BusinessLocationSetupScreen.swift | 63 +++++++- .../TelegramUI/Sources/ChatController.swift | 66 ++++---- .../ChatControllerOpenAttachmentMenu.swift | 39 ++++- .../ChatInterfaceStateContextMenus.swift | 144 ++++++++++-------- .../CommandChatInputContextPanelNode.swift | 53 ++++--- .../Sources/SharedAccountContext.swift | 10 +- ...ntextResultsChatInputPanelButtonItem.swift | 12 +- 18 files changed, 517 insertions(+), 217 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index b70a3f2d4b..39adb4ea99 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal func navigateToChatController(_ params: NavigateToChatControllerParams) diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 0fe0add1fa..f1ec3c9d1f 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -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 + } } } } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 39ebcc7c5e..dc3a040e4c 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -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 diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index f0a3ca6946..0971935516 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1156,6 +1156,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var avatarIconComponent: EmojiStatusComponent? var avatarVideoNode: AvatarVideoNode? var avatarTapRecognizer: UITapGestureRecognizer? + var avatarMediaNode: AvatarVideoNode? private var inlineNavigationMarkLayer: SimpleLayer? diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index a879a4fb62..ddac44e315 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -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)) } diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index bed6a901b1..8ed18226d6 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 821eb02bba..3d6e0eb6b1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD index b64e56b028..0e2891e840 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/TelegramStringFormatting", "//submodules/TelegramUI/Components/TimeSelectionActionSheet", "//submodules/TelegramUI/Components/ChatListHeaderComponent", + "//submodules/AttachmentUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index 409efe246f..e30630f5a3 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -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] = [] - 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 { + 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 + ) + } + } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 595483c17d..bd555555bd 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -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 + } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD index a689556122..83cfb9c9c7 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/LocationUI", "//submodules/AppBundle", + "//submodules/Geocoding", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index 1b35f9fe0e..d2a203000e 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -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, 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)) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 7ccc9b03f8..248c1bac4d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 + } } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 310f14acef..c3b7ac9db7 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 510ae5cca2..6e172bbe2d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -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) } } diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 4b6334f13e..37e177ab8f 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 2dca74162c..f5e3d2b61a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 { + 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 { diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelButtonItem.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelButtonItem.swift index 515c8e8188..51c63e48bb 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelButtonItem.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelButtonItem.swift @@ -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))