From 1ccad40a1b390a832956b8c210efb88bd93e97a5 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 9 Feb 2024 23:04:16 +0400 Subject: [PATCH] [WIP] Folder tags --- .../Sources/AccountContext.swift | 2 + submodules/ChatListUI/BUILD | 1 + .../ChatListFilterPresetController.swift | 244 +++----- .../ChatListFilterPresetListController.swift | 43 +- .../Sources/ChatListSearchListPaneNode.swift | 6 +- .../Sources/ChatListShimmerNode.swift | 3 +- .../Sources/Node/ChatListItem.swift | 236 ++++++- .../Sources/Node/ChatListNode.swift | 91 ++- .../Sources/Node/ChatListViewTransition.swift | 4 +- .../Source/Components/Image.swift | 7 +- submodules/Display/Source/TextNode.swift | 590 +----------------- .../Items/ItemListSectionHeaderItem.swift | 32 +- .../Sources/MTRequestMessageService.m | 3 + submodules/SettingsUI/BUILD | 1 + .../TextSizeSelectionController.swift | 3 +- .../Themes/ThemePreviewControllerNode.swift | 3 +- .../Themes/ThemeSettingsController.swift | 1 + .../SyncCore/SyncCore_CachedUserData.swift | 2 +- .../TelegramEngine/Data/ChatListData.swift | 19 +- .../Peers/ChatListFiltering.swift | 27 +- .../Peers/TelegramEnginePeers.swift | 10 + submodules/TelegramUI/BUILD | 2 + .../Components/BackButtonComponent/BUILD | 20 + .../Sources/BackButtonComponent.swift | 103 +++ ...ChatInlineSearchResultsListComponent.swift | 3 +- .../StringForMessageTimestampStatus.swift | 3 +- .../Components/ListActionItemComponent/BUILD | 1 + .../Sources/ListActionItemComponent.swift | 165 ++++- .../Sources/ListSectionComponent.swift | 160 ++++- .../ListTextFieldItemComponent/BUILD | 21 + .../Sources/ListTextFieldItemComponent.swift | 152 +++++ .../Components/PeerInfo/PeerInfoScreen/BUILD | 1 + .../Sources/PeerInfoScreen.swift | 10 +- .../Settings/BusinessSetupScreen/BUILD | 35 ++ .../Sources/BusinessSetupScreen.swift | 426 +++++++++++++ .../Settings/ChatbotSetupScreen/BUILD | 38 ++ .../Sources/ChatbotSetupScreen.swift | 579 +++++++++++++++++ .../Settings/PeerNameColorItem/BUILD | 27 + .../Sources/PeerNameColorItem.swift | 36 +- .../Settings/PeerNameColorScreen/BUILD | 1 + .../Sources/ChannelAppearanceScreen.swift | 35 +- .../Sources/PeerNameColorScreen.swift | 1 + .../ThemeAccentColorControllerNode.swift | 3 +- .../Resources/Animations/BotEmoji.tgs | Bin 0 -> 30782 bytes ...rollerOpenMessageReactionContextMenu.swift | 2 +- .../ChatSearchResultsContollerNode.swift | 3 +- .../ChatSearchTitleAccessoryPanelNode.swift | 2 +- .../Sources/SharedAccountContext.swift | 10 + 48 files changed, 2296 insertions(+), 871 deletions(-) create mode 100644 submodules/TelegramUI/Components/BackButtonComponent/BUILD create mode 100644 submodules/TelegramUI/Components/BackButtonComponent/Sources/BackButtonComponent.swift create mode 100644 submodules/TelegramUI/Components/ListTextFieldItemComponent/BUILD create mode 100644 submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift create mode 100644 submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorItem/BUILD rename submodules/TelegramUI/Components/Settings/{PeerNameColorScreen => PeerNameColorItem}/Sources/PeerNameColorItem.swift (91%) create mode 100644 submodules/TelegramUI/Resources/Animations/BotEmoji.tgs diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 4ad8ef2d68..245ab7bb59 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -930,6 +930,8 @@ public protocol SharedAccountContext: AnyObject { func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController + func makeBusinessSetupScreen(context: AccountContext) -> ViewController + func makeChatbotSetupScreen(context: AccountContext) -> ViewController func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 1e8e8a45b9..f15878e53f 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -100,6 +100,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen", "//submodules/TelegramUI/Components/Settings/ArchiveInfoScreen", "//submodules/TelegramUI/Components/Settings/NewSessionInfoScreen", + "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 86d48faeb5..2644249b50 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -18,6 +18,7 @@ import QrCodeUI import ContextUI import AsyncDisplayKit import UndoUI +import PeerNameColorItem private enum FilterSection: Int32, Hashable { case include @@ -42,6 +43,7 @@ private final class ChatListFilterPresetControllerArguments { let removeLink: (ExportedChatFolderLink) -> Void let linkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void + let updateTagColor: (PeerNameColor?) -> Void init( context: AccountContext, @@ -60,7 +62,8 @@ private final class ChatListFilterPresetControllerArguments { openLink: @escaping (ExportedChatFolderLink) -> Void, removeLink: @escaping (ExportedChatFolderLink) -> Void, linkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void, - peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void + peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, + updateTagColor: @escaping (PeerNameColor?) -> Void ) { self.context = context self.updateState = updateState @@ -79,6 +82,7 @@ private final class ChatListFilterPresetControllerArguments { self.removeLink = removeLink self.linkContextAction = linkContextAction self.peerContextAction = peerContextAction + self.updateTagColor = updateTagColor } } @@ -88,6 +92,7 @@ private enum ChatListFilterPresetControllerSection: Int32 { case includePeers case excludePeers case inviteLinks + case tagColor } private enum ChatListFilterPresetEntryStableId: Hashable { @@ -100,129 +105,20 @@ private enum ChatListFilterPresetEntryStableId: Hashable { case includeExpand case excludeExpand case inviteLink(String) + case tagColorHeader + case tagColor + case tagColorFooter } -private enum ChatListFilterPresetEntrySortId: Comparable { - case screenHeader - case topIndex(Int) - case includeIndex(Int) - case excludeIndex(Int) - case bottomIndex(Int) - case inviteLink(Int) - case inviteLinkFooter +private struct ChatListFilterPresetEntrySortId: Comparable { + var section: Int + var index: Int static func <(lhs: ChatListFilterPresetEntrySortId, rhs: ChatListFilterPresetEntrySortId) -> Bool { - switch lhs { - case .screenHeader: - switch rhs { - case .screenHeader: - return false - default: - return true - } - case let .topIndex(lhsIndex): - switch rhs { - case .screenHeader: - return false - case let .topIndex(rhsIndex): - return lhsIndex < rhsIndex - case .includeIndex: - return true - case .excludeIndex: - return true - case .bottomIndex: - return true - case .inviteLink: - return true - case .inviteLinkFooter: - return true - } - case let .includeIndex(lhsIndex): - switch rhs { - case .screenHeader: - return false - case .topIndex: - return false - case let .includeIndex(rhsIndex): - return lhsIndex < rhsIndex - case .excludeIndex: - return true - case .bottomIndex: - return true - case .inviteLink: - return true - case .inviteLinkFooter: - return true - } - case let .excludeIndex(lhsIndex): - switch rhs { - case .screenHeader: - return false - case .topIndex: - return false - case .includeIndex: - return false - case let .excludeIndex(rhsIndex): - return lhsIndex < rhsIndex - case .bottomIndex: - return true - case .inviteLink: - return true - case .inviteLinkFooter: - return true - } - case let .bottomIndex(lhsIndex): - switch rhs { - case .screenHeader: - return false - case .topIndex: - return false - case .includeIndex: - return false - case .excludeIndex: - return false - case let .bottomIndex(rhsIndex): - return lhsIndex < rhsIndex - case .inviteLink: - return true - case .inviteLinkFooter: - return true - } - case let .inviteLink(lhsIndex): - switch rhs { - case .screenHeader: - return false - case .topIndex: - return false - case .includeIndex: - return false - case .excludeIndex: - return false - case .bottomIndex: - return false - case let .inviteLink(rhsIndex): - return lhsIndex < rhsIndex - case .inviteLinkFooter: - return true - } - case .inviteLinkFooter: - switch rhs { - case .screenHeader: - return false - case .topIndex: - return false - case .includeIndex: - return false - case .excludeIndex: - return false - case .bottomIndex: - return false - case .inviteLink: - return false - case .inviteLinkFooter: - return false - } + if lhs.section != rhs.section { + return lhs.section < rhs.section } + return lhs.index < rhs.index } } @@ -335,6 +231,9 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { case inviteLinkCreate(hasLinks: Bool) case inviteLink(Int, ExportedChatFolderLink) case inviteLinkInfo(text: String) + case tagColorHeader(name: String, color: PeerNameColors.Colors) + case tagColor(colors: PeerNameColors, currentColor: PeerNameColor?) + case tagColorFooter var section: ItemListSectionId { switch self { @@ -348,6 +247,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return ChatListFilterPresetControllerSection.excludePeers.rawValue case .inviteLinkHeader, .inviteLinkCreate, .inviteLink, .inviteLinkInfo: return ChatListFilterPresetControllerSection.inviteLinks.rawValue + case .tagColorHeader, .tagColor, .tagColorFooter: + return ChatListFilterPresetControllerSection.tagColor.rawValue } } @@ -391,49 +292,61 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return .inviteLink(link.link) case .inviteLinkInfo: return .index(13) + case .tagColorHeader: + return .index(14) + case .tagColor: + return .index(15) + case .tagColorFooter: + return .index(16) } } private var sortIndex: ChatListFilterPresetEntrySortId { switch self { case .screenHeader: - return .screenHeader + return ChatListFilterPresetEntrySortId(section: 0, index: 0) case .nameHeader: - return .topIndex(0) + return ChatListFilterPresetEntrySortId(section: 1, index: 0) case .name: - return .topIndex(1) + return ChatListFilterPresetEntrySortId(section: 1, index: 1) case .includePeersHeader: - return .includeIndex(0) + return ChatListFilterPresetEntrySortId(section: 2, index: 0) case .addIncludePeer: - return .includeIndex(1) + return ChatListFilterPresetEntrySortId(section: 2, index: 1) case let .includeCategory(index, _, _, _): - return .includeIndex(2 + index) + return ChatListFilterPresetEntrySortId(section: 2, index: 2 + index) case let .includePeer(index, _, _): - return .includeIndex(200 + index) + return ChatListFilterPresetEntrySortId(section: 3, index: index) case .includeExpand: - return .includeIndex(999) + return ChatListFilterPresetEntrySortId(section: 4, index: 0) case .includePeerInfo: - return .includeIndex(1000) + return ChatListFilterPresetEntrySortId(section: 5, index: 0) case .excludePeersHeader: - return .excludeIndex(0) + return ChatListFilterPresetEntrySortId(section: 6, index: 0) case .addExcludePeer: - return .excludeIndex(1) + return ChatListFilterPresetEntrySortId(section: 6, index: 1) case let .excludeCategory(index, _, _, _): - return .excludeIndex(2 + index) + return ChatListFilterPresetEntrySortId(section: 6, index: 2 + index) case let .excludePeer(index, _, _): - return .excludeIndex(200 + index) + return ChatListFilterPresetEntrySortId(section: 7, index: index) case .excludeExpand: - return .excludeIndex(999) + return ChatListFilterPresetEntrySortId(section: 8, index: 0) case .excludePeerInfo: - return .excludeIndex(1000) + return ChatListFilterPresetEntrySortId(section: 9, index: 0) + case .tagColorHeader: + return ChatListFilterPresetEntrySortId(section: 10, index: 0) + case .tagColor: + return ChatListFilterPresetEntrySortId(section: 10, index: 1) + case .tagColorFooter: + return ChatListFilterPresetEntrySortId(section: 10, index: 2) case .inviteLinkHeader: - return .bottomIndex(0) + return ChatListFilterPresetEntrySortId(section: 11, index: 0) case .inviteLinkCreate: - return .bottomIndex(1) + return ChatListFilterPresetEntrySortId(section: 11, index: 1) case let .inviteLink(index, _): - return .inviteLink(index) + return ChatListFilterPresetEntrySortId(section: 12, index: index) case .inviteLinkInfo: - return .inviteLinkFooter + return ChatListFilterPresetEntrySortId(section: 13, index: 0) } } @@ -545,8 +458,28 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: { arguments.expandSection(.exclude) }) - case let .inviteLinkHeader(hasLinks): - return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_SectionShare, badge: hasLinks ? nil : presentationData.strings.ChatList_ContextMenuBadgeNew, sectionId: self.section) + case let .tagColorHeader(name, color): + //TODO:localize + return ItemListSectionHeaderItem(presentationData: presentationData, text: "FOLDER COLOR", badge: name.uppercased(), badgeStyle: ItemListSectionHeaderItem.BadgeStyle( + background: color.main.withMultipliedAlpha(0.1), + foreground: color.main + ), sectionId: self.section) + case let .tagColor(colors, color): + return PeerNameColorItem( + theme: presentationData.theme, + colors: colors, + isProfile: true, + currentColor: color, + updated: { color in + arguments.updateTagColor(color) + }, + sectionId: self.section + ) + case .tagColorFooter: + //TODO:localize + return ItemListTextItem(presentationData: presentationData, text: .plain("This color will be used for the folder's tag in the chat list"), sectionId: self.section) + case .inviteLinkHeader: + return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_SectionShare, badge: nil, sectionId: self.section) case let .inviteLinkCreate(hasLinks): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: hasLinks ? presentationData.strings.ChatListFilter_CreateLink : presentationData.strings.ChatListFilter_CreateLinkNew, sectionId: self.section, editing: false, action: { arguments.createLink() @@ -568,6 +501,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { private struct ChatListFilterPresetControllerState: Equatable { var name: String var changedName: Bool + var color: PeerNameColor? var includeCategories: ChatListFilterPeerCategories var excludeMuted: Bool var excludeRead: Bool @@ -603,7 +537,7 @@ private struct ChatListFilterPresetControllerState: Equatable { } } -private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, currentPreset: ChatListFilter?, state: ChatListFilterPresetControllerState, includePeers: [EngineRenderedPeer], excludePeers: [EngineRenderedPeer], isPremium: Bool, limit: Int32, inviteLinks: [ExportedChatFolderLink]?, hadLinks: Bool) -> [ChatListFilterPresetEntry] { +private func chatListFilterPresetControllerEntries(context: AccountContext, presentationData: PresentationData, isNewFilter: Bool, currentPreset: ChatListFilter?, state: ChatListFilterPresetControllerState, includePeers: [EngineRenderedPeer], excludePeers: [EngineRenderedPeer], isPremium: Bool, limit: Int32, inviteLinks: [ExportedChatFolderLink]?, hadLinks: Bool) -> [ChatListFilterPresetEntry] { var entries: [ChatListFilterPresetEntry] = [] if isNewFilter { @@ -682,6 +616,13 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) } + let tagColor = state.color ?? .blue + let resolvedColor = context.peerNameColors.getProfile(tagColor, dark: presentationData.theme.overallDarkAppearance, subject: .palette) + + entries.append(.tagColorHeader(name: state.name, color: resolvedColor)) + entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor)) + entries.append(.tagColorFooter) + var hasLinks = false if let inviteLinks, !inviteLinks.isEmpty { hasLinks = true @@ -1077,7 +1018,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi } else { initialName = "" } - let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: initialPreset != nil, includeCategories: initialPreset?.data?.categories ?? [], excludeMuted: initialPreset?.data?.excludeMuted ?? false, excludeRead: initialPreset?.data?.excludeRead ?? false, excludeArchived: initialPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [], expandedSections: []) + let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: initialPreset != nil, color: initialPreset?.data?.color, includeCategories: initialPreset?.data?.categories ?? [], excludeMuted: initialPreset?.data?.excludeMuted ?? false, excludeRead: initialPreset?.data?.excludeRead ?? false, excludeArchived: initialPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [], expandedSections: []) let updatedCurrentPreset: Signal if let initialPreset { @@ -1099,7 +1040,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi let presentationData = context.sharedContext.currentPresentationData.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: initialPreset?.id ?? -1, title: state.name, emoticon: initialPreset?.emoticon, data: ChatListFilterData(isShared: initialPreset?.data?.isShared ?? false, hasSharedLinks: initialPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: initialPreset?.id ?? -1, title: state.name, emoticon: initialPreset?.emoticon, data: ChatListFilterData(isShared: initialPreset?.data?.isShared ?? false, hasSharedLinks: initialPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers, color: state.color)) if let data = filter.data { switch chatListFilterType(data) { case .generic: @@ -1251,7 +1192,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi let state = stateValue.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers, color: state.color)) let _ = (context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in @@ -1279,7 +1220,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi let state = stateValue.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers, color: state.color)) let _ = (context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in @@ -1597,6 +1538,13 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) + }, + updateTagColor: { color in + updateState { state in + var state = state + state.color = color + return state + } } ) @@ -1613,7 +1561,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi if currentPreset == nil { filterId = context.engine.peers.generateNewChatListFilterId(filters: filters) } - var updatedFilter: ChatListFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + var updatedFilter: ChatListFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers, color: state.color)) var filters = filters if let _ = currentPreset { @@ -1715,7 +1663,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(currentPreset != nil ? presentationData.strings.ChatListFolder_TitleEdit : presentationData.strings.ChatListFolder_TitleCreate), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, isNewFilter: currentPreset == nil, currentPreset: currentPreset, state: state, includePeers: includePeers, excludePeers: excludePeers, isPremium: isPremium, limit: premiumLimits.maxFolderChatsCount, inviteLinks: sharedLinks, hadLinks: hadLinks), style: .blocks, emptyStateItem: nil, crossfadeState: crossfadeAnimation, animateChanges: !skipStateAnimation) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(context: context, presentationData: presentationData, isNewFilter: currentPreset == nil, currentPreset: currentPreset, state: state, includePeers: includePeers, excludePeers: excludePeers, isPremium: isPremium, limit: premiumLimits.maxFolderChatsCount, inviteLinks: sharedLinks, hadLinks: hadLinks), style: .blocks, emptyStateItem: nil, crossfadeState: crossfadeAnimation, animateChanges: !skipStateAnimation) skipStateAnimation = false return (controllerState, (listState, arguments)) @@ -1802,7 +1750,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(isShared: currentPreset.data?.isShared ?? false, hasSharedLinks: currentPreset.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(isShared: currentPreset.data?.isShared ?? false, hasSharedLinks: currentPreset.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers, color: state.color)) if currentPresetWithoutPinnedPeers != filter { displaySaveAlert() f(false) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 5fb2f708a8..98560d89e7 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -21,14 +21,16 @@ private final class ChatListFilterPresetListControllerArguments { let addNew: () -> Void let setItemWithRevealedOptions: (Int32?, Int32?) -> Void let removePreset: (Int32) -> Void + let updateDisplayTags: (Bool) -> Void - init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void) { + init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void) { self.context = context self.addSuggestedPressed = addSuggestedPressed self.openPreset = openPreset self.addNew = addNew self.setItemWithRevealedOptions = setItemWithRevealedOptions self.removePreset = removePreset + self.updateDisplayTags = updateDisplayTags } } @@ -36,6 +38,7 @@ private enum ChatListFilterPresetListSection: Int32 { case screenHeader case suggested case list + case tags } private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String { @@ -59,6 +62,8 @@ private enum ChatListFilterPresetListEntryStableId: Hashable { case addItem case preset(Int32) case listFooter + case displayTags + case displayTagsFooter } private struct PresetIndex: Equatable { @@ -78,6 +83,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool) case addItem(text: String, isEditing: Bool) case listFooter(String) + case displayTags(Bool) + case displayTagsFooter var section: ItemListSectionId { switch self { @@ -87,6 +94,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return ChatListFilterPresetListSection.suggested.rawValue case .listHeader, .preset, .addItem, .listFooter: return ChatListFilterPresetListSection.list.rawValue + case .displayTags, .displayTagsFooter: + return ChatListFilterPresetListSection.tags.rawValue } } @@ -108,6 +117,10 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return 1003 + index.value case .suggestedAddCustom: return 2000 + case .displayTags: + return 3000 + case .displayTagsFooter: + return 3001 } } @@ -129,6 +142,10 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return .addItem case .listFooter: return .listFooter + case .displayTags: + return .displayTags + case .displayTagsFooter: + return .displayTagsFooter } } @@ -171,6 +188,14 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { }) case let .listFooter(text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .displayTags(value): + //TODO:localize + return ItemListSwitchItem(presentationData: presentationData, title: "Show Folder Tags", value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateDisplayTags(value) + }) + case .displayTagsFooter: + //TODO:localize + return ItemListTextItem(presentationData: presentationData, text: .plain("Display folder names for each chat in the chat list."), sectionId: self.section) } } } @@ -196,7 +221,7 @@ private func filtersWithAppliedOrder(filters: [(ChatListFilter, Int)], order: [I return sortedFilters } -private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], isPremium: Bool, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] { +private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], displayTags: Bool, isPremium: Bool, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] { var entries: [ChatListFilterPresetListEntry] = [] entries.append(.screenHeader(presentationData.strings.ChatListFolderSettings_Info)) @@ -249,6 +274,9 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present } } + entries.append(.displayTags(displayTags)) + entries.append(.displayTagsFooter) + return entries } @@ -493,6 +521,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch presentControllerImpl?(actionSheet) } }) + }, updateDisplayTags: { value in + context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value) }) let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) @@ -520,9 +550,12 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch updatedFilterOrder.get(), featuredFilters, context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), - limits + limits, + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.ChatList.FiltersDisplayTags() + ) ) - |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits, displayTags -> (ItemListControllerState, (ItemListNodeState, Any)) in let isPremium = peer?.isPremium ?? false let limits = allLimits.0 let premiumLimits = allLimits.1 @@ -594,7 +627,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true) return (controllerState, (listState, arguments)) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 2f288b1a55..2a6b2cc4f5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -883,7 +883,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { ) }, requiresPremiumForMessaging: requiresPremiumForMessaging, - displayAsTopicList: false + displayAsTopicList: false, + tags: [] )), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } case let .addContact(phoneNumber, theme, strings): @@ -3747,7 +3748,8 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { autoremoveTimeout: nil, storyState: nil, requiresPremiumForMessaging: false, - displayAsTopicList: false + displayAsTopicList: false, + tags: [] )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) case .media: return nil diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 46fbf13029..760ec06591 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -210,7 +210,8 @@ public final class ChatListShimmerNode: ASDisplayNode { autoremoveTimeout: nil, storyState: nil, requiresPremiumForMessaging: false, - displayAsTopicList: false + displayAsTopicList: false, + tags: [] )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 4082b4e1ec..862e359ee8 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -77,6 +77,18 @@ public enum ChatListItemContent { } } + public struct Tag: Equatable { + public var id: Int32 + public var title: String + public var colorId: Int32 + + public init(id: Int32, title: String, colorId: Int32) { + self.id = id + self.title = title + self.colorId = colorId + } + } + public struct PeerData { public var messages: [EngineMessage] public var peer: EngineRenderedPeer @@ -99,6 +111,7 @@ public enum ChatListItemContent { public var storyState: StoryState? public var requiresPremiumForMessaging: Bool public var displayAsTopicList: Bool + public var tags: [Tag] public init( messages: [EngineMessage], @@ -121,7 +134,8 @@ public enum ChatListItemContent { autoremoveTimeout: Int32?, storyState: StoryState?, requiresPremiumForMessaging: Bool, - displayAsTopicList: Bool + displayAsTopicList: Bool, + tags: [Tag] ) { self.messages = messages self.peer = peer @@ -144,6 +158,7 @@ public enum ChatListItemContent { self.storyState = storyState self.requiresPremiumForMessaging = requiresPremiumForMessaging self.displayAsTopicList = displayAsTopicList + self.tags = tags } } @@ -185,6 +200,167 @@ public enum ChatListItemContent { } } +private let tagBackgroundImage: UIImage? = { + return generateStretchableFilledCircleImage(diameter: 8.0, color: .white)?.withRenderingMode(.alwaysTemplate) +}() + +private final class ChatListItemTagListComponent: Component { + let context: AccountContext + let tags: [ChatListItemContent.Tag] + let theme: PresentationTheme + + init( + context: AccountContext, + tags: [ChatListItemContent.Tag], + theme: PresentationTheme + ) { + self.context = context + self.tags = tags + self.theme = theme + } + + static func ==(lhs: ChatListItemTagListComponent, rhs: ChatListItemTagListComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.tags != rhs.tags { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + private final class ItemView: UIView { + let backgroundView: UIImageView + let title = ComponentView() + + override init(frame: CGRect) { + self.backgroundView = UIImageView(image: tagBackgroundImage) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func update(context: AccountContext, title: String, backgroundColor: UIColor, foregroundColor: UIColor) -> CGSize { + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(Text(text: title.isEmpty ? " " : title, font: Font.semibold(11.0), color: foregroundColor)), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let backgroundSideInset: CGFloat = 4.0 + let backgroundVerticalInset: CGFloat = 2.0 + let backgroundSize = CGSize(width: titleSize.width + backgroundSideInset * 2.0, height: titleSize.height + backgroundVerticalInset * 2.0) + + let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) + self.backgroundView.frame = backgroundFrame + self.backgroundView.tintColor = backgroundColor + + let titleFrame = titleSize.centered(in: backgroundFrame) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + return backgroundSize + } + } + + final class View: UIView { + private var itemViews: [Int32: ItemView] = [:] + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func update(component: ChatListItemTagListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + var validIds: [Int32] = [] + let spacing: CGFloat = 5.0 + var nextX: CGFloat = 0.0 + for tag in component.tags { + if nextX != 0.0 { + nextX += spacing + } + + let itemId: Int32 + let itemTitle: String + let itemBackgroundColor: UIColor + let itemForegroundColor: UIColor + + if validIds.count >= 3 { + itemId = Int32.max + itemTitle = "+\(component.tags.count - validIds.count)" + itemForegroundColor = component.theme.chatList.dateTextColor + itemBackgroundColor = itemForegroundColor.withMultipliedAlpha(0.1) + } else { + itemId = tag.id + + let tagColor = PeerNameColor(rawValue: tag.colorId) + let resolvedColor = component.context.peerNameColors.getProfile(tagColor, dark: component.theme.overallDarkAppearance, subject: .palette) + + itemTitle = tag.title.uppercased() + itemBackgroundColor = resolvedColor.main.withMultipliedAlpha(0.1) + itemForegroundColor = resolvedColor.main + } + + let itemView: ItemView + if let current = self.itemViews[itemId] { + itemView = current + } else { + itemView = ItemView() + self.itemViews[itemId] = itemView + self.addSubview(itemView) + } + + let itemSize = itemView.update(context: component.context, title: itemTitle, backgroundColor: itemBackgroundColor, foregroundColor: itemForegroundColor) + let itemFrame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize) + itemView.frame = itemFrame + + validIds.append(itemId) + nextX += itemSize.width + + if validIds.count >= 4 { + break + } + } + var removedIds: [Int32] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + itemView.removeFromSuperview() + removedIds.append(id) + } + } + for id in removedIds { + self.itemViews.removeValue(forKey: id) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { let presentationData: ChatListPresentationData let context: AccountContext @@ -992,6 +1168,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var credibilityIconView: ComponentHostView? var credibilityIconComponent: EmojiStatusComponent? let mutedIconNode: ASImageNode + var itemTagList: ComponentView? private var hierarchyTrackingLayer: HierarchyTrackingLayer? private var cachedDataDisposable = MetaDisposable() @@ -1663,6 +1840,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var forumTopicData: EngineChatList.ForumTopicData? var topForumTopicItems: [EngineChatList.ForumTopicData] = [] var autoremoveTimeout: Int32? + var itemTags: [ChatListItemContent.Tag] = [] var groupHiddenByDefault = false @@ -1683,6 +1861,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let displayAsMessageValue = peerData.displayAsMessage let forumTopicDataValue = peerData.forumTopicData let topForumTopicItemsValue = peerData.topForumTopicItems + + itemTags = peerData.tags autoremoveTimeout = peerData.autoremoveTimeout @@ -1907,7 +2087,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var hasDraft = false var inlineAuthorPrefix: String? + var useInlineAuthorPrefix = false if case .groupReference = item.content { + useInlineAuthorPrefix = true + } + if !itemTags.isEmpty { + if case let .chat(peer) = contentPeer, peer.peerId == item.context.account.peerId { + } else { + useInlineAuthorPrefix = true + } + + forumTopicData = nil + topForumTopicItems = [] + } + + if useInlineAuthorPrefix { if case let .user(author) = messages.last?.author { if author.id == item.context.account.peerId { inlineAuthorPrefix = item.presentationData.strings.DialogList_You @@ -2669,6 +2863,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } badgeSize = max(badgeSize, reorderInset) + if !itemTags.isEmpty { + authorAttributedString = nil + } + var effectiveAuthorTitle = (hideAuthor && !hasDraft) ? nil : authorAttributedString let isSearching = item.interaction.searchTextHighightState != nil @@ -2717,7 +2915,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { textMaxWidth -= 18.0 } - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty) ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) let maxTitleLines: Int switch item.index { @@ -3491,6 +3689,40 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.authorNode.assignParentNode(parentNode: nil) } + if !itemTags.isEmpty { + let itemTagListFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.maxY - 12.0), size: CGSize(width: contentRect.width, height: 20.0)) + + let itemTagList: ComponentView + if let current = strongSelf.itemTagList { + itemTagList = current + } else { + itemTagList = ComponentView() + strongSelf.itemTagList = itemTagList + } + let _ = itemTagList.update( + transition: .immediate, + component: AnyComponent(ChatListItemTagListComponent( + context: item.context, + tags: itemTags, + theme: item.presentationData.theme + )), + environment: {}, + containerSize: itemTagListFrame.size + ) + if let itemTagListView = itemTagList.view { + if itemTagListView.superview == nil { + itemTagListView.isUserInteractionEnabled = false + strongSelf.mainContentContainerNode.view.addSubview(itemTagListView) + } + itemTagListView.frame = itemTagListFrame + } + } else { + if let itemTagList = strongSelf.itemTagList { + strongSelf.itemTagList = nil + itemTagList.view?.removeFromSuperview() + } + } + if !textLayout.spoilers.isEmpty { let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 164feb1bdc..40c8dda432 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -347,7 +347,7 @@ public struct ChatListNodeState: Equatable { } } -private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case .HeaderEntry: @@ -429,7 +429,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL ) }, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, - displayAsTopicList: peerEntry.displayAsTopicList + displayAsTopicList: peerEntry.displayAsTopicList, + tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -747,7 +748,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL } } -private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .PeerEntry(peerEntry): @@ -806,7 +807,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL ) }, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, - displayAsTopicList: peerEntry.displayAsTopicList + displayAsTopicList: peerEntry.displayAsTopicList, + tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -1101,8 +1103,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL } } -private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { - return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) +private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { + return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) } private final class ChatListOpaqueTransactionState { @@ -2086,6 +2088,28 @@ public final class ChatListNode: ListView { let accountPeerId = context.account.peerId + let chatListFilters: Signal<[ChatListFilter]?, NoError> + if case .chatList = mode { + chatListFilters = combineLatest(queue: .mainQueue(), + context.engine.peers.updatedChatListFilters(), + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.ChatList.FiltersDisplayTags() + ) + ) + |> map { filters, displayTags -> [ChatListFilter]? in + if !displayTags { + return nil + } + return filters.filter { filter in + return filter.id != chatListFilter?.id + } + } + |> distinctUntilChanged + } else { + chatListFilters = .single(nil) + } + let previousChatListFilters = Atomic<[ChatListFilter]?>(value: nil) + let chatListNodeViewTransition = combineLatest( queue: viewProcessingQueue, hideArchivedFolderByDefault, @@ -2095,9 +2119,10 @@ public final class ChatListNode: ListView { savedMessagesPeer, chatListViewUpdate, self.statePromise.get(), - contacts + contacts, + chatListFilters ) - |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state, contacts) -> Signal in + |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state, contacts, chatListFilters) -> Signal in let (update, filter) = updateAndFilter let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) @@ -2407,15 +2432,15 @@ public final class ChatListNode: ListView { updatedScrollPosition = nil } else { switch update.type { - case .InitialUnread, .Initial: - reason = .initial - prepareOnMainQueue = true - case .Generic: - reason = .interactiveChanges - case .UpdateVisible: - reason = .reload - case .FillHole: - reason = .reload + case .InitialUnread, .Initial: + reason = .initial + prepareOnMainQueue = true + case .Generic: + reason = .interactiveChanges + case .UpdateVisible: + reason = .reload + case .FillHole: + reason = .reload } } } @@ -2553,8 +2578,14 @@ public final class ChatListNode: ListView { } } - return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode) - |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) }) + var forceAllUpdated = false + let previousChatListFiltersValue = previousChatListFilters.swap(chatListFilters) + if chatListFilters != previousChatListFiltersValue { + forceAllUpdated = true + } + + return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode, forceAllUpdated: forceAllUpdated) + |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } @@ -4168,3 +4199,25 @@ public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { 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] { + guard let chatListFilters, !chatListFilters.isEmpty else { + return [] + } + guard let peer else { + return [] + } + + var result: [ChatListItemContent.Tag] = [] + for case let .filter(id, title, _, data) in chatListFilters { + let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId) + if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { + result.append(ChatListItemContent.Tag( + id: id, + title: title, + colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue + )) + } + } + return result +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift index 77a086921f..5d10a7a9ed 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift @@ -51,9 +51,9 @@ enum ChatListNodeViewScrollPosition { case index(index: ChatListIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } -func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toView: ChatListNodeView, reason: ChatListNodeViewTransitionReason, previewing: Bool, disableAnimations: Bool, account: Account, scrollPosition: ChatListNodeViewScrollPosition?, searchMode: Bool) -> Signal { +func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toView: ChatListNodeView, reason: ChatListNodeViewTransitionReason, previewing: Bool, disableAnimations: Bool, account: Account, scrollPosition: ChatListNodeViewScrollPosition?, searchMode: Bool, forceAllUpdated: Bool) -> Signal { return Signal { subscriber in - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries, allUpdated: forceAllUpdated) var adjustedDeleteIndices: [ListViewDeleteItem] = [] let previousCount: Int diff --git a/submodules/ComponentFlow/Source/Components/Image.swift b/submodules/ComponentFlow/Source/Components/Image.swift index 5517a6b071..dfc4d34ecc 100644 --- a/submodules/ComponentFlow/Source/Components/Image.swift +++ b/submodules/ComponentFlow/Source/Components/Image.swift @@ -50,7 +50,12 @@ public final class Image: Component { transition.setTintColor(view: self, color: component.tintColor ?? .white) - return component.size ?? availableSize + switch component.contentMode { + case .center: + return component.image?.size ?? availableSize + default: + return component.size ?? availableSize + } } } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 8c965c0a18..d0ac26e952 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1171,7 +1171,7 @@ open class TextNode: ASDisplayNode { public static let all: RenderContentTypes = [.text, .emoji] } - private final class DrawingParameters: NSObject { + final class DrawingParameters: NSObject { let cachedLayout: TextNodeLayout? let renderContentTypes: RenderContentTypes @@ -2715,438 +2715,10 @@ open class TextView: UIView { } private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout { - if let attributedString = attributedString { - let stringLength = attributedString.length - - let font: CTFont - let resolvedAlignment: NSTextAlignment - - if stringLength != 0 { - if let stringFont = attributedString.attribute(NSAttributedString.Key.font, at: 0, effectiveRange: nil) { - font = stringFont as! CTFont - } else { - font = defaultFont - } - if alignment == .center { - resolvedAlignment = .center - } else { - if let paragraphStyle = attributedString.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { - resolvedAlignment = paragraphStyle.alignment - } else { - resolvedAlignment = alignment - } - } - } else { - font = defaultFont - resolvedAlignment = alignment - } - - let fontAscent = CTFontGetAscent(font) - let fontDescent = CTFontGetDescent(font) - let fontLineHeight = floor(fontAscent + fontDescent) - let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) - - var lines: [TextNodeLine] = [] - let blockQuotes: [TextNodeBlockQuote] = [] - - var maybeTypesetter: CTTypesetter? - maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) - if maybeTypesetter == nil { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) - } - - let typesetter = maybeTypesetter! - - var lastLineCharacterIndex: CFIndex = 0 - var layoutSize = CGSize() - - var cutoutEnabled = false - var cutoutMinY: CGFloat = 0.0 - var cutoutMaxY: CGFloat = 0.0 - var cutoutWidth: CGFloat = 0.0 - var cutoutOffset: CGFloat = 0.0 - - var bottomCutoutEnabled = false - var bottomCutoutSize = CGSize() - - if let topLeft = cutout?.topLeft { - cutoutMinY = -fontLineSpacing - cutoutMaxY = topLeft.height + fontLineSpacing - cutoutWidth = topLeft.width - cutoutOffset = cutoutWidth - cutoutEnabled = true - } else if let topRight = cutout?.topRight { - cutoutMinY = -fontLineSpacing - cutoutMaxY = topRight.height + fontLineSpacing - cutoutWidth = topRight.width - cutoutEnabled = true - } - - if let bottomRight = cutout?.bottomRight { - bottomCutoutSize = bottomRight - bottomCutoutEnabled = true - } - - let firstLineOffset = floorToScreenPixels(fontDescent) - - var truncated = false - var first = true - while true { - var strikethroughs: [TextNodeStrikethrough] = [] - var spoilers: [TextNodeSpoiler] = [] - var spoilerWords: [TextNodeSpoiler] = [] - var attachments: [TextNodeAttachment] = [] - - var lineConstrainedWidth = constrainedSize.width - var lineConstrainedWidthDelta: CGFloat = 0.0 - var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent) - if !first { - lineOriginY += fontLineSpacing - } - var lineCutoutOffset: CGFloat = 0.0 - var lineAdditionalWidth: CGFloat = 0.0 - - if cutoutEnabled { - if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { - lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) - lineConstrainedWidthDelta = -cutoutWidth - lineCutoutOffset = cutoutOffset - lineAdditionalWidth = cutoutWidth - } - } - - let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) - - func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } - - spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) - } - - func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } - - spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) - } - - func addAttachment(attachment: UIImage, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } - - attachments.append(TextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment)) - } - - var isLastLine = false - if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { - isLastLine = true - } else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height { - isLastLine = true - } - if isLastLine { - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) - var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) - if brokenLineRange.location + brokenLineRange.length > attributedString.length { - brokenLineRange.length = attributedString.length - brokenLineRange.location - } - if lineRange.length == 0 { - break - } - - let coreTextLine: CTLine - let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) - - var lineConstrainedSize = constrainedSize - lineConstrainedSize.width += lineConstrainedWidthDelta - if bottomCutoutEnabled { - lineConstrainedSize.width -= bottomCutoutSize.width - } - - if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(lineConstrainedSize.width) { - coreTextLine = originalLine - } else { - var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] - truncationTokenAttributes[NSAttributedString.Key.font] = font - truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber - let tokenString = "\u{2026}" - let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) - let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) - - coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken - let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun] - for run in runs { - let runAttributes: NSDictionary = CTRunGetAttributes(run) - if let _ = runAttributes["CTForegroundColorFromContext"] { - brokenLineRange.length = CTRunGetStringRange(run).location - break - } - } - if brokenLineRange.location + brokenLineRange.length > attributedString.length { - brokenLineRange.length = attributedString.length - brokenLineRange.location - } - truncated = true - } - - var headIndent: CGFloat = 0.0 - if brokenLineRange.location >= 0 && brokenLineRange.length > 0 && brokenLineRange.location + brokenLineRange.length <= attributedString.length { - attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in - if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - var startIndex: Int? - var currentIndex: Int? - - let nsString = (attributedString.string as NSString) - nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in - if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { - if let currentStartIndex = startIndex { - startIndex = nil - let endIndex = range.location - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) - } - } else if startIndex == nil { - startIndex = range.location - } - currentIndex = range.location + range.length - } - - if let currentStartIndex = startIndex, let currentIndex = currentIndex { - startIndex = nil - let endIndex = currentIndex - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) - } - - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { - let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) - let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { - headIndent = paragraphStyle.headIndent - } - - if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } - } - } - - var lineAscent: CGFloat = 0.0 - var lineDescent: CGFloat = 0.0 - let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) - let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) - layoutSize.height += fontLineHeight + fontLineSpacing - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - var isRTL = false - let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray - if glyphRuns.count != 0 { - let run = glyphRuns[0] as! CTRun - if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { - isRTL = true - } - } - - lines.append(TextNodeLine( - line: coreTextLine, - frame: lineFrame, - ascent: lineAscent, - descent: lineDescent, - range: NSMakeRange(lineRange.location, lineRange.length), - isRTL: isRTL, - strikethroughs: strikethroughs, - spoilers: spoilers, - spoilerWords: spoilerWords, - embeddedItems: [], - attachments: attachments, - additionalTrailingLine: nil - )) - break - } else { - if lineCharacterCount > 0 { - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - var lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) - if lineRange.location + lineRange.length > attributedString.length { - lineRange.length = attributedString.length - lineRange.location - } - if lineRange.length < 0 { - break - } - - let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) - lastLineCharacterIndex += lineCharacterCount - - var headIndent: CGFloat = 0.0 - attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in - if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - var startIndex: Int? - var currentIndex: Int? - - let nsString = (attributedString.string as NSString) - nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in - if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { - if let currentStartIndex = startIndex { - startIndex = nil - let endIndex = range.location - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) - } - } else if startIndex == nil { - startIndex = range.location - } - currentIndex = range.location + range.length - } - - if let currentStartIndex = startIndex, let currentIndex = currentIndex { - startIndex = nil - let endIndex = currentIndex - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) - } - - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { - let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) - let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { - headIndent = paragraphStyle.headIndent - } - - if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } - } - - var lineAscent: CGFloat = 0.0 - var lineDescent: CGFloat = 0.0 - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) - let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) - layoutSize.height += fontLineHeight - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - var isRTL = false - let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray - if glyphRuns.count != 0 { - let run = glyphRuns[0] as! CTRun - if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { - isRTL = true - } - } - - lines.append(TextNodeLine( - line: coreTextLine, - frame: lineFrame, - ascent: lineAscent, - descent: lineDescent, - range: NSMakeRange(lineRange.location, lineRange.length), - isRTL: isRTL, - strikethroughs: strikethroughs, - spoilers: spoilers, - spoilerWords: spoilerWords, - embeddedItems: [], - attachments: attachments, - additionalTrailingLine: nil - )) - } else { - if !lines.isEmpty { - layoutSize.height += fontLineSpacing - } - break - } - } - } - - let rawLayoutSize = layoutSize - if !lines.isEmpty && bottomCutoutEnabled { - let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width - if proposedWidth > layoutSize.width { - if proposedWidth <= constrainedSize.width + .ulpOfOne { - layoutSize.width = proposedWidth - } else { - layoutSize.height += bottomCutoutSize.height - } - } - } - - if lines.count < minimumNumberOfLines { - var lineCount = lines.count - while lineCount < minimumNumberOfLines { - if lineCount != 0 { - layoutSize.height += fontLineSpacing - } - layoutSize.height += fontLineHeight - lineCount += 1 - } - } - - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) - } else { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) - } + return TextNode.calculateLayout(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers, displayEmbeddedItemsUnderSpoilers: false, customTruncationToken: nil) } public override func draw(_ rect: CGRect) { - let bounds = self.bounds let layout = self.cachedLayout let context = UIGraphicsGetCurrentContext()! @@ -3162,163 +2734,7 @@ open class TextView: UIView { context.setAllowsFontSubpixelQuantization(true) context.setShouldSubpixelQuantizeFonts(true) - var clearRects: [CGRect] = [] - if let layout = layout { - if layout.backgroundColor != nil { - context.setBlendMode(.copy) - context.setFillColor((layout.backgroundColor ?? UIColor.clear).cgColor) - context.fill(bounds) - context.setBlendMode(.copy) - } - - if let textShadowColor = layout.textShadowColor { - context.setTextDrawingMode(.fill) - context.setShadow(offset: layout.textShadowBlur != nil ? .zero : CGSize(width: 0.0, height: 1.0), blur: layout.textShadowBlur ?? 0.0, color: textShadowColor.cgColor) - } - - if let (textStrokeColor, textStrokeWidth) = layout.textStroke { - context.setBlendMode(.normal) - context.setLineCap(.round) - context.setLineJoin(.round) - context.setStrokeColor(textStrokeColor.cgColor) - context.setFillColor(textStrokeColor.cgColor) - context.setLineWidth(textStrokeWidth) - context.setTextDrawingMode(.fillStroke) - } - - let textMatrix = context.textMatrix - let textPosition = context.textPosition - context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) - - let alignment = layout.resolvedAlignment - var offset = CGPoint(x: layout.insets.left, y: layout.insets.top) - switch layout.verticalAlignment { - case .top: - break - case .middle: - offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top - case .bottom: - offset.y = floor(bounds.height - layout.size.height) + layout.insets.top - } - - for i in 0 ..< layout.lines.count { - let line = layout.lines[i] - - var lineFrame = line.frame - lineFrame.origin.y += offset.y - - if alignment == .center { - lineFrame.origin.x = offset.x + floor((bounds.size.width - lineFrame.width) / 2.0) - } else if alignment == .natural, line.isRTL { - lineFrame.origin.x = offset.x + floor(bounds.size.width - lineFrame.width) - - lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: bounds.size), cutout: layout.cutout) - } - context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY) - - if layout.displaySpoilers && !line.spoilers.isEmpty { - context.saveGState() - var clipRects: [CGRect] = [] - for spoiler in line.spoilerWords { - var spoilerClipRect = spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY - UIScreenPixel) - spoilerClipRect.size.height += 1.0 + UIScreenPixel - clipRects.append(spoilerClipRect) - } - context.clip(to: clipRects) - } - - if line.attachments.isEmpty { - let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray - if glyphRuns.count != 0 { - for run in glyphRuns { - let run = run as! CTRun - let glyphCount = CTRunGetGlyphCount(run) - CTRunDraw(run, context, CFRangeMake(0, glyphCount)) - } - } - } else { - let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray - if glyphRuns.count != 0 { - for run in glyphRuns { - let run = run as! CTRun - - let stringRange = CTRunGetStringRange(run) - if line.attachments.contains(where: { $0.range.contains(stringRange.location) }) { - continue - } - - let glyphCount = CTRunGetGlyphCount(run) - CTRunDraw(run, context, CFRangeMake(0, glyphCount)) - } - } - } - - for attachment in line.attachments { - let image = attachment.attachment - var textColor: UIColor? - layout.attributedString?.enumerateAttributes(in: attachment.range, options: []) { attributes, range, _ in - if let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { - textColor = color - } - } - if let textColor { - if let tintedImage = generateTintedImage(image: image, color: textColor) { - let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - tintedImage.size.width * 0.5, y: attachment.frame.midY - tintedImage.size.height * 0.5 + 1.0), size: tintedImage.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) - context.draw(tintedImage.cgImage!, in: imageRect) - } - } - } - - if !line.spoilers.isEmpty { - if layout.displaySpoilers { - context.restoreGState() - } else { - for spoiler in line.spoilerWords { - var spoilerClearRect = spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY - UIScreenPixel) - spoilerClearRect.size.height += 1.0 + UIScreenPixel - clearRects.append(spoilerClearRect) - } - } - } - } - - var blockQuoteFrames: [CGRect] = [] - var currentBlockQuoteFrame: CGRect? - for blockQuote in layout.blockQuotes { - if let frame = currentBlockQuoteFrame { - if blockQuote.frame.minY - frame.maxY < 20.0 { - currentBlockQuoteFrame = frame.union(blockQuote.frame) - } else { - blockQuoteFrames.append(frame) - currentBlockQuoteFrame = frame - } - } else { - currentBlockQuoteFrame = blockQuote.frame - } - } - - if let frame = currentBlockQuoteFrame { - blockQuoteFrames.append(frame) - } - - for frame in blockQuoteFrames { - if let lineColor = layout.lineColor { - context.setFillColor(lineColor.cgColor) - } - let rect = UIBezierPath(roundedRect: CGRect(x: frame.minX - 9.0, y: frame.minY - 14.0, width: 2.0, height: frame.height), cornerRadius: 1.0) - context.addPath(rect.cgPath) - context.fillPath() - } - - context.textMatrix = textMatrix - context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y) - } - - context.setBlendMode(.normal) - - for rect in clearRects { - context.clear(rect) - } + TextNode.draw(rect, withParameters: TextNode.DrawingParameters(cachedLayout: layout, renderContentTypes: .all), isCancelled: { false }, isRasterizing: false) } public static func asyncLayout(_ maybeView: TextView?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextView) { diff --git a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift index af59c31803..71c604aadd 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift @@ -39,9 +39,20 @@ public enum ItemListSectionHeaderActivityIndicator { } public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { + public struct BadgeStyle: Equatable { + public var background: UIColor + public var foreground: UIColor + + public init(background: UIColor, foreground: UIColor) { + self.background = background + self.foreground = foreground + } + } + let presentationData: ItemListPresentationData let text: String let badge: String? + let badgeStyle: BadgeStyle? let multiline: Bool let activityIndicator: ItemListSectionHeaderActivityIndicator let accessoryText: ItemListSectionHeaderAccessoryText? @@ -51,10 +62,11 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { public let isAlwaysPlain: Bool = true - public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId) { + public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId) { self.presentationData = presentationData self.text = text self.badge = badge + self.badgeStyle = badgeStyle self.multiline = multiline self.activityIndicator = activityIndicator self.accessoryText = accessoryText @@ -149,8 +161,13 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { var badgeLayoutAndApply: (TextNodeLayout, () -> TextNode)? if let badge = item.badge { - let badgeFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize * 11.0 / 13.0) - badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: badge, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + if item.badgeStyle != nil { + let badgeFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize * 12.0 / 13.0) + badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: badge, font: badgeFont, textColor: item.badgeStyle?.foreground ?? item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + } else { + let badgeFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize * 11.0 / 13.0) + badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: badge, font: badgeFont, textColor: item.badgeStyle?.foreground ?? item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + } } let badgeSpacing: CGFloat = 6.0 @@ -245,7 +262,12 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { if let badgeLayoutAndApply { let badgeTextNode = badgeLayoutAndApply.1() let badgeSideInset: CGFloat = 4.0 - let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0) + let badgeBackgroundSize: CGSize + if item.badgeStyle != nil { + badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0) + } else { + badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0) + } let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY - UIScreenPixel + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize) let badgeBackgroundLayer: SimpleLayer @@ -264,7 +286,7 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { } badgeBackgroundLayer.frame = badgeBackgroundFrame - badgeBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemCheckColors.fillColor.cgColor + badgeBackgroundLayer.backgroundColor = item.badgeStyle?.background.cgColor ?? item.presentationData.theme.list.itemCheckColors.fillColor.cgColor badgeBackgroundLayer.cornerRadius = 5.0 badgeTextNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeLayoutAndApply.0.size.width) * 0.5), y: badgeBackgroundFrame.minY + 1.0 + floorToScreenPixels((badgeBackgroundFrame.height - badgeLayoutAndApply.0.size.height) * 0.5)), size: badgeLayoutAndApply.0.size) diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index 6c6d9f4b88..83457c3c61 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -1010,6 +1010,7 @@ if (request.requestContext != nil && request.requestContext.messageId == messageId) { if (request.requestContext.transactionId == nil || [request.requestContext.transactionId isEqual:currentTransactionId]) { + MTLog(@"[MTRequestMessageService#%" PRIxPTR " will request message %" PRId64 "]", (intptr_t)self, messageId); request.requestContext.responseMessageId = responseMessageId; return true; } else { @@ -1020,6 +1021,8 @@ } } + MTLog(@"[MTRequestMessageService#%" PRIxPTR " will not request message %" PRId64 " (request not found)]", (intptr_t)self, messageId); + return false; } diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index d989444aba..228236a2bc 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -124,6 +124,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/WallpaperGridScreen", "//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen", "//submodules/TelegramUI/Components/Settings/GenerateThemeName", + "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 64f4d74eaf..17af5284bf 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -297,7 +297,8 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView autoremoveTimeout: nil, storyState: nil, requiresPremiumForMessaging: false, - displayAsTopicList: false + displayAsTopicList: false, + tags: [] )), editing: false, hasActiveRevealControls: false, diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 10d7b69502..d096a99ee1 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -444,7 +444,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { autoremoveTimeout: nil, storyState: nil, requiresPremiumForMessaging: false, - displayAsTopicList: false + displayAsTopicList: false, + tags: [] )), editing: false, hasActiveRevealControls: false, diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 22cd47d571..cdc6600918 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -22,6 +22,7 @@ import PeerNameColorScreen import ThemeCarouselItem import ThemeAccentColorScreen import WallpaperGridScreen +import PeerNameColorItem private final class ThemeSettingsControllerArguments { let context: AccountContext diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 4b93034336..87c5e65003 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -114,7 +114,7 @@ public struct CachedPremiumGiftOption: Equatable, PostboxCoding { } } -public enum PeerNameColor: Equatable { +public enum PeerNameColor: Hashable { case red case orange case violet diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ChatListData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ChatListData.swift index e6359c3365..7bb681a2f8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ChatListData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ChatListData.swift @@ -3,6 +3,23 @@ import Postbox public extension TelegramEngine.EngineData.Item { enum ChatList { - + public struct FiltersDisplayTags: TelegramEngineDataItem, PostboxViewDataItem { + public typealias Result = Bool + + public init() { + } + + var key: PostboxViewKey { + return .preferences(keys: Set([PreferencesKeys.chatListFilters])) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? PreferencesView else { + preconditionFailure() + } + let state = view.values[PreferencesKeys.chatListFilters]?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default + return state.displayTags + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 76e4cee9f5..cec7854829 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -182,6 +182,7 @@ public struct ChatListFilterData: Equatable, Hashable { public var excludeArchived: Bool public var includePeers: ChatListFilterIncludePeers public var excludePeers: [PeerId] + public var color: PeerNameColor? public init( isShared: Bool, @@ -191,7 +192,8 @@ public struct ChatListFilterData: Equatable, Hashable { excludeRead: Bool, excludeArchived: Bool, includePeers: ChatListFilterIncludePeers, - excludePeers: [PeerId] + excludePeers: [PeerId], + color: PeerNameColor? ) { self.isShared = isShared self.hasSharedLinks = hasSharedLinks @@ -201,6 +203,7 @@ public struct ChatListFilterData: Equatable, Hashable { self.excludeArchived = excludeArchived self.includePeers = includePeers self.excludePeers = excludePeers + self.color = color } public mutating func addIncludePeer(peerId: PeerId) -> Bool { @@ -262,7 +265,8 @@ public enum ChatListFilter: Codable, Equatable { peers: (try container.decode([Int64].self, forKey: "includePeers")).map(PeerId.init), pinnedPeers: (try container.decode([Int64].self, forKey: "pinnedPeers")).map(PeerId.init) ), - excludePeers: (try container.decode([Int64].self, forKey: "excludePeers")).map(PeerId.init) + excludePeers: (try container.decode([Int64].self, forKey: "excludePeers")).map(PeerId.init), + color: (try container.decodeIfPresent(Int32.self, forKey: "color")).flatMap(PeerNameColor.init(rawValue:)) ) self = .filter(id: id, title: title, emoticon: emoticon, data: data) } @@ -292,6 +296,7 @@ public enum ChatListFilter: Codable, Equatable { try container.encode(data.includePeers.peers.map { $0.toInt64() }, forKey: "includePeers") try container.encode(data.includePeers.pinnedPeers.map { $0.toInt64() }, forKey: "pinnedPeers") try container.encode(data.excludePeers.map { $0.toInt64() }, forKey: "excludePeers") + try container.encodeIfPresent(data.color?.rawValue, forKey: "color") } } } @@ -347,7 +352,8 @@ extension ChatListFilter { default: return nil } - } + }, + color: nil ) ) case let .dialogFilterChatlist(flags, id, title, emoticon, pinnedPeers, includePeers): @@ -385,7 +391,8 @@ extension ChatListFilter { return nil } }), - excludePeers: [] + excludePeers: [], + color: nil ) ) } @@ -882,12 +889,15 @@ struct ChatListFiltersState: Codable, Equatable { var updates: [ChatListFilterUpdates] - static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil, updates: []) + var displayTags: Bool - fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?, updates: [ChatListFilterUpdates]) { + static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil, updates: [], displayTags: false) + + fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?, updates: [ChatListFilterUpdates], displayTags: Bool) { self.filters = filters self.remoteFilters = remoteFilters self.updates = updates + self.displayTags = displayTags } public init(from decoder: Decoder) throws { @@ -896,6 +906,7 @@ struct ChatListFiltersState: Codable, Equatable { self.filters = try container.decode([ChatListFilter].self, forKey: "filters") self.remoteFilters = try container.decodeIfPresent([ChatListFilter].self, forKey: "remoteFilters") self.updates = try container.decodeIfPresent([ChatListFilterUpdates].self, forKey: "updates") ?? [] + self.displayTags = try container.decodeIfPresent(Bool.self, forKey: "displayTags") ?? false } func encode(to encoder: Encoder) throws { @@ -904,6 +915,7 @@ struct ChatListFiltersState: Codable, Equatable { try container.encode(self.filters, forKey: "filters") try container.encodeIfPresent(self.remoteFilters, forKey: "remoteFilters") try container.encode(self.updates, forKey: "updates") + try container.encode(self.displayTags, forKey: "displayTags") } mutating func normalize() { @@ -1068,7 +1080,8 @@ public struct ChatListFeaturedFilter: Codable, Equatable { peers: (try container.decode([Int64].self, forKey: ("includePeers"))).map(PeerId.init), pinnedPeers: (try container.decode([Int64].self, forKey: ("pinnedPeers"))).map(PeerId.init) ), - excludePeers: (try container.decode([Int64].self, forKey: ("excludePeers"))).map(PeerId.init) + excludePeers: (try container.decode([Int64].self, forKey: ("excludePeers"))).map(PeerId.init), + color: nil ) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index ee8a1629fe..0d1e3ea5d7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -587,6 +587,16 @@ public extension TelegramEngine { public func updateChatListFiltersInteractively(_ f: @escaping ([ChatListFilter]) -> [ChatListFilter]) -> Signal<[ChatListFilter], NoError> { return _internal_updateChatListFiltersInteractively(postbox: self.account.postbox, f) } + + public func updateChatListFiltersDisplayTags(isEnabled: Bool) { + let _ = self.account.postbox.transaction({ transaction in + updateChatListFiltersState(transaction: transaction, { state in + var state = state + state.displayTags = isEnabled + return state + }) + }).start() + } public func updatedChatListFilters() -> Signal<[ChatListFilter], NoError> { return _internal_updatedChatListFilters(postbox: self.account.postbox, hiddenIds: self.account.viewTracker.hiddenChatListFilterIds) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 3d22f9ef11..e5fca19f8a 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -432,6 +432,8 @@ swift_library( "//submodules/TelegramUI/Components/Chat/TopMessageReactions", "//submodules/TelegramUI/Components/Chat/SavedTagNameAlertController", "//submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent", + "//submodules/TelegramUI/Components/Settings/BusinessSetupScreen", + "//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/BackButtonComponent/BUILD b/submodules/TelegramUI/Components/BackButtonComponent/BUILD new file mode 100644 index 0000000000..0ef42567cf --- /dev/null +++ b/submodules/TelegramUI/Components/BackButtonComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BackButtonComponent", + module_name = "BackButtonComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/ComponentFlow", + "//submodules/Components/BundleIconComponent", + "//submodules/Display", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/BackButtonComponent/Sources/BackButtonComponent.swift b/submodules/TelegramUI/Components/BackButtonComponent/Sources/BackButtonComponent.swift new file mode 100644 index 0000000000..88379e4199 --- /dev/null +++ b/submodules/TelegramUI/Components/BackButtonComponent/Sources/BackButtonComponent.swift @@ -0,0 +1,103 @@ +import Foundation +import UIKit +import ComponentFlow +import BundleIconComponent +import Display + +public final class BackButtonComponent: Component { + public let title: String + public let color: UIColor + public let action: () -> Void + + public init( + title: String, + color: UIColor, + action: @escaping () -> Void + ) { + self.title = title + self.color = color + self.action = action + } + + public static func ==(lhs: BackButtonComponent, rhs: BackButtonComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private let arrow = ComponentView() + private let title = ComponentView() + + private var component: BackButtonComponent? + + public override init(frame: CGRect) { + super.init(frame: frame) + + self.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.layer.removeAnimation(forKey: "opacity") + self.alpha = 0.65 + } else { + self.alpha = 1.0 + self.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + func update(component: BackButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let sideInset: CGFloat = 4.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(Text(text: component.title, font: Font.regular(17.0), color: component.color)), + environment: {}, + containerSize: CGSize(width: availableSize.width - 4.0, height: availableSize.height) + ) + + let size = CGSize(width: sideInset * 2.0 + titleSize.width, height: availableSize.height) + + let titleFrame = titleSize.centered(in: CGRect(origin: CGPoint(), size: size)) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint() + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.origin) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 087597bc24..0340332567 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -757,7 +757,8 @@ public final class ChatInlineSearchResultsListComponent: Component { autoremoveTimeout: nil, storyState: nil, requiresPremiumForMessaging: false, - displayAsTopicList: false + displayAsTopicList: false, + tags: [] )), editing: false, hasActiveRevealControls: false, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index d400478e3c..3b33dfa6e0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -88,6 +88,7 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess if let sourceAuthorInfo = message.sourceAuthorInfo, let orignalDate = sourceAuthorInfo.orignalDate { timestamp = orignalDate + isLocalTimestamp = false } var dateText = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat) @@ -102,7 +103,7 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess var t: time_t = time_t(timestamp) var timeinfo: tm = tm() - gmtime_r(&t, &timeinfo) + localtime_r(&t, &timeinfo) var now: time_t = time_t(nowTimestamp) var timeinfoNow: tm = tm() diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD index dae202b12d..ee143c4bab 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD +++ b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/SwitchNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 957dca00c5..b6c169f388 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -4,25 +4,34 @@ import Display import ComponentFlow import TelegramPresentationData import ListSectionComponent +import SwitchNode public final class ListActionItemComponent: Component { + public enum Accessory: Equatable { + case arrow + case toggle(Bool) + } + public let theme: PresentationTheme public let title: AnyComponent + public let leftIcon: AnyComponentWithIdentity? public let icon: AnyComponentWithIdentity? - public let hasArrow: Bool + public let accessory: Accessory? public let action: ((UIView) -> Void)? public init( theme: PresentationTheme, title: AnyComponent, - icon: AnyComponentWithIdentity?, - hasArrow: Bool = true, + leftIcon: AnyComponentWithIdentity? = nil, + icon: AnyComponentWithIdentity? = nil, + accessory: Accessory? = .arrow, action: ((UIView) -> Void)? ) { self.theme = theme self.title = title + self.leftIcon = leftIcon self.icon = icon - self.hasArrow = hasArrow + self.accessory = accessory self.action = action } @@ -33,10 +42,13 @@ public final class ListActionItemComponent: Component { if lhs.title != rhs.title { return false } + if lhs.leftIcon != rhs.leftIcon { + return false + } if lhs.icon != rhs.icon { return false } - if lhs.hasArrow != rhs.hasArrow { + if lhs.accessory != rhs.accessory { return false } if (lhs.action == nil) != (rhs.action == nil) { @@ -47,9 +59,11 @@ public final class ListActionItemComponent: Component { public final class View: HighlightTrackingButton, ListSectionComponent.ChildView { private let title = ComponentView() + private var leftIcon: ComponentView? private var icon: ComponentView? - private let arrowView: UIImageView + private var arrowView: UIImageView? + private var switchNode: IconSwitchNode? private var component: ListActionItemComponent? @@ -57,15 +71,16 @@ public final class ListActionItemComponent: Component { return self.icon?.view } + public var leftIconView: UIView? { + return self.leftIcon?.view + } + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var separatorInset: CGFloat = 0.0 public override init(frame: CGRect) { - self.arrowView = UIImageView() - super.init(frame: CGRect()) - self.addSubview(self.arrowView) - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.internalHighligthedChanged = { [weak self] isHighlighted in guard let self else { @@ -91,13 +106,27 @@ public final class ListActionItemComponent: Component { self.isEnabled = component.action != nil - let verticalInset: CGFloat = 11.0 + let themeUpdated = component.theme !== previousComponent?.theme - let contentLeftInset: CGFloat = 16.0 - let contentRightInset: CGFloat = component.hasArrow ? 30.0 : 16.0 + let verticalInset: CGFloat = 12.0 + + var contentLeftInset: CGFloat = 16.0 + let contentRightInset: CGFloat + switch component.accessory { + case .none: + contentRightInset = 16.0 + case .arrow: + contentRightInset = 30.0 + case .toggle: + contentRightInset = 42.0 + } var contentHeight: CGFloat = 0.0 contentHeight += verticalInset + + if component.leftIcon != nil { + contentLeftInset += 46.0 + } let titleSize = self.title.update( transition: transition, @@ -163,15 +192,111 @@ public final class ListActionItemComponent: Component { } } - if self.arrowView.image == nil { - self.arrowView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)?.withRenderingMode(.alwaysTemplate) + if let leftIconValue = component.leftIcon { + if previousComponent?.leftIcon?.id != leftIconValue.id, let leftIcon = self.leftIcon { + self.leftIcon = nil + if let iconView = leftIcon.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + } + } + + var leftIconTransition = transition + let leftIcon: ComponentView + if let current = self.leftIcon { + leftIcon = current + } else { + leftIconTransition = leftIconTransition.withAnimation(.none) + leftIcon = ComponentView() + self.leftIcon = leftIcon + } + + let leftIconSize = leftIcon.update( + transition: leftIconTransition, + component: leftIconValue.component, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let leftIconFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - leftIconSize.width) * 0.5), y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize) + if let leftIconView = leftIcon.view { + if leftIconView.superview == nil { + leftIconView.isUserInteractionEnabled = false + self.addSubview(leftIconView) + transition.animateAlpha(view: leftIconView, from: 0.0, to: 1.0) + } + leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame) + } + } else { + if let leftIcon = self.leftIcon { + self.leftIcon = nil + if let leftIconView = leftIcon.view { + transition.setAlpha(view: leftIconView, alpha: 0.0, completion: { [weak leftIconView] _ in + leftIconView?.removeFromSuperview() + }) + } + } } - self.arrowView.tintColor = component.theme.list.disclosureArrowColor - if let image = self.arrowView.image { - let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 7.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) - transition.setFrame(view: self.arrowView, frame: arrowFrame) + + if case .arrow = component.accessory { + let arrowView: UIImageView + var arrowTransition = transition + if let current = self.arrowView { + arrowView = current + } else { + arrowTransition = arrowTransition.withAnimation(.none) + arrowView = UIImageView(image: PresentationResourcesItemList.disclosureArrowImage(component.theme)?.withRenderingMode(.alwaysTemplate)) + self.arrowView = arrowView + self.addSubview(arrowView) + } + + arrowView.tintColor = component.theme.list.disclosureArrowColor + + if let image = arrowView.image { + let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 7.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) + arrowTransition.setFrame(view: arrowView, frame: arrowFrame) + } + } else { + if let arrowView = self.arrowView { + self.arrowView = nil + arrowView.removeFromSuperview() + } } - transition.setAlpha(view: self.arrowView, alpha: component.hasArrow ? 1.0 : 0.0) + + if case let .toggle(isOn) = component.accessory { + let switchNode: IconSwitchNode + var switchTransition = transition + var updateSwitchTheme = themeUpdated + if let current = self.switchNode { + switchNode = current + switchNode.setOn(isOn, animated: !transition.animation.isImmediate) + } else { + switchTransition = switchTransition.withAnimation(.none) + updateSwitchTheme = true + switchNode = IconSwitchNode() + switchNode.setOn(isOn, animated: false) + self.addSubview(switchNode.view) + } + + if updateSwitchTheme { + switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor + switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor + switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor + switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor + switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor + } + + let switchSize = CGSize(width: 51.0, height: 31.0) + let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize) + switchTransition.setFrame(view: switchNode.view, frame: switchFrame) + } else { + if let switchNode = self.switchNode { + self.switchNode = nil + switchNode.view.removeFromSuperview() + } + } + + self.separatorInset = contentLeftInset return CGSize(width: availableSize.width, height: contentHeight) } diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index afea63de5f..ce6b71a1ef 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -7,6 +7,7 @@ import DynamicCornerRadiusView public protocol ListSectionComponentChildView: AnyObject { var customUpdateIsHighlighted: ((Bool) -> Void)? { get set } + var separatorInset: CGFloat { get } } public final class ListSectionComponent: Component { @@ -23,19 +24,25 @@ public final class ListSectionComponent: Component { public let header: AnyComponent? public let footer: AnyComponent? public let items: [AnyComponentWithIdentity] + public let displaySeparators: Bool + public let extendsItemHighlightToSection: Bool public init( theme: PresentationTheme, background: Background = .all, header: AnyComponent?, footer: AnyComponent?, - items: [AnyComponentWithIdentity] + items: [AnyComponentWithIdentity], + displaySeparators: Bool = true, + extendsItemHighlightToSection: Bool = false ) { self.theme = theme self.background = background self.header = header self.footer = footer self.items = items + self.displaySeparators = displaySeparators + self.extendsItemHighlightToSection = extendsItemHighlightToSection } public static func ==(lhs: ListSectionComponent, rhs: ListSectionComponent) -> Bool { @@ -54,18 +61,41 @@ public final class ListSectionComponent: Component { if lhs.items != rhs.items { return false } + if lhs.displaySeparators != rhs.displaySeparators { + return false + } + if lhs.extendsItemHighlightToSection != rhs.extendsItemHighlightToSection { + return false + } return true } + private final class ItemView: UIView { + let contents = ComponentView() + let separatorLayer = SimpleLayer() + let highlightLayer = SimpleLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + public final class View: UIView { private let contentView: UIView + private let contentSeparatorContainerLayer: SimpleLayer + private let contentHighlightContainerLayer: SimpleLayer + private let contentItemContainerView: UIView private let contentBackgroundView: DynamicCornerRadiusView private var header: ComponentView? private var footer: ComponentView? - private var itemViews: [AnyHashable: ComponentView] = [:] + private var itemViews: [AnyHashable: ItemView] = [:] - private var isHighlighted: Bool = false + private var highlightedItemId: AnyHashable? private var component: ListSectionComponent? @@ -73,45 +103,64 @@ public final class ListSectionComponent: Component { self.contentView = UIView() self.contentView.clipsToBounds = true + self.contentSeparatorContainerLayer = SimpleLayer() + self.contentHighlightContainerLayer = SimpleLayer() + self.contentItemContainerView = UIView() + self.contentBackgroundView = DynamicCornerRadiusView() super.init(frame: CGRect()) self.addSubview(self.contentBackgroundView) self.addSubview(self.contentView) + + self.contentView.layer.addSublayer(self.contentSeparatorContainerLayer) + self.contentView.layer.addSublayer(self.contentHighlightContainerLayer) + self.contentView.addSubview(self.contentItemContainerView) } required public init?(coder: NSCoder) { preconditionFailure() } - private func updateIsHighlighted(isHighlighted: Bool) { - if self.isHighlighted == isHighlighted { + private func updateHighlightedItem(itemId: AnyHashable?) { + if self.highlightedItemId == itemId { return } - self.isHighlighted = isHighlighted + let previousHighlightedItemId = self.highlightedItemId + self.highlightedItemId = itemId guard let component = self.component else { return } - let transition: Transition - let backgroundColor: UIColor - if isHighlighted { - transition = .immediate - backgroundColor = component.theme.list.itemHighlightedBackgroundColor + if component.extendsItemHighlightToSection { + let transition: Transition + let backgroundColor: UIColor + if itemId != nil { + transition = .immediate + backgroundColor = component.theme.list.itemHighlightedBackgroundColor + } else { + transition = .easeInOut(duration: 0.2) + backgroundColor = component.theme.list.itemBlocksBackgroundColor + } + + self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition) } else { - transition = .easeInOut(duration: 0.2) - backgroundColor = component.theme.list.itemBlocksBackgroundColor + if let previousHighlightedItemId, let previousItemView = self.itemViews[previousHighlightedItemId] { + Transition.easeInOut(duration: 0.2).setBackgroundColor(layer: previousItemView.highlightLayer, color: .clear) + } + if let itemId, let itemView = self.itemViews[itemId] { + Transition.immediate.setBackgroundColor(layer: itemView.highlightLayer, color: component.theme.list.itemHighlightedBackgroundColor) + } } - self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition) } func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component let backgroundColor: UIColor - if self.isHighlighted { + if self.highlightedItemId != nil && component.extendsItemHighlightToSection { backgroundColor = component.theme.list.itemHighlightedBackgroundColor } else { backgroundColor = component.theme.list.itemBlocksBackgroundColor @@ -155,41 +204,74 @@ public final class ListSectionComponent: Component { var innerContentHeight: CGFloat = 0.0 var validItemIds: [AnyHashable] = [] - for item in component.items { - validItemIds.append(item.id) + for i in 0 ..< component.items.count { + let item = component.items[i] + let itemId = item.id + validItemIds.append(itemId) - let itemView: ComponentView + let itemView: ItemView var itemTransition = transition - if let current = self.itemViews[item.id] { + if let current = self.itemViews[itemId] { itemView = current } else { itemTransition = itemTransition.withAnimation(.none) - itemView = ComponentView() - self.itemViews[item.id] = itemView + itemView = ItemView() + self.itemViews[itemId] = itemView } - let itemSize = itemView.update( + let itemSize = itemView.contents.update( transition: itemTransition, component: item.component, environment: {}, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: itemSize) - if let itemComponentView = itemView.view { + if let itemComponentView = itemView.contents.view { if itemComponentView.superview == nil { - self.contentView.addSubview(itemComponentView) - transition.animateAlpha(view: itemComponentView, from: 0.0, to: 1.0) + itemView.addSubview(itemComponentView) + self.contentItemContainerView.addSubview(itemView) + self.contentSeparatorContainerLayer.addSublayer(itemView.separatorLayer) + self.contentHighlightContainerLayer.addSublayer(itemView.highlightLayer) + transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) + transition.animateAlpha(layer: itemView.separatorLayer, from: 0.0, to: 1.0) + transition.animateAlpha(layer: itemView.highlightLayer, from: 0.0, to: 1.0) if let itemComponentView = itemComponentView as? ChildView { itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in guard let self else { return } - self.updateIsHighlighted(isHighlighted: isHighlighted) + self.updateHighlightedItem(itemId: isHighlighted ? itemId : nil) } } } - itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + var separatorInset: CGFloat = 0.0 + if let itemComponentView = itemComponentView as? ChildView { + separatorInset = itemComponentView.separatorInset + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + + let itemSeparatorTopOffset: CGFloat = i == 0 ? 0.0 : -UIScreenPixel + let itemHighlightFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + itemSeparatorTopOffset), size: CGSize(width: itemFrame.width, height: itemFrame.height - itemSeparatorTopOffset)) + itemTransition.setFrame(layer: itemView.highlightLayer, frame: itemHighlightFrame) + + itemTransition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size)) + + let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: availableSize.width - separatorInset, height: UIScreenPixel)) + itemTransition.setFrame(layer: itemView.separatorLayer, frame: itemSeparatorFrame) + + let separatorAlpha: CGFloat + if component.displaySeparators { + if i != component.items.count - 1 { + separatorAlpha = 1.0 + } else { + separatorAlpha = 0.0 + } + } else { + separatorAlpha = 0.0 + } + itemTransition.setAlpha(layer: itemView.separatorLayer, alpha: separatorAlpha) + itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor } innerContentHeight += itemSize.height } @@ -198,11 +280,17 @@ public final class ListSectionComponent: Component { if !validItemIds.contains(id) { removedItemIds.append(id) - if let itemComponentView = itemView.view { - transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in - itemComponentView?.removeFromSuperview() - }) - } + transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in + itemView?.removeFromSuperview() + }) + let separatorLayer = itemView.separatorLayer + transition.setAlpha(layer: separatorLayer, alpha: 0.0, completion: { [weak separatorLayer] _ in + separatorLayer?.removeFromSuperlayer() + }) + let highlightLayer = itemView.highlightLayer + transition.setAlpha(layer: highlightLayer, alpha: 0.0, completion: { [weak highlightLayer] _ in + highlightLayer?.removeFromSuperlayer() + }) } } for id in removedItemIds { @@ -216,6 +304,10 @@ public final class ListSectionComponent: Component { let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: innerContentHeight)) transition.setFrame(view: self.contentView, frame: contentFrame) + transition.setFrame(view: self.contentItemContainerView, frame: CGRect(origin: CGPoint(), size: contentFrame.size)) + transition.setFrame(layer: self.contentSeparatorContainerLayer, frame: CGRect(origin: CGPoint(), size: contentFrame.size)) + transition.setFrame(layer: self.contentHighlightContainerLayer, frame: CGRect(origin: CGPoint(), size: contentFrame.size)) + let backgroundFrame: CGRect var backgroundAlpha: CGFloat = 1.0 var contentCornerRadius: CGFloat = 11.0 @@ -231,8 +323,8 @@ public final class ListSectionComponent: Component { backgroundFrame = contentFrame self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) case let .range(from, corners): - if let itemComponentView = self.itemViews[from]?.view, itemComponentView.frame.minY < contentFrame.height { - backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY + itemComponentView.frame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height - itemComponentView.frame.minY)) + if let itemView = self.itemViews[from], itemView.frame.minY < contentFrame.height { + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY + itemView.frame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height - itemView.frame.minY)) } else { backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minY, y: contentFrame.height), size: CGSize(width: contentFrame.width, height: 0.0)) } diff --git a/submodules/TelegramUI/Components/ListTextFieldItemComponent/BUILD b/submodules/TelegramUI/Components/ListTextFieldItemComponent/BUILD new file mode 100644 index 0000000000..c0256f3093 --- /dev/null +++ b/submodules/TelegramUI/Components/ListTextFieldItemComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListTextFieldItemComponent", + module_name = "ListTextFieldItemComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/Components/MultilineTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift new file mode 100644 index 0000000000..c91f9b9414 --- /dev/null +++ b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift @@ -0,0 +1,152 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import MultilineTextComponent + +public final class ListTextFieldItemComponent: Component { + public let theme: PresentationTheme + public let initialText: String + public let placeholder: String + public let updated: ((String) -> Void)? + + public init( + theme: PresentationTheme, + initialText: String, + placeholder: String, + updated: ((String) -> Void)? + ) { + self.theme = theme + self.initialText = initialText + self.placeholder = placeholder + self.updated = updated + } + + public static func ==(lhs: ListTextFieldItemComponent, rhs: ListTextFieldItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.initialText != rhs.initialText { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if (lhs.updated == nil) != (rhs.updated == nil) { + return false + } + return true + } + + private final class TextField: UITextField { + var sideInset: CGFloat = 0.0 + + override func textRect(forBounds bounds: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) + } + } + + public final class View: UIView, UITextFieldDelegate { + private let textField: TextField + private let placeholder = ComponentView() + + private var component: ListTextFieldItemComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + public var currentText: String { + return self.textField.text ?? "" + } + + public override init(frame: CGRect) { + self.textField = TextField() + + super.init(frame: CGRect()) + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return true + } + + @objc private func textDidChange() { + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + } + + func update(component: ListTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let previousComponent = self.component + self.component = component + self.state = state + + self.textField.isEnabled = component.updated != nil + + if self.textField.superview == nil { + self.textField.text = component.initialText + self.addSubview(self.textField) + self.textField.delegate = self + self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) + } + + let themeUpdated = component.theme !== previousComponent?.theme + + if themeUpdated { + self.textField.font = Font.regular(17.0) + self.textField.textColor = component.theme.list.itemPrimaryTextColor + } + + let verticalInset: CGFloat = 12.0 + let sideInset: CGFloat = 16.0 + + self.textField.sideInset = sideInset + + let placeholderSize = self.placeholder.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let contentHeight: CGFloat = placeholderSize.height + verticalInset * 2.0 + let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((contentHeight - placeholderSize.height) * 0.5)), size: placeholderSize) + if let placeholderView = self.placeholder.view { + if placeholderView.superview == nil { + placeholderView.layer.anchorPoint = CGPoint() + placeholderView.isUserInteractionEnabled = false + self.insertSubview(placeholderView, belowSubview: self.textField) + } + transition.setPosition(view: placeholderView, position: placeholderFrame.origin) + placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) + + placeholderView.isHidden = !self.currentText.isEmpty + } + + transition.setFrame(view: self.textField, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: contentHeight))) + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 1123166542..f7f139d03c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -138,6 +138,7 @@ swift_library( "//submodules/AttachmentUI", "//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 7556a3c249..3b97e6ad4d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -105,6 +105,7 @@ import BoostLevelIconComponent import PeerInfoChatPaneNode import PeerInfoChatListPaneNode import GroupStickerPackSetupController +import PeerNameColorItem public enum PeerInfoAvatarEditingMode { case generic @@ -509,6 +510,7 @@ private enum PeerInfoSettingsSection { case rememberPassword case emojiStatus case powerSaving + case businessSetup } private enum PeerInfoReportType { @@ -924,7 +926,11 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: presentationData.strings.Settings_Premium, icon: PresentationResourcesSettings.premium, action: { interaction.openSettings(.premium) })) - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { + //TODO:localize + /*items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Telegram Business", icon: PresentationResourcesSettings.chatFolders, action: { + interaction.openSettings(.businessSetup) + }))*/ + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { interaction.openSettings(.premiumGift) })) } @@ -8942,6 +8948,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.invokeDisplayPremiumIntro() case .powerSaving: push(energySavingSettingsScreen(context: self.context)) + case .businessSetup: + push(self.context.sharedContext.makeBusinessSetupScreen(context: self.context)) } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD new file mode 100644 index 0000000000..6866f506c4 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BusinessSetupScreen", + module_name = "BusinessSetupScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/BackButtonComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift new file mode 100644 index 0000000000..26e1df5d66 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift @@ -0,0 +1,426 @@ +import Foundation +import UIKit +import Photos +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import BackButtonComponent +import ListSectionComponent +import ListActionItemComponent +import BundleIconComponent + +final class BusinessSetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: BusinessSetupScreenComponent, rhs: BusinessSetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let title = ComponentView() + private let subtitle = ComponentView() + private let actionsSection = ComponentView() + + private var isUpdating: Bool = false + + private var component: BusinessSetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + var scrolledUp = true + private func updateScrolling(transition: Transition) { + guard let environment = self.environment else { + return + } + + let navigationRevealOffsetY: CGFloat = -(environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) * 0.5) + (self.title.view?.frame.midY ?? 0.0) + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: navigationAlpha) + } + } + + func update(component: BusinessSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + //TODO:localize + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Telegram Business", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 32.0 + + let _ = bottomContentInset + let _ = sectionSpacing + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + contentHeight += 81.0 + + //TODO:localize + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Telegram Business", font: Font.bold(29.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.center) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + contentHeight += titleSize.height + contentHeight += 17.0 + + //TODO:localize + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: "You have now unlocked these additional business features.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.25 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.scrollView.addSubview(subtitleView) + } + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + } + contentHeight += subtitleSize.height + contentHeight += 21.0 + + struct Item { + var icon: String + var title: String + var subtitle: String + var action: () -> Void + } + var items: [Item] = [] + //TODO:localize + items.append(Item( + icon: "Settings/Menu/AddAccount", + title: "Location", + subtitle: "Display the location of your business on your account.", + action: { + } + )) + items.append(Item( + icon: "Settings/Menu/DataVoice", + title: "Opening Hours", + subtitle: "Show to your customers when you are open for business.", + action: { + } + )) + items.append(Item( + icon: "Settings/Menu/Photos", + title: "Quick Replies", + subtitle: "Set up shortcuts with rich text and media to respond to messages faster.", + action: { + } + )) + items.append(Item( + icon: "Settings/Menu/Stories", + title: "Greeting Messages", + subtitle: "Create greetings that will be automatically sent to new customers.", + action: { + } + )) + items.append(Item( + icon: "Settings/Menu/Trending", + title: "Away Messages", + subtitle: "Define messages that are automatically sent when you are off.", + action: { + } + )) + items.append(Item( + icon: "Settings/Menu/DataStickers", + title: "Chatbots", + subtitle: "Add any third-party chatbots that will process customer interactions.", + action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + environment.controller()?.push(component.context.sharedContext.makeChatbotSetupScreen(context: component.context)) + } + )) + + var actionsSectionItems: [AnyComponentWithIdentity] = [] + for item in items { + actionsSectionItems.append(AnyComponentWithIdentity(id: actionsSectionItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: item.title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: item.subtitle, + font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: item.icon, + tintColor: nil + ))), + action: { _ in + item.action() + } + )))) + } + + let actionsSectionSize = self.actionsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: actionsSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let actionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: actionsSectionSize) + if let actionsSectionView = self.actionsSection.view { + if actionsSectionView.superview == nil { + self.scrollView.addSubview(actionsSectionView) + } + transition.setFrame(view: actionsSectionView, frame: actionsSectionFrame) + } + contentHeight += actionsSectionSize.height + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class BusinessSetupScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init(context: AccountContext) { + self.context = context + + super.init(context: context, component: BusinessSetupScreenComponent( + context: context + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? BusinessSetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? BusinessSetupScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD new file mode 100644 index 0000000000..9cc671937c --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatbotSetupScreen", + module_name = "ChatbotSetupScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/Markdown", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/BackButtonComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ListTextFieldItemComponent", + "//submodules/TelegramUI/Components/LottieComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift new file mode 100644 index 0000000000..44b119f895 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -0,0 +1,579 @@ +import Foundation +import UIKit +import Photos +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import BackButtonComponent +import ListSectionComponent +import ListActionItemComponent +import ListTextFieldItemComponent +import BundleIconComponent +import LottieComponent +import Markdown + +private let checkIcon: UIImage = { + return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(1.98) + context.setLineCap(.round) + context.setLineJoin(.round) + context.translateBy(x: 1.0, y: 1.0) + + let _ = try? drawSvgPath(context, path: "M0.215053763,4.36080467 L3.31621263,7.70466293 L3.31621263,7.70466293 C3.35339229,7.74475231 3.41603123,7.74711109 3.45612061,7.70993143 C3.45920681,7.70706923 3.46210733,7.70401312 3.46480451,7.70078171 L9.89247312,0 S ") + })!.withRenderingMode(.alwaysTemplate) +}() + +final class ChatbotSetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: ChatbotSetupScreenComponent, rhs: ChatbotSetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let icon = ComponentView() + private let subtitle = ComponentView() + private let nameSection = ComponentView() + private let accessSection = ComponentView() + private let excludedSection = ComponentView() + private let permissionsSection = ComponentView() + + private var isUpdating: Bool = false + + private var component: ChatbotSetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var chevronImage: UIImage? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + var scrolledUp = true + private func updateScrolling(transition: Transition) { + let navigationRevealOffsetY: CGFloat = 0.0 + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: 1.0) + } + } + + func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + //TODO:localize + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Chatbots", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 32.0 + + let _ = bottomContentInset + let _ = sectionSpacing + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "BotEmoji"), + 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) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + } + transition.setPosition(view: iconView, position: iconFrame.center) + iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + } + + contentHeight += 129.0 + + //TODO:localize + let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More>]()", attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { attributes in + return ("URL", "") + }), textAlignment: .center + )) + if self.chevronImage == nil { + self.chevronImage = UIImage(bundleImageName: "Settings/TextArrowRight") + } + if let range = subtitleString.string.range(of: ">"), let chevronImage = self.chevronImage { + subtitleString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: subtitleString.string)) + } + + //TODO:localize + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(subtitleString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.25, + highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + let _ = component + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.scrollView.addSubview(subtitleView) + } + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + } + contentHeight += subtitleSize.height + contentHeight += 27.0 + + //TODO:localize + let nameSectionSize = self.nameSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Enter the username or URL of the Telegram bot that you want to automatically process your chats.", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent( + theme: environment.theme, + initialText: "", + placeholder: "Bot Username", + updated: { value in + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let nameSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: nameSectionSize) + if let nameSectionView = self.nameSection.view { + if nameSectionView.superview == nil { + self.scrollView.addSubview(nameSectionView) + } + transition.setFrame(view: nameSectionView, frame: nameSectionFrame) + } + contentHeight += nameSectionSize.height + contentHeight += sectionSpacing + + //TODO:localize + let accessSectionSize = self.accessSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "CHATS ACCESSIBLE FOR THE BOT", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "All 1-to-1 Chats Except...", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + image: checkIcon, + tintColor: environment.theme.list.itemAccentColor, + contentMode: .center + ))), + accessory: nil, + action: { _ in + } + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Only Selected Chats", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + image: checkIcon, + tintColor: .clear, + contentMode: .center + ))), + accessory: nil, + action: { _ in + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let accessSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: accessSectionSize) + if let accessSectionView = self.accessSection.view { + if accessSectionView.superview == nil { + self.scrollView.addSubview(accessSectionView) + } + transition.setFrame(view: accessSectionView, frame: accessSectionFrame) + } + contentHeight += accessSectionSize.height + contentHeight += sectionSpacing + + //TODO:localize + let excludedSectionSize = self.excludedSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "EXCLUDED CHATS", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Select chats or entire chat categories which the bot WILL NOT have access to.", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Exclude Chats...", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Chat List/AddIcon", + tintColor: environment.theme.list.itemAccentColor + ))), + accessory: nil, + action: { _ in + } + ))), + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let excludedSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: excludedSectionSize) + if let excludedSectionView = self.excludedSection.view { + if excludedSectionView.superview == nil { + self.scrollView.addSubview(excludedSectionView) + } + transition.setFrame(view: excludedSectionView, frame: excludedSectionFrame) + } + contentHeight += excludedSectionSize.height + contentHeight += sectionSpacing + + //TODO:localize + let permissionsSectionSize = self.permissionsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "BOT PERMISSIONS", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot.", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Reply to Messages", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(true), + action: nil + ))), + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let permissionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: permissionsSectionSize) + if let permissionsSectionView = self.permissionsSection.view { + if permissionsSectionView.superview == nil { + self.scrollView.addSubview(permissionsSectionView) + } + transition.setFrame(view: permissionsSectionView, frame: permissionsSectionFrame) + } + contentHeight += permissionsSectionSize.height + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class ChatbotSetupScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init(context: AccountContext) { + self.context = context + + super.init(context: context, component: ChatbotSetupScreenComponent( + context: context + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? ChatbotSetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? ChatbotSetupScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorItem/BUILD b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/BUILD new file mode 100644 index 0000000000..5c280d2a22 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerNameColorItem", + module_name = "PeerNameColorItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/MergeLists", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift similarity index 91% rename from submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift rename to submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift index 8f5e5fcb86..0df4e747c1 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift @@ -207,17 +207,17 @@ private final class PeerNameColorIconItemNode : ASDisplayNode { } } -final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { - var sectionId: ItemListSectionId +public final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + public var sectionId: ItemListSectionId - let theme: PresentationTheme - let colors: PeerNameColors - let isProfile: Bool - let currentColor: PeerNameColor? - let updated: (PeerNameColor) -> Void - let tag: ItemListItemTag? + public let theme: PresentationTheme + public let colors: PeerNameColors + public let isProfile: Bool + public let currentColor: PeerNameColor? + public let updated: (PeerNameColor) -> Void + public let tag: ItemListItemTag? - init(theme: PresentationTheme, colors: PeerNameColors, isProfile: Bool, currentColor: PeerNameColor?, updated: @escaping (PeerNameColor) -> Void, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId) { + public init(theme: PresentationTheme, colors: PeerNameColors, isProfile: Bool, currentColor: PeerNameColor?, updated: @escaping (PeerNameColor) -> Void, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId) { self.theme = theme self.colors = colors self.isProfile = isProfile @@ -227,7 +227,7 @@ final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemComponentAdap self.sectionId = sectionId } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = PeerNameColorItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -243,7 +243,7 @@ final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemComponentAdap } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? PeerNameColorItemNode { let makeLayout = nodeValue.asyncLayout() @@ -282,7 +282,7 @@ final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemComponentAdap } } -final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { +public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { private let containerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -296,11 +296,11 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { private var item: PeerNameColorItem? private var layoutParams: ListViewItemLayoutParams? - var tag: ItemListItemTag? { + public var tag: ItemListItemTag? { return self.item?.tag } - init() { + public init() { self.containerNode = ASDisplayNode() self.backgroundNode = ASDisplayNode() @@ -319,7 +319,7 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.containerNode) } - func asyncLayout() -> (_ item: PeerNameColorItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: PeerNameColorItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let currentItem = self.item return { item, params, neighbors in @@ -439,7 +439,7 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.items = items - let sideInset: CGFloat = 10.0 + let sideInset: CGFloat = params.leftInset + 10.0 let iconSize = CGSize(width: 32.0, height: 32.0) let spacing = (params.width - sideInset * 2.0 - iconSize.width * CGFloat(itemsPerRow)) / CGFloat(itemsPerRow - 1) @@ -474,11 +474,11 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD index e07e58d229..56a50b2be2 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD @@ -50,6 +50,7 @@ swift_library( "//submodules/Markdown", "//submodules/TelegramUI/Components/GroupStickerPackSetupController", "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", + "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index 2dcb381b36..1f9c0e7378 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -37,6 +37,7 @@ import BoostLevelIconComponent import BundleIconComponent import Markdown import GroupStickerPackSetupController +import PeerNameColorItem private final class EmojiActionIconComponent: Component { let context: AccountContext @@ -1140,7 +1141,9 @@ final class ChannelAppearanceScreenComponent: Component { ), params: ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) ))), - ] + ], + displaySeparators: false, + extendsItemHighlightToSection: true )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 1000.0) @@ -1190,7 +1193,9 @@ final class ChannelAppearanceScreenComponent: Component { self?.displayBoostLevels(subject: nil) } ))) - ] + ], + displaySeparators: false, + extendsItemHighlightToSection: true )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) @@ -1273,7 +1278,9 @@ final class ChannelAppearanceScreenComponent: Component { } ?? environment.theme.list.itemAccentColor, subject: .profile) } ))) - ] + ], + displaySeparators: false, + extendsItemHighlightToSection: true )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) @@ -1306,7 +1313,7 @@ final class ChannelAppearanceScreenComponent: Component { maximumNumberOfLines: 0 )), icon: nil, - hasArrow: false, + accessory: nil, action: { [weak self] view in guard let self else { return @@ -1317,7 +1324,9 @@ final class ChannelAppearanceScreenComponent: Component { self.state?.updated(transition: .spring(duration: 0.4)) } ))) - ] + ], + displaySeparators: false, + extendsItemHighlightToSection: true )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) @@ -1394,7 +1403,9 @@ final class ChannelAppearanceScreenComponent: Component { self.openEmojiPackSetup() } ))) - ] + ], + displaySeparators: false, + extendsItemHighlightToSection: true )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) @@ -1456,7 +1467,9 @@ final class ChannelAppearanceScreenComponent: Component { self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.emojiStatus?.fileId, color: nil, subject: .status) } ))) - ] + ], + displaySeparators: false, + extendsItemHighlightToSection: true )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) @@ -1578,7 +1591,9 @@ final class ChannelAppearanceScreenComponent: Component { self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.replyFileId, color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, subject: .reply) } ))) - ] + ], + displaySeparators: false, + extendsItemHighlightToSection: true )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) @@ -1726,7 +1741,9 @@ final class ChannelAppearanceScreenComponent: Component { )), maximumNumberOfLines: 0 )), - items: wallpaperItems + items: wallpaperItems, + displaySeparators: false, + extendsItemHighlightToSection: true )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift index e227af0c81..945edb7b48 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -13,6 +13,7 @@ import AccountContext import UndoUI import EntityKeyboard import PremiumUI +import PeerNameColorItem private final class PeerNameColorScreenArguments { let context: AccountContext diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 3e186da46a..4944cbc942 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -939,7 +939,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate autoremoveTimeout: nil, storyState: nil, requiresPremiumForMessaging: false, - displayAsTopicList: false + displayAsTopicList: false, + tags: [] )), editing: false, hasActiveRevealControls: false, diff --git a/submodules/TelegramUI/Resources/Animations/BotEmoji.tgs b/submodules/TelegramUI/Resources/Animations/BotEmoji.tgs new file mode 100644 index 0000000000000000000000000000000000000000..0076344f4c799f30199f553d5402e5f8b8c1daf8 GIT binary patch literal 30782 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index b0ebd4e2a1..ce7728a103 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -99,7 +99,7 @@ extension ChatControllerImpl { let reaction = value - let promptController = savedTagNameAlertController(context: self.context, updatedPresentationData: nil, text: optionTitle, subtext: self.presentationData.strings.Chat_EditTagTitle_Text, value: savedMessageTags?.tags.first(where: { $0.reaction == reaction })?.title ?? "", reaction: reaction, file: reactionFile, characterLimit: 10, apply: { [weak self] value in + let promptController = savedTagNameAlertController(context: self.context, updatedPresentationData: nil, text: optionTitle, subtext: self.presentationData.strings.Chat_EditTagTitle_Text, value: savedMessageTags?.tags.first(where: { $0.reaction == reaction })?.title ?? "", reaction: reaction, file: reactionFile, characterLimit: 12, apply: { [weak self] value in guard let self else { return } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 0eef5984a6..757c4e17b4 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -111,7 +111,8 @@ private enum ChatListSearchEntry: Comparable, Identifiable { autoremoveTimeout: nil, storyState: nil, requiresPremiumForMessaging: false, - displayAsTopicList: false + displayAsTopicList: false, + tags: [] )), editing: false, hasActiveRevealControls: false, diff --git a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift index be56406222..dde6928282 100644 --- a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift @@ -786,7 +786,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat return } - let promptController = savedTagNameAlertController(context: self.context, updatedPresentationData: nil, text: optionTitle, subtext: presentationData.strings.Chat_EditTagTitle_Text, value: savedMessageTags?.tags.first(where: { $0.reaction == reaction })?.title ?? "", reaction: reaction, file: reactionFile, characterLimit: 10, apply: { [weak self] value in + let promptController = savedTagNameAlertController(context: self.context, updatedPresentationData: nil, text: optionTitle, subtext: presentationData.strings.Chat_EditTagTitle_Text, value: savedMessageTags?.tags.first(where: { $0.reaction == reaction })?.title ?? "", reaction: reaction, file: reactionFile, characterLimit: 12, apply: { [weak self] value in guard let self else { return } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 8e473e0375..d02afd93df 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -51,6 +51,8 @@ import PeerInfoScreen import ChatQrCodeScreen import UndoUI import ChatMessageNotificationItem +import BusinessSetupScreen +import ChatbotSetupScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1878,6 +1880,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { return archiveSettingsController(context: context) } + public func makeBusinessSetupScreen(context: AccountContext) -> ViewController { + return BusinessSetupScreen(context: context) + } + + public func makeChatbotSetupScreen(context: AccountContext) -> ViewController { + return ChatbotSetupScreen(context: context) + } + public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { var modal = true let mappedSource: PremiumSource