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 0000000000..0076344f4c Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/BotEmoji.tgs differ 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