From e18795980ee38705514330cb97eab7eca3fb1f88 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 18 Dec 2024 23:34:33 +0800 Subject: [PATCH] [WIP] Chat folder emoji --- submodules/ChatListUI/BUILD | 4 + .../ChatListUI/Sources/ChatContextMenus.swift | 12 +- .../Sources/ChatListController.swift | 18 +- .../ChatListFilterPresetController.swift | 510 ++++++++++++++++-- .../ChatListFilterPresetListController.swift | 9 +- .../ChatListFilterTabContainerNode.swift | 74 ++- .../ChatListFilterTagSectionHeaderItem.swift | 364 +++++++++++++ .../ItemListFilterTitleInputItem.swift | 275 ++++++++++ .../Sources/Node/ChatListNode.swift | 6 +- .../ChatSendMessageContextScreen.swift | 1 + .../Sources/ComposePollScreen.swift | 6 +- .../ListComposePollOptionComponent.swift | 33 +- .../ContextUI/Sources/ContextController.swift | 23 +- .../ContextControllerActionsStackNode.swift | 68 ++- ...tControllerExtractedPresentationNode.swift | 6 + .../Sources/ContextSourceContainer.swift | 11 +- .../Sources/PeekControllerNode.swift | 1 + .../FolderInviteLinkListController.swift | 5 +- .../Items/ItemListSectionHeaderItem.swift | 2 +- .../Peers/ChatListFiltering.swift | 67 ++- .../TelegramEngine/Peers/Communities.swift | 37 +- .../Peers/TelegramEnginePeers.swift | 2 +- .../ChatFolderLinkHeaderComponent.swift | 8 +- .../Sources/ChatFolderLinkPreviewScreen.swift | 18 +- .../Sources/EmojiTextAttachmentView.swift | 53 +- .../Sources/StickerPackListContextItem.swift | 2 +- .../Sources/PeerSelectionController.swift | 2 +- .../Components/TextFieldComponent/BUILD | 1 + .../Sources/TextFieldComponent.swift | 60 +++ .../Sources/ChatHistoryListNode.swift | 5 +- .../ContactMultiselectionControllerNode.swift | 4 +- 31 files changed, 1510 insertions(+), 177 deletions(-) create mode 100644 submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift create mode 100644 submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 67bc7a1af1..1eaca712b2 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -105,6 +105,10 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", + "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/ComposePollUI", + "//submodules/ChatPresentationInterfaceState", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 3a93d96776..9b64ab804e 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -221,9 +221,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } return filters } - |> deliverOnMainQueue).startStandalone(completed: { + |> deliverOnMainQueue).startStandalone(completed: { c?.dismiss(completion: { - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + //TODO:release + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) @@ -273,7 +274,8 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } let filterType = chatListFilterType(data) - updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in + //TODO:release + updatedItems.append(.action(ContextMenuActionItem(text: title.text, icon: { theme in let imageName: String switch filterType { case .generic: @@ -337,8 +339,8 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } return filters }).startStandalone() - - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + //TODO:release + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d46cf25f11..809952921f 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -246,7 +246,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.tabsNode = SparseNode() - self.tabContainerNode = ChatListFilterTabContainerNode() + self.tabContainerNode = ChatListFilterTabContainerNode(context: context) self.tabsNode.addSubnode(self.tabContainerNode) super.init(context: context, navigationBarPresentationData: nil, mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource) @@ -1809,10 +1809,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } + //TODO:release let iconColor: UIColor = .white let overlayController: UndoOverlayController if !filterPeersAreMuted.areMuted { - let text = strongSelf.presentationData.strings.ChatList_ToastFolderMuted(title).string + let text = strongSelf.presentationData.strings.ChatList_ToastFolderMuted(title.text).string overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ "Middle.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor, @@ -1821,7 +1822,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController "Line.Group 1.Stroke 1": iconColor ], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) } else { - let text = strongSelf.presentationData.strings.ChatList_ToastFolderUnmuted(title).string + //TODO:release + let text = strongSelf.presentationData.strings.ChatList_ToastFolderUnmuted(title.text).string overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ "Middle.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor, @@ -3920,7 +3922,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) { + private func shareFolder(filterId: Int32, data: ChatListFilterData, title: ChatFolderTitle) { let presentationData = self.presentationData let progressSignal = Signal { [weak self] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) @@ -3962,7 +3964,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController context: self.context, subject: .linkList(folderId: filterId, initialLinks: links ?? []), contents: ChatFolderLinkContents( - localFilterId: filterId, title: title, + localFilterId: filterId, + title: title, peers: [], alreadyMemberPeerIds: Set(), memberCounts: [:] @@ -5928,7 +5931,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive) } } - items.append(.action(ContextMenuActionItem(text: title, badge: badge, icon: { theme in + //TODO:release + items.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, badge: badge, icon: { theme in let imageName: String if isDisabled { imageName = "Chat/Context Menu/Lock" @@ -5981,7 +5985,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 9b3b4522bc..b2b18055d9 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -19,6 +19,12 @@ import ContextUI import AsyncDisplayKit import UndoUI import PeerNameColorItem +import EntityKeyboard +import ComposePollUI +import ChatEntityKeyboardInputNode +import ComponentFlow +import ChatPresentationInterfaceState +import ComponentDisplayAdapters private enum FilterSection: Int32, Hashable { case include @@ -28,6 +34,8 @@ private enum FilterSection: Int32, Hashable { private final class ChatListFilterPresetControllerArguments { let context: AccountContext let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void + let updateName: (ChatFolderTitle) -> Void + let toggleNameAnimations: () -> Void let openAddIncludePeer: () -> Void let openAddExcludePeer: () -> Void let deleteIncludePeer: (EnginePeer.Id) -> Void @@ -49,6 +57,8 @@ private final class ChatListFilterPresetControllerArguments { init( context: AccountContext, updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void, + updateName: @escaping (ChatFolderTitle) -> Void, + toggleNameAnimations: @escaping () -> Void, openAddIncludePeer: @escaping () -> Void, openAddExcludePeer: @escaping () -> Void, deleteIncludePeer: @escaping (EnginePeer.Id) -> Void, @@ -69,6 +79,8 @@ private final class ChatListFilterPresetControllerArguments { ) { self.context = context self.updateState = updateState + self.updateName = updateName + self.toggleNameAnimations = toggleNameAnimations self.openAddIncludePeer = openAddIncludePeer self.openAddExcludePeer = openAddExcludePeer self.deleteIncludePeer = deleteIncludePeer @@ -216,8 +228,8 @@ private enum ChatListFilterRevealedItemId: Equatable { private enum ChatListFilterPresetEntry: ItemListNodeEntry { case screenHeader - case nameHeader(String) - case name(placeholder: String, value: String) + case nameHeader(title: String, enableAnimations: Bool) + case name(placeholder: String, value: NSAttributedString, inputMode: ListComposePollOptionComponent.InputMode?, enableAnimations: Bool) case includePeersHeader(String) case addIncludePeer(title: String) case includeCategory(index: Int, category: ChatListFilterIncludeCategory, title: String, isRevealed: Bool) @@ -234,7 +246,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { case inviteLinkCreate(hasLinks: Bool) case inviteLink(Int, ExportedChatFolderLink) case inviteLinkInfo(text: String) - case tagColorHeader(name: String, color: PeerNameColors.Colors?, isPremium: Bool) + case tagColorHeader(name: ChatFolderTitle, color: PeerNameColors.Colors?, isPremium: Bool) case tagColor(colors: PeerNameColors, currentColor: PeerNameColor?, isPremium: Bool) case tagColorFooter @@ -362,21 +374,36 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { switch self { case .screenHeader: return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section) - case let .nameHeader(title): - return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) - case let .name(placeholder, value): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .done, clearType: .always, maxLength: 12, sectionId: self.section, textUpdated: { value in - arguments.updateState { current in - var state = current - state.name = value - state.changedName = true - return state + case let .nameHeader(title, enableAnimations): + //TODO:localize + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: enableAnimations ? "Disable Animations" : "Enable Animations", action: { + arguments.toggleNameAnimations() + }, sectionId: self.section) + case let .name(placeholder, value, inputMode, enableAnimations): + return ItemListFilterTitleInputItem( + context: arguments.context, + presentationData: presentationData, + text: value, + enableAnimations: enableAnimations, + placeholder: placeholder, + maxLength: 12, + inputMode: inputMode, + sectionId: self.section, + textUpdated: { value in + arguments.updateName(ChatFolderTitle(attributedString: value, enableAnimations: true)) + }, + toggleInputMode: { + arguments.updateState { current in + var state = current + if state.nameInputMode == .emoji { + state.nameInputMode = .keyboard + } else { + state.nameInputMode = .emoji + } + return state + } } - }, action: { - arguments.clearFocus() - }, cleared: { - arguments.focusOnName() - }) + ) case .includePeersHeader(let text), .excludePeersHeader(let text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case .includePeerInfo(let text), .excludePeerInfo(let text): @@ -462,13 +489,13 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { arguments.expandSection(.exclude) }) case let .tagColorHeader(name, color, isPremium): - var badge: String? - var badgeStyle: ItemListSectionHeaderItem.BadgeStyle? + var badge: ChatFolderTitle? + var badgeStyle: ChatListFilterTagSectionHeaderItem.BadgeStyle? var accessoryText: ItemListSectionHeaderAccessoryText? if isPremium { if let color { - badge = name.uppercased() - badgeStyle = ItemListSectionHeaderItem.BadgeStyle( + badge = ChatFolderTitle(text: name.text.uppercased(), entities: name.entities, enableAnimations: name.enableAnimations) + badgeStyle = ChatListFilterTagSectionHeaderItem.BadgeStyle( background: color.main.withMultipliedAlpha(0.1), foreground: color.main ) @@ -478,7 +505,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { } else if color != nil { accessoryText = ItemListSectionHeaderAccessoryText(value: presentationData.strings.ChatListFilter_TagLabelPremiumExpired, color: .generic) } - return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_TagSectionTitle, badge: badge, badgeStyle: badgeStyle, accessoryText: accessoryText, sectionId: self.section) + return ChatListFilterTagSectionHeaderItem(context: arguments.context, presentationData: presentationData, text: presentationData.strings.ChatListFilter_TagSectionTitle, badge: badge, badgeStyle: badgeStyle, accessoryText: accessoryText, sectionId: self.section) case let .tagColor(colors, color, isPremium): return PeerNameColorItem( theme: presentationData.theme, @@ -518,9 +545,41 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { } } +extension ChatFolderTitle { + init(attributedString: NSAttributedString, enableAnimations: Bool) { + let inputStateText = ChatTextInputStateText(attributedText: attributedString) + self.init(text: inputStateText.text, entities: inputStateText.attributes.compactMap { attribute -> MessageTextEntity? in + if case let .customEmoji(_, fileId) = attribute.type { + return MessageTextEntity(range: attribute.range, type: .CustomEmoji(stickerPack: nil, fileId: fileId)) + } + return nil + }, enableAnimations: enableAnimations) + } + + var rawAttributedString: NSAttributedString { + let inputStateText = ChatTextInputStateText(text: self.text, attributes: self.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in + if case let .CustomEmoji(_, fileId) = entity.type { + return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range) + } + return nil + }) + return inputStateText.attributedText() + } + + func attributedString(font: UIFont, textColor: UIColor) -> NSAttributedString { + let result = NSMutableAttributedString(attributedString: self.rawAttributedString) + result.addAttributes([ + .font: font, + .foregroundColor: textColor + ], range: NSRange(location: 0, length: result.length)) + return result + } +} + private struct ChatListFilterPresetControllerState: Equatable { - var name: String + var name: ChatFolderTitle var changedName: Bool + var nameInputMode: ListComposePollOptionComponent.InputMode = .keyboard var color: PeerNameColor? var colorUpdated: Bool = false var includeCategories: ChatListFilterPeerCategories @@ -534,7 +593,7 @@ private struct ChatListFilterPresetControllerState: Equatable { var expandedSections: Set var isComplete: Bool { - if self.name.isEmpty { + if self.name.text.isEmpty { return false } @@ -565,8 +624,8 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres entries.append(.screenHeader) } - entries.append(.nameHeader(presentationData.strings.ChatListFolder_NameSectionHeader)) - entries.append(.name(placeholder: presentationData.strings.ChatListFolder_NamePlaceholder, value: state.name)) + entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.enableAnimations)) + entries.append(.name(placeholder: presentationData.strings.ChatListFolder_NamePlaceholder, value: state.name.rawAttributedString, inputMode: state.nameInputMode, enableAnimations: state.name.enableAnimations)) entries.append(.includePeersHeader(presentationData.strings.ChatListFolder_IncludedSectionHeader)) if includePeers.count < limit { @@ -648,6 +707,7 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres resolvedColor = context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance) } + //TODO:localize entries.append(.tagColorHeader(name: state.name, color: resolvedColor, isPremium: isPremium)) entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor, isPremium: isPremium)) entries.append(.tagColorFooter) @@ -1015,11 +1075,11 @@ func chatListFilterType(_ data: ChatListFilterData) -> ChatListFilterType { } private extension ChatListFilter { - var title: String { + var title: ChatFolderTitle { if case let .filter(_, title, _, _) = self { return title } else { - return "" + return ChatFolderTitle(text: "", entities: [], enableAnimations: true) } } @@ -1040,14 +1100,338 @@ private extension ChatListFilter { } } +private final class ChatListFilterPresetController: ItemListController { + private let context: AccountContext + + private var currentLayout: ContainerViewLayout? + + var titleItemNode: (() -> ItemListFilterTitleInputItemNode?)? + + var currentInputMode: ListComposePollOptionComponent.InputMode? + + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataDisposable: Disposable? + private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() + private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? + private var inputMediaNode: ChatEntityKeyboardInputNode? + private var inputMediaNodeBackground = SimpleLayer() + + private let inputMediaNodeDataPromise = Promise() + + init( + context: AccountContext, + state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), + NoError> + ) { + self.context = context + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: context.sharedContext.presentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: nil) + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: self.context, + chatPeerId: nil, + areCustomEmojiEnabled: true, + hasTrending: false, + hasSearch: true, + hasStickers: false, + hasGifs: false, + hideBackground: true, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + }) + + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _ , _, _, _, _ in + return false + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + guard let self else { + return + } + self.currentInputMode = .keyboard + self.update(transition: .animated(duration: 0.4, curve: .spring)) + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + guard let self else { + return + } + guard let titleItemNode = self.titleItemNode?() else { + return + } + guard let textFieldView = titleItemNode.textFieldView else { + return + } + + if titleItemNode.textFieldState.isEditing { + textFieldView.insertText(text: text) + } + }, + backwardsDeleteText: { [weak self] in + guard let self else { + return + } + guard let titleItemNode = self.titleItemNode?() else { + return + } + guard let textFieldView = titleItemNode.textFieldView else { + return + } + if titleItemNode.textFieldState.isEditing { + textFieldView.backwardsDeleteText() + } + }, + openStickerEditor: { + }, + presentController: { [weak self] c, a in + guard let self else { + return + } + self.present(c, in: .window(.root), with: a) + }, + presentGlobalOverlayController: { [weak self] c, a in + guard let self else { + return + } + self.presentInGlobalOverlay(c, with: a) + }, + getNavigationController: { [weak self] () -> NavigationController? in + guard let self else { + return nil + } + + if let navigationController = self.navigationController as? NavigationController { + return navigationController + } + return nil + }, + requestLayout: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + } + ) + } + + @MainActor required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.inputMediaNodeDataDisposable?.dispose() + } + + private func updateInputMediaNode( + context: AccountContext, + availableSize: CGSize, + bottomInset: CGFloat, + inputHeight: CGFloat, + effectiveInputHeight: CGFloat, + metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, + transition: ComponentTransition + ) -> CGFloat { + let bottomInset: CGFloat = bottomInset + 8.0 + let bottomContainerInset: CGFloat = 0.0 + let needsInputActivation: Bool = !"".isEmpty + + var height: CGFloat = 0.0 + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + let inputMediaNode: ChatEntityKeyboardInputNode + var inputMediaNodeTransition = transition + var animateIn = false + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + animateIn = true + inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none) + inputMediaNode = ChatEntityKeyboardInputNode( + context: context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + useOpaqueTheme: true, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext + ) + inputMediaNode.clipsToBounds = true + + inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true + if inputMediaNode.view.superview == nil { + self.inputMediaNodeBackground.removeAllAnimations() + self.displayNode.layer.addSublayer(self.inputMediaNodeBackground) + self.displayNode.view.addSubview(inputMediaNode.view) + } + self.inputMediaNode = inputMediaNode + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .builtin(WallpaperSettings()), + theme: presentationData.theme, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + accountPeerId: context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: context.account.peerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + importState: nil, + threadData: nil, + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + + self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor + + let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) + let inputNodeHeight = heightAndOverflow.0 + let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) + + let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0)) + + if needsInputActivation { + let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) + ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + if animateIn { + var targetFrame = inputNodeFrame + targetFrame.origin.y = availableSize.height + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame) + + let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0)) + + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame) + + transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } else { + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + height = heightAndOverflow.0 + } else { + if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + var targetFrame = inputMediaNode.frame + targetFrame.origin.y = availableSize.height + transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in + if let inputMediaNode { + Queue.mainQueue().after(0.3) { + inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } + } + }) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in + Queue.mainQueue().after(0.3) { + guard let self else { + return + } + if self.currentInputMode == .keyboard { + self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in + guard let self else { + return + } + + if finished { + self.inputMediaNodeBackground.removeFromSuperlayer() + } + self.inputMediaNodeBackground.removeAllAnimations() + }) + } + } + }) + } + } + + return height + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + + let inputHeight = self.updateInputMediaNode( + context: self.context, + availableSize: layout.size, + bottomInset: layout.intrinsicInsets.bottom, + inputHeight: layout.inputHeight ?? 0.0, + effectiveInputHeight: layout.deviceMetrics.standardInputHeight(inLandscape: false), + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + transition: ComponentTransition(transition) + ) + + var innerLayout = layout + innerLayout.inputHeight = max(innerLayout.inputHeight ?? 0.0, inputHeight) + + super.containerLayoutUpdated(innerLayout, transition: transition) + } + + func update(transition: ContainedViewLayoutTransition) { + if let currentLayout = self.currentLayout { + self.containerLayoutUpdated(currentLayout, transition: transition) + } + } +} + func chatListFilterPresetController(context: AccountContext, currentPreset initialPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { - let initialName: String + let initialName: ChatFolderTitle if let initialPreset { initialName = initialPreset.title } else { - initialName = "" + initialName = ChatFolderTitle(text: "", entities: [], enableAnimations: true) } - var 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: []) + var 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: [] + ) initialState.colorUpdated = true let updatedCurrentPreset: Signal @@ -1061,6 +1445,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi updatedCurrentPreset = .single(nil) } + var withController: (((ChatListFilterPresetController) -> Void) -> Void)? + let stateValue = Atomic(value: initialState) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in @@ -1076,24 +1462,29 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi case .generic: state.name = initialName case .unmuted: - state.name = presentationData.strings.ChatListFolder_NameNonMuted + state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameNonMuted, entities: [], enableAnimations: true) case .unread: - state.name = presentationData.strings.ChatListFolder_NameUnread + state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameUnread, entities: [], enableAnimations: true) case .channels: - state.name = presentationData.strings.ChatListFolder_NameChannels + state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameChannels, entities: [], enableAnimations: true) case .groups: - state.name = presentationData.strings.ChatListFolder_NameGroups + state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameGroups, entities: [], enableAnimations: true) case .bots: - state.name = presentationData.strings.ChatListFolder_NameBots + state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameBots, entities: [], enableAnimations: true) case .contacts: - state.name = presentationData.strings.ChatListFolder_NameContacts + state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameContacts, entities: [], enableAnimations: true) case .nonContacts: - state.name = presentationData.strings.ChatListFolder_NameNonContacts + state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameNonContacts, entities: [], enableAnimations: true) } } } return state }) + withController?({ c in + let state = stateValue.with({ $0 }) + c.currentInputMode = state.nameInputMode + c.update(transition: .animated(duration: 0.5, curve: .spring)) + }) } var skipStateAnimation = false @@ -1180,6 +1571,30 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi updateState: { f in updateState(f) }, + updateName: { name in + if name != stateValue.with({ $0 }).name { + updateState { current in + var name = name + name.enableAnimations = current.name.enableAnimations + + var state = current + state.name = name + state.changedName = true + return state + } + } + }, + toggleNameAnimations: { + updateState { current in + var name = current.name + name.enableAnimations = !current.name.enableAnimations + + var state = current + state.name = name + state.changedName = true + return state + } + }, openAddIncludePeer: { let _ = combineLatest( queue: Queue.mainQueue(), @@ -1714,7 +2129,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi actionsDisposable.dispose() } - let controller = ItemListController(context: context, state: signal) + let controller = ChatListFilterPresetController(context: context, state: signal) controller.navigationPresentation = .modal presentControllerImpl = { [weak controller] c, d in controller?.present(c, in: .window(.root), with: d) @@ -1741,6 +2156,21 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi } controller.view.endEditing(true) } + withController = { [weak controller] f in + guard let controller = controller else { + return + } + f(controller) + } + controller.titleItemNode = { [weak controller] in + var foundItemNode: ItemListFilterTitleInputItemNode? + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListFilterTitleInputItemNode { + foundItemNode = itemNode + } + } + return foundItemNode + } controller.attemptNavigation = { _ in if let attemptNavigationImpl { attemptNavigationImpl({ value in @@ -1812,7 +2242,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi return controller } -func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, checkIfExists: Bool, title: String, peerIds: [EnginePeer.Id], pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, pushPremiumController: @escaping (ViewController) -> Void, completed: @escaping () -> Void, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) { +func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, checkIfExists: Bool, title: ChatFolderTitle, peerIds: [EnginePeer.Id], pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, pushPremiumController: @escaping (ViewController) -> Void, completed: @escaping () -> Void, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) { if peerIds.isEmpty { completed() return diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index a6e9fe1af9..e1105affa4 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -289,7 +289,8 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present } if case let .filter(_, title, _, _) = filter { folderCount += 1 - entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true)) + //TODO:release + entries.append(.preset(index: PresetIndex(value: entries.count), title: title.text, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true)) } } @@ -299,7 +300,8 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present if !filteredSuggestedFilters.isEmpty && actualFilters.count < limits.maxFoldersCount { entries.append(.suggestedListHeader(presentationData.strings.ChatListFolderSettings_RecommendedFoldersSection)) for filter in filteredSuggestedFilters { - entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title, label: filter.description, preset: filter.data)) + //TODO:release + entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title.text, label: filter.description, preset: filter.data)) } if filters.isEmpty { entries.append(.suggestedAddCustom(presentationData.strings.ChatListFolderSettings_RecommendedNewFolder)) @@ -387,7 +389,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters let id = context.engine.peers.generateNewChatListFilterId(filters: filters) - filters.append(.filter(id: id, title: title, emoticon: nil, data: data)) + //TODO:release + filters.append(.filter(id: id, title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), emoticon: nil, data: data)) return filters } |> deliverOnMainQueue).start(next: { _ in diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index dadb6d7ec4..37f621cdfa 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -4,6 +4,8 @@ import AsyncDisplayKit import Display import TelegramCore import TelegramPresentationData +import TextNodeWithEntities +import AccountContext private final class ItemNodeDeleteButtonNode: HighlightableButtonNode { private let pressed: () -> Void @@ -55,6 +57,7 @@ private final class ItemNodeDeleteButtonNode: HighlightableButtonNode { } private final class ItemNode: ASDisplayNode { + private let context: AccountContext private let pressed: (Bool) -> Void private let requestedDeletion: () -> Void @@ -63,11 +66,11 @@ private final class ItemNode: ASDisplayNode { private let extractedBackgroundNode: ASImageNode private let titleContainer: ASDisplayNode - private let titleNode: ImmediateTextNode - private let titleActiveNode: ImmediateTextNode + private let titleNode: ImmediateTextNodeWithEntities + private let titleActiveNode: ImmediateTextNodeWithEntities private let shortTitleContainer: ASDisplayNode - private let shortTitleNode: ImmediateTextNode - private let shortTitleActiveNode: ImmediateTextNode + private let shortTitleNode: ImmediateTextNodeWithEntities + private let shortTitleActiveNode: ImmediateTextNodeWithEntities private let badgeContainerNode: ASDisplayNode private let badgeTextNode: ImmediateTextNode private let badgeBackgroundActiveNode: ASImageNode @@ -84,11 +87,12 @@ private final class ItemNode: ASDisplayNode { private var isDisabled: Bool = false private var theme: PresentationTheme? - private var currentTitle: (String, String)? + private var currentTitle: (ChatFolderTitle, ChatFolderTitle)? private var pointerInteraction: PointerInteraction? - init(pressed: @escaping (Bool) -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void) { + init(context: AccountContext, pressed: @escaping (Bool) -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void) { + self.context = context self.pressed = pressed self.requestedDeletion = requestedDeletion @@ -102,23 +106,23 @@ private final class ItemNode: ASDisplayNode { self.titleContainer = ASDisplayNode() - self.titleNode = ImmediateTextNode() + self.titleNode = ImmediateTextNodeWithEntities() self.titleNode.displaysAsynchronously = false self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) - self.titleActiveNode = ImmediateTextNode() + self.titleActiveNode = ImmediateTextNodeWithEntities() self.titleActiveNode.displaysAsynchronously = false self.titleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) self.titleActiveNode.alpha = 0.0 self.shortTitleContainer = ASDisplayNode() - self.shortTitleNode = ImmediateTextNode() + self.shortTitleNode = ImmediateTextNodeWithEntities() self.shortTitleNode.displaysAsynchronously = false self.shortTitleNode.alpha = 0.0 self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) - self.shortTitleActiveNode = ImmediateTextNode() + self.shortTitleActiveNode = ImmediateTextNodeWithEntities() self.shortTitleActiveNode.displaysAsynchronously = false self.shortTitleActiveNode.alpha = 0.0 self.shortTitleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) @@ -194,7 +198,7 @@ private final class ItemNode: ASDisplayNode { self.pressed(self.isDisabled) } - func updateText(strings: PresentationStrings, title: String, shortTitle: String, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, selectionFraction: CGFloat, isEditing: Bool, isReordering: Bool, canReorderAllChats: Bool, isDisabled: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + func updateText(strings: PresentationStrings, title: ChatFolderTitle, shortTitle: ChatFolderTitle, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, selectionFraction: CGFloat, isEditing: Bool, isReordering: Bool, canReorderAllChats: Bool, isDisabled: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { self.isEditing = isEditing self.isDisabled = isDisabled @@ -221,7 +225,7 @@ private final class ItemNode: ASDisplayNode { self.unreadCount = unreadCount } - self.buttonNode.accessibilityLabel = title + self.buttonNode.accessibilityLabel = title.text if unreadCount > 0 { if self.buttonNode.accessibilityValue == nil || unreadCountUpdated { self.buttonNode.accessibilityValue = strings.VoiceOver_Chat_UnreadMessages(Int32(unreadCount)) @@ -271,11 +275,31 @@ private final class ItemNode: ASDisplayNode { transition.updateAlpha(node: self.shortTitleNode, alpha: deselectionAlpha) transition.updateAlpha(node: self.shortTitleActiveNode, alpha: selectionAlpha) + let titleArguments = TextNodeWithEntities.Arguments( + context: self.context, + cache: self.context.animationCache, + renderer: self.context.animationRenderer, + placeholderColor: presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: false + ) + + self.titleNode.arguments = titleArguments + self.titleActiveNode.arguments = titleArguments + self.shortTitleNode.arguments = titleArguments + self.shortTitleActiveNode.arguments = titleArguments + + self.titleNode.visibility = title.enableAnimations + self.titleActiveNode.visibility = title.enableAnimations + self.shortTitleNode.visibility = title.enableAnimations + self.shortTitleActiveNode.visibility = title.enableAnimations + if themeUpdated || titleUpdated { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) - self.shortTitleNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - self.shortTitleActiveNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + //TODO:release + self.titleNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + self.titleActiveNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + + self.shortTitleNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + self.shortTitleActiveNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) } if unreadCount != 0 { @@ -467,7 +491,7 @@ public struct ChatListFilterTabEntryUnreadCount: Equatable { public enum ChatListFilterTabEntry: Equatable { case all(unreadCount: Int) - case filter(id: Int32, text: String, unread: ChatListFilterTabEntryUnreadCount) + case filter(id: Int32, text: ChatFolderTitle, unread: ChatListFilterTabEntryUnreadCount) public var id: ChatListFilterTabEntryId { switch self { @@ -478,19 +502,19 @@ public enum ChatListFilterTabEntry: Equatable { } } - func title(strings: PresentationStrings) -> String { + func title(strings: PresentationStrings) -> ChatFolderTitle { switch self { case .all: - return strings.ChatList_Tabs_AllChats + return ChatFolderTitle(text: strings.ChatList_Tabs_AllChats, entities: [], enableAnimations: true) case let .filter(_, text, _): return text } } - func shortTitle(strings: PresentationStrings) -> String { + func shortTitle(strings: PresentationStrings) -> ChatFolderTitle { switch self { case .all: - return strings.ChatList_Tabs_All + return ChatFolderTitle(text: strings.ChatList_Tabs_All, entities: [], enableAnimations: true) case let .filter(_, text, _): return text } @@ -498,6 +522,7 @@ public enum ChatListFilterTabEntry: Equatable { } public final class ChatListFilterTabContainerNode: ASDisplayNode { + private let context: AccountContext private let scrollNode: ASScrollNode private let selectedLineNode: ASImageNode private var itemNodes: [ChatListFilterTabEntryId: ItemNode] = [:] @@ -546,7 +571,8 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } } - public override init() { + public init(context: AccountContext) { + self.context = context self.scrollNode = ASScrollNode() self.selectedLineNode = ASImageNode() @@ -778,7 +804,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } else { itemNodeTransition = .immediate wasAdded = true - itemNode = ItemNode(pressed: { [weak self] disabled in + itemNode = ItemNode(context: self.context, pressed: { [weak self] disabled in self?.tabSelected?(filter.id, disabled) }, requestedDeletion: { [weak self] in self?.tabRequestedDeletion?(filter.id) @@ -831,7 +857,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { selectionFraction = 0.0 } - itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: i == 0 ? filter.shortTitle(strings: presentationData.strings) : filter.title(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition) + itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: i == 0 ? filter.shortTitle(strings: presentationData.strings) : filter.title(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition) } var removeKeys: [ChatListFilterTabEntryId] = [] for (id, _) in self.itemNodes { diff --git a/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift b/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift new file mode 100644 index 0000000000..f6cca407a6 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift @@ -0,0 +1,364 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import ActivityIndicator +import ItemListUI +import AccountContext +import TelegramCore +import TextNodeWithEntities + +public class ChatListFilterTagSectionHeaderItem: 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 context: AccountContext + let presentationData: ItemListPresentationData + let text: String + let badge: ChatFolderTitle? + let badgeStyle: BadgeStyle? + let multiline: Bool + let activityIndicator: ItemListSectionHeaderActivityIndicator + let accessoryText: ItemListSectionHeaderAccessoryText? + let actionText: String? + let action: (() -> Void)? + public let sectionId: ItemListSectionId + + public let isAlwaysPlain: Bool = true + + public init(context: AccountContext, presentationData: ItemListPresentationData, text: String, badge: ChatFolderTitle? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId) { + self.context = context + self.presentationData = presentationData + self.text = text + self.badge = badge + self.badgeStyle = badgeStyle + self.multiline = multiline + self.activityIndicator = activityIndicator + self.accessoryText = accessoryText + self.actionText = actionText + self.action = action + self.sectionId = sectionId + } + + 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 = ChatListFilterTagSectionHeaderItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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 { + guard let nodeValue = node() as? ChatListFilterTagSectionHeaderItemNode else { + assertionFailure() + return + } + + let makeLayout = nodeValue.asyncLayout() + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } +} + +public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode { + private var item: ChatListFilterTagSectionHeaderItem? + + private let titleNode: TextNode + private var badgeBackgroundLayer: SimpleLayer? + private var badgeTextNode: TextNodeWithEntities? + private let accessoryTextNode: TextNode + private var accessoryImageNode: ASImageNode? + private var activityIndicator: ActivityIndicator? + private var actionNode: TextNode? + private var actionButtonNode: HighlightableButtonNode? + + private let activateArea: AccessibilityAreaNode + + public init() { + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.accessoryTextNode = TextNode() + self.accessoryTextNode.isUserInteractionEnabled = false + self.accessoryTextNode.contentMode = .left + self.accessoryTextNode.contentsScale = UIScreen.main.scale + + self.activateArea = AccessibilityAreaNode() + self.activateArea.accessibilityTraits = [.staticText, .header] + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.accessoryTextNode) + self.addSubnode(self.activateArea) + } + + public func asyncLayout() -> (_ item: ChatListFilterTagSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeActionLayout = TextNode.asyncLayout(self.actionNode) + let makeBadgeTextLayout = TextNodeWithEntities.asyncLayout(self.badgeTextNode) + let makeAccessoryTextLayout = TextNode.asyncLayout(self.accessoryTextNode) + + let previousItem = self.item + + return { item, params, neighbors in + let leftInset: CGFloat = 15.0 + params.leftInset + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + + var badgeLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? + if let badge = item.badge { + if item.badgeStyle != nil { + let badgeFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize * 12.0 / 13.0) + badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badge.attributedString(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: badge.attributedString(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 + var textRightInset: CGFloat = 20.0 + if let badgeLayoutAndApply { + textRightInset += badgeLayoutAndApply.0.size.width + badgeSpacing + } + + var actionLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let actionText = item.actionText { + let actionLayoutAndApplyValue = makeActionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: actionText, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + actionLayoutAndApply = actionLayoutAndApplyValue + textRightInset += actionLayoutAndApplyValue.0.size.width + 2.0 + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + var accessoryTextString: NSAttributedString? + var accessoryIcon: UIImage? + if let accessoryText = item.accessoryText { + let color: UIColor + switch accessoryText.color { + case .generic: + color = item.presentationData.theme.list.sectionHeaderTextColor + case .destructive: + color = item.presentationData.theme.list.freeTextErrorColor + } + accessoryTextString = NSAttributedString(string: accessoryText.value, font: titleFont, textColor: color) + accessoryIcon = accessoryText.icon + } + let (accessoryLayout, accessoryApply) = makeAccessoryTextLayout(TextNodeLayoutArguments(attributedString: accessoryTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let contentSize: CGSize + var insets = UIEdgeInsets() + + contentSize = CGSize(width: params.width, height: titleLayout.size.height + 13.0) + switch neighbors.top { + case .none: + insets.top += 24.0 + case .otherSection: + insets.top += 28.0 + default: + break + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + let _ = titleApply() + let _ = accessoryApply() + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.text + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: titleLayout.size) + + if let (actionLayout, actionApply) = actionLayoutAndApply { + let actionButtonNode: HighlightableButtonNode + if let current = strongSelf.actionButtonNode { + actionButtonNode = current + } else { + actionButtonNode = HighlightableButtonNode() + strongSelf.actionButtonNode = actionButtonNode + actionButtonNode.hitTestSlop = UIEdgeInsets(top: -4.0, left: -4.0, bottom: -4.0, right: -4.0) + strongSelf.addSubnode(actionButtonNode) + actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed), forControlEvents: .touchUpInside) + } + + let actionNode = actionApply() + if strongSelf.actionNode !== actionNode { + strongSelf.actionNode?.removeFromSupernode() + strongSelf.actionNode = actionNode + actionButtonNode.addSubnode(actionNode) + } + + actionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - actionLayout.size.width, y: 7.0), size: actionLayout.size) + + actionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionLayout.size) + } else { + if let actionNode = strongSelf.actionNode { + strongSelf.actionNode = nil + actionNode.removeFromSupernode() + } + if let actionButtonNode = strongSelf.actionButtonNode { + strongSelf.actionButtonNode = nil + actionButtonNode.removeFromSupernode() + } + } + + if let badgeLayoutAndApply { + let badgeTextNode = badgeLayoutAndApply.1(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: true + )) + let badgeSideInset: CGFloat = 4.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 + if let current = strongSelf.badgeBackgroundLayer { + badgeBackgroundLayer = current + } else { + badgeBackgroundLayer = SimpleLayer() + strongSelf.badgeBackgroundLayer = badgeBackgroundLayer + strongSelf.layer.addSublayer(badgeBackgroundLayer) + } + + if strongSelf.badgeTextNode !== badgeTextNode { + strongSelf.badgeTextNode?.textNode.removeFromSupernode() + strongSelf.badgeTextNode = badgeTextNode + strongSelf.addSubnode(badgeTextNode.textNode) + } + + badgeBackgroundLayer.frame = badgeBackgroundFrame + badgeBackgroundLayer.backgroundColor = item.badgeStyle?.background.cgColor ?? item.presentationData.theme.list.itemCheckColors.fillColor.cgColor + badgeBackgroundLayer.cornerRadius = 5.0 + + badgeTextNode.textNode.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) + if item.badge?.enableAnimations ?? false { + badgeTextNode.visibilityRect = .infinite + } else { + badgeTextNode.visibilityRect = CGRect() + } + } else { + if let badgeTextNode = strongSelf.badgeTextNode { + strongSelf.badgeTextNode = nil + badgeTextNode.textNode.removeFromSupernode() + } + if let badgeBackgroundLayer = strongSelf.badgeBackgroundLayer { + strongSelf.badgeBackgroundLayer = nil + badgeBackgroundLayer.removeFromSuperlayer() + } + } + + var accessoryTextOffset: CGFloat = 0.0 + if let accessoryIcon = accessoryIcon { + accessoryTextOffset += accessoryIcon.size.width + 3.0 + } + strongSelf.accessoryTextNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryLayout.size.width - accessoryTextOffset, y: 7.0), size: accessoryLayout.size) + + if let accessoryIcon = accessoryIcon { + let accessoryImageNode: ASImageNode + if let currentAccessoryImageNode = strongSelf.accessoryImageNode { + accessoryImageNode = currentAccessoryImageNode + } else { + accessoryImageNode = ASImageNode() + accessoryImageNode.displaysAsynchronously = false + accessoryImageNode.displayWithoutProcessing = true + strongSelf.addSubnode(accessoryImageNode) + strongSelf.accessoryImageNode = accessoryImageNode + } + accessoryImageNode.image = accessoryIcon + accessoryImageNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryIcon.size.width, y: 7.0), size: accessoryIcon.size) + } else if let accessoryImageNode = strongSelf.accessoryImageNode { + accessoryImageNode.removeFromSupernode() + strongSelf.accessoryImageNode = nil + } + + if previousItem?.activityIndicator != item.activityIndicator { + if item.activityIndicator.hasActivity { + let activityIndicator: ActivityIndicator + if let currentActivityIndicator = strongSelf.activityIndicator { + activityIndicator = currentActivityIndicator + } else { + activityIndicator = ActivityIndicator(type: .custom(item.presentationData.theme.list.sectionHeaderTextColor, 18.0, 1.0, false)) + strongSelf.addSubnode(activityIndicator) + strongSelf.activityIndicator = activityIndicator + } + activityIndicator.isHidden = false + if previousItem != nil { + activityIndicator.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false) + } + } else if let activityIndicator = strongSelf.activityIndicator { + activityIndicator.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { finished in + if finished { + activityIndicator.isHidden = true + } + }) + } + } + + var activityIndicatorOrigin: CGPoint? + switch item.activityIndicator { + case .left: + activityIndicatorOrigin = CGPoint(x: strongSelf.titleNode.frame.maxX + 6.0, y: 7.0 - UIScreenPixel) + case .right: + activityIndicatorOrigin = CGPoint(x: params.width - leftInset - 18.0, y: 7.0 - UIScreenPixel) + default: + break + } + if let activityIndicatorOrigin = activityIndicatorOrigin { + strongSelf.activityIndicator?.frame = CGRect(origin: activityIndicatorOrigin, size: CGSize(width: 18.0, height: 18.0)) + } + } + }) + } + } + + @objc private func actionButtonPressed() { + self.item?.action?() + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + 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/ChatListUI/Sources/ItemListFilterTitleInputItem.swift b/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift new file mode 100644 index 0000000000..bc91aa74dd --- /dev/null +++ b/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift @@ -0,0 +1,275 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import TextNodeWithEntities +import AccountContext +import ItemListUI +import ComponentFlow +import ComposePollUI +import TextFieldComponent + +public class ItemListFilterTitleInputItem: ListViewItem, ItemListItem { + let context: AccountContext + let presentationData: ItemListPresentationData + let text: NSAttributedString + let enableAnimations: Bool + let placeholder: String + let maxLength: Int + let inputMode: ListComposePollOptionComponent.InputMode? + let enabled: Bool + public let sectionId: ItemListSectionId + let textUpdated: (NSAttributedString) -> Void + let updatedFocus: ((Bool) -> Void)? + let toggleInputMode: () -> Void + public let tag: ItemListItemTag? + + public init( + context: AccountContext, + presentationData: ItemListPresentationData, + text: NSAttributedString, + enableAnimations: Bool, + placeholder: String, + maxLength: Int = 0, + inputMode: ListComposePollOptionComponent.InputMode?, + enabled: Bool = true, + tag: ItemListItemTag? = nil, + sectionId: ItemListSectionId, + textUpdated: @escaping (NSAttributedString) -> Void, + updatedFocus: ((Bool) -> Void)? = nil, + toggleInputMode: @escaping () -> Void + ) { + self.context = context + self.presentationData = presentationData + self.text = text + self.enableAnimations = enableAnimations + self.placeholder = placeholder + self.maxLength = maxLength + self.inputMode = inputMode + self.enabled = enabled + self.tag = tag + self.sectionId = sectionId + self.textUpdated = textUpdated + self.updatedFocus = updatedFocus + self.toggleInputMode = toggleInputMode + } + + 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 = ItemListFilterTitleInputItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? ItemListFilterTitleInputItemNode { + + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +public class ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemNode, ItemListItemFocusableNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + let textFieldState = TextFieldComponent.ExternalState() + private let textField = ComponentView() + private let componentState = EmptyComponentState() + + private var item: ItemListFilterTitleInputItem? + + public var tag: ItemListItemTag? { + return self.item?.tag + } + + var textFieldView: ListComposePollOptionComponent.View? { + return self.textField.view as? ListComposePollOptionComponent.View + } + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + super.init(layerBacked: false, dynamicBounce: false) + } + + override public func didLoad() { + super.didLoad() + } + + public func asyncLayout() -> (_ item: ItemListFilterTitleInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + return { [weak self] item, params, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let leftInset: CGFloat = 16.0 + params.leftInset + let rightInset: CGFloat = 16.0 + params.rightInset + let _ = rightInset + + let separatorHeight = UIScreenPixel + + let contentSize = CGSize(width: params.width, height: 44.0) + let insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) + let _ = attributedPlaceholderText + + return (layout, { + guard let self else { + return + } + self.item = item + + if let _ = updatedTheme { + self.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + self.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + self.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + } + + if self.backgroundNode.supernode == nil { + self.insertSubnode(self.backgroundNode, at: 0) + } + if self.topStripeNode.supernode == nil { + self.insertSubnode(self.topStripeNode, at: 1) + } + if self.bottomStripeNode.supernode == nil { + self.insertSubnode(self.bottomStripeNode, at: 2) + } + if self.maskNode.supernode == nil { + self.insertSubnode(self.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + self.topStripeNode.isHidden = true + default: + hasTopCorners = true + self.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + self.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + self.bottomStripeNode.isHidden = hasCorners + } + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + self.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + self.textField.parentState = self.componentState + self.componentState._updated = { [weak self] transition, _ in + guard let self, let item = self.item else { + return + } + guard let textFieldView = self.textFieldView else { + return + } + item.textUpdated(textFieldView.currentAttributedText) + } + let textFieldSize = self.textField.update( + transition: .immediate, + component: AnyComponent(ListComposePollOptionComponent( + externalState: self.textFieldState, + context: item.context, + theme: item.presentationData.theme, + strings: item.presentationData.strings, + placeholder: NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPlaceholderTextColor), + resetText: self.textField.view == nil ? ListComposePollOptionComponent.ResetText(value: item.text) : nil, + characterLimit: item.maxLength, + enableInlineAnimations: item.enableAnimations, + emptyLineHandling: .notAllowed, + returnKeyAction: { [weak self] in + guard let self else { + return + } + let _ = self + }, + backspaceKeyAction: nil, + selection: nil, + inputMode: item.inputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + self.item?.toggleInputMode() + } + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: layout.size.height) + ) + let textFieldFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: textFieldSize) + if let textFieldView = self.textField.view { + if textFieldView.superview == nil { + self.view.addSubview(textFieldView) + } + textFieldView.frame = textFieldFrame + } + }) + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + public func focus() { + } + + public func selectAll() { + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 93d21da646..177841ad47 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -4295,7 +4295,8 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres if !result.isEmpty { result.append(", ") } - result.append(title) + //TODO:release + result.append(title.text) } } @@ -4422,9 +4423,10 @@ func chatListItemTags(location: ChatListControllerLocation, accountPeerId: Engin if data.color != nil { let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId) if predicate.pinnedPeerIds.contains(peer.id) || predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { + //TODO:release result.append(ChatListItemContent.Tag( id: id, - title: title, + title: title.text, colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue )) } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 29125b33ac..5d0bdb5453 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -650,6 +650,7 @@ final class ChatSendMessageContextScreenComponent: Component { ), animated: !transition.animation.isImmediate) } else { actionsStackNode = ContextControllerActionsStackNode( + context: component.context, getController: { return nil }, diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index 6628446965..3686e0a1bd 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -655,7 +655,7 @@ final class ComposePollScreenComponent: Component { theme: environment.theme, strings: environment.strings, resetText: self.resetPollText.flatMap { resetText in - return ListComposePollOptionComponent.ResetText(value: resetText) + return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText)) }, assumeIsEditing: self.inputMediaNodeTargetTag === self.pollTextFieldTag, characterLimit: component.initialData.maxPollTextLength, @@ -750,7 +750,7 @@ final class ComposePollScreenComponent: Component { theme: environment.theme, strings: environment.strings, resetText: pollOption.resetText.flatMap { resetText in - return ListComposePollOptionComponent.ResetText(value: resetText) + return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText)) }, assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag, characterLimit: component.initialData.maxPollOptionLength, @@ -1139,7 +1139,7 @@ final class ComposePollScreenComponent: Component { theme: environment.theme, strings: environment.strings, resetText: self.resetQuizAnswerText.flatMap { resetText in - return ListComposePollOptionComponent.ResetText(value: resetText) + return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText)) }, assumeIsEditing: self.inputMediaNodeTargetTag === self.quizAnswerTextInputTag, characterLimit: component.initialData.maxPollTextLength, diff --git a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift index 4aba30339b..8e371e3220 100644 --- a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift +++ b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift @@ -16,9 +16,9 @@ import SwiftSignalKit public final class ListComposePollOptionComponent: Component { public final class ResetText: Equatable { - public let value: String + public let value: NSAttributedString - public init(value: String) { + public init(value: NSAttributedString) { self.value = value } @@ -72,9 +72,11 @@ public final class ListComposePollOptionComponent: Component { public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings + public let placeholder: NSAttributedString? public let resetText: ResetText? public let assumeIsEditing: Bool public let characterLimit: Int? + public let enableInlineAnimations: Bool public let emptyLineHandling: TextFieldComponent.EmptyLineHandling public let returnKeyAction: (() -> Void)? public let backspaceKeyAction: (() -> Void)? @@ -88,9 +90,11 @@ public final class ListComposePollOptionComponent: Component { context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, + placeholder: NSAttributedString? = nil, resetText: ResetText? = nil, assumeIsEditing: Bool = false, characterLimit: Int, + enableInlineAnimations: Bool = true, emptyLineHandling: TextFieldComponent.EmptyLineHandling, returnKeyAction: (() -> Void)?, backspaceKeyAction: (() -> Void)?, @@ -103,9 +107,11 @@ public final class ListComposePollOptionComponent: Component { self.context = context self.theme = theme self.strings = strings + self.placeholder = placeholder self.resetText = resetText self.assumeIsEditing = assumeIsEditing self.characterLimit = characterLimit + self.enableInlineAnimations = enableInlineAnimations self.emptyLineHandling = emptyLineHandling self.returnKeyAction = returnKeyAction self.backspaceKeyAction = backspaceKeyAction @@ -128,6 +134,9 @@ public final class ListComposePollOptionComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.placeholder != rhs.placeholder { + return false + } if lhs.resetText != rhs.resetText { return false } @@ -137,6 +146,9 @@ public final class ListComposePollOptionComponent: Component { if lhs.characterLimit != rhs.characterLimit { return false } + if lhs.enableInlineAnimations != rhs.enableInlineAnimations { + return false + } if lhs.emptyLineHandling != rhs.emptyLineHandling { return false } @@ -244,6 +256,14 @@ public final class ListComposePollOptionComponent: Component { } } + public var currentAttributedText: NSAttributedString { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + return textFieldView.inputState.inputText + } else { + return NSAttributedString(string: "") + } + } + public var textFieldView: TextFieldComponent.View? { return self.textField.view as? TextFieldComponent.View } @@ -327,11 +347,18 @@ public final class ListComposePollOptionComponent: Component { insets: UIEdgeInsets(top: verticalInset, left: 8.0, bottom: verticalInset, right: 8.0), hideKeyboard: component.inputMode == .emoji, customInputView: nil, + placeholder: component.placeholder, resetText: component.resetText.flatMap { resetText in - return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor) + let result = NSMutableAttributedString(attributedString: resetText.value) + result.addAttributes([ + .font: Font.regular(17.0), + .foregroundColor: component.theme.list.itemPrimaryTextColor + ], range: NSRange(location: 0, length: result.length)) + return result }, isOneLineWhenUnfocused: false, characterLimit: component.characterLimit, + enableInlineAnimations: component.enableInlineAnimations, emptyLineHandling: component.emptyLineHandling, formatMenuAvailability: .none, returnKeyType: .next, diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 8bccf7692b..dbcab9d1ea 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -125,6 +125,8 @@ public final class ContextMenuActionItem { public let id: AnyHashable? public let text: String + public let entities: [MessageTextEntity] + public let enableEntityAnimations: Bool public let textColor: ContextMenuActionItemTextColor public let textFont: ContextMenuActionItemFont public let textLayout: ContextMenuActionItemTextLayout @@ -143,6 +145,8 @@ public final class ContextMenuActionItem { convenience public init( id: AnyHashable? = nil, text: String, + entities: [MessageTextEntity] = [], + enableEntityAnimations: Bool = true, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textFont: ContextMenuActionItemFont = .regular, @@ -161,6 +165,8 @@ public final class ContextMenuActionItem { self.init( id: id, text: text, + entities: entities, + enableEntityAnimations: enableEntityAnimations, textColor: textColor, textLayout: textLayout, textFont: textFont, @@ -185,6 +191,8 @@ public final class ContextMenuActionItem { public init( id: AnyHashable? = nil, text: String, + entities: [MessageTextEntity] = [], + enableEntityAnimations: Bool = true, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textFont: ContextMenuActionItemFont = .regular, @@ -202,6 +210,8 @@ public final class ContextMenuActionItem { ) { self.id = id self.text = text + self.entities = entities + self.enableEntityAnimations = enableEntityAnimations self.textColor = textColor self.textFont = textFont self.textLayout = textLayout @@ -258,6 +268,7 @@ func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelegate { private weak var controller: ContextController? + private let context: AccountContext? private var presentationData: PresentationData private let configuration: ContextController.Configuration @@ -324,6 +335,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega init( controller: ContextController, + context: AccountContext?, presentationData: PresentationData, configuration: ContextController.Configuration, beginDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -333,6 +345,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega attemptTransitionControllerIntoNavigation: @escaping () -> Void ) { self.controller = controller + self.context = context self.presentationData = presentationData self.configuration = configuration self.beginDismiss = beginDismiss @@ -704,7 +717,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega } if let controller = self.controller { - let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration) + let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration, context: self.context) self.contentReady.set(sourceContainer.ready.get()) self.itemsReady.set(.single(true)) self.sourceContainer = sourceContainer @@ -2437,6 +2450,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } } + private let context: AccountContext? private var presentationData: PresentationData private let configuration: ContextController.Configuration @@ -2491,8 +2505,9 @@ public final class ContextController: ViewController, StandalonePresentableContr public var getOverlayViews: (() -> [UIView])? - convenience public init(presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false) { + convenience public init(context: AccountContext? = nil, presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false) { self.init( + context: context, presentationData: presentationData, configuration: ContextController.Configuration( sources: [ContextController.Source( @@ -2511,6 +2526,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } public init( + context: AccountContext? = nil, presentationData: PresentationData, configuration: ContextController.Configuration, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, @@ -2518,6 +2534,7 @@ public final class ContextController: ViewController, StandalonePresentableContr workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false ) { + self.context = context self.presentationData = presentationData self.configuration = configuration self.recognizer = recognizer @@ -2586,7 +2603,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } override public func loadDisplayNode() { - self.displayNode = ContextControllerNode(controller: self, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in + self.displayNode = ContextControllerNode(controller: self, context: self.context, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in self?.dismiss(result: result, completion: nil) }, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in guard let strongSelf = self else { diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 71efef132d..717558faf1 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -15,6 +15,7 @@ import MultiAnimationRenderer import AnimationUI import ComponentFlow import LottieComponent +import TextNodeWithEntities public protocol ContextControllerActionsStackItemNode: ASDisplayNode { var wantsFullWidth: Bool { get } @@ -71,6 +72,7 @@ public final class ContextControllerPreviewReaction { public protocol ContextControllerActionsStackItem: AnyObject { func node( + context: AccountContext?, getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, @@ -94,13 +96,14 @@ public protocol ContextControllerActionsListItemNode: ASDisplayNode { } public final class ContextControllerActionsListActionItemNode: HighlightTrackingButtonNode, ContextControllerActionsListItemNode { + private let context: AccountContext? private let getController: () -> ContextControllerProtocol? private let requestDismiss: (ContextMenuActionResult) -> Void private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void private var item: ContextMenuActionItem private let highlightBackgroundNode: ASDisplayNode - private let titleLabelNode: ImmediateTextNode + private let titleLabelNode: ImmediateTextNodeWithEntities private let subtitleNode: ImmediateTextNode private let iconNode: ASImageNode private let additionalIconNode: ASImageNode @@ -115,11 +118,13 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking private var iconDisposable: Disposable? public init( + context: AccountContext?, getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void, item: ContextMenuActionItem ) { + self.context = context self.getController = getController self.requestDismiss = requestDismiss self.requestUpdateAction = requestUpdateAction @@ -130,7 +135,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking self.highlightBackgroundNode.isUserInteractionEnabled = false self.highlightBackgroundNode.alpha = 0.0 - self.titleLabelNode = ImmediateTextNode() + self.titleLabelNode = ImmediateTextNodeWithEntities() self.titleLabelNode.isAccessibilityElement = false self.titleLabelNode.displaysAsynchronously = false @@ -262,6 +267,17 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0) let subtitleColor = presentationData.theme.contextMenu.secondaryColor + if let context = self.context { + self.titleLabelNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1), + attemptSynchronous: true + ) + } + self.titleLabelNode.visibility = self.item.enableEntityAnimations + var subtitle: NSAttributedString? switch self.item.textLayout { case .singleLine: @@ -296,16 +312,32 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking titleColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4) } - if self.item.parseMarkdown { - let attributedText = parseMarkdownIntoAttributedString( - self.item.text, - attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: titleFont, textColor: titleColor), - bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor), - link: MarkdownAttributeSet(font: titleBoldFont, textColor: presentationData.theme.list.itemAccentColor), - linkAttribute: { value in return ("URL", value) } + if self.item.parseMarkdown || !self.item.entities.isEmpty { + let attributedText: NSAttributedString + if !self.item.entities.isEmpty { + let inputStateText = ChatTextInputStateText(text: self.item.text, attributes: self.item.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in + if case let .CustomEmoji(_, fileId) = entity.type { + return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range) + } + return nil + }) + let result = NSMutableAttributedString(attributedString: inputStateText.attributedText()) + result.addAttributes([ + .font: titleFont, + .foregroundColor: titleColor + ], range: NSRange(location: 0, length: result.length)) + attributedText = result + } else { + attributedText = parseMarkdownIntoAttributedString( + self.item.text, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: titleFont, textColor: titleColor), + bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor), + link: MarkdownAttributeSet(font: titleBoldFont, textColor: presentationData.theme.list.itemAccentColor), + linkAttribute: { value in return ("URL", value) } + ) ) - ) + } self.titleLabelNode.attributedText = attributedText self.titleLabelNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5) self.titleLabelNode.highlightAttributeAction = { attributes in @@ -692,6 +724,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio } } + private let context: AccountContext? private let requestUpdate: (ContainedViewLayoutTransition) -> Void private let getController: () -> ContextControllerProtocol? private let requestDismiss: (ContextMenuActionResult) -> Void @@ -708,11 +741,13 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio } init( + context: AccountContext?, getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, items: [ContextMenuItem] ) { + self.context = context self.requestUpdate = requestUpdate self.getController = getController self.requestDismiss = requestDismiss @@ -724,6 +759,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio case let .action(actionItem): return Item( node: ContextControllerActionsListActionItemNode( + context: context, getController: getController, requestDismiss: requestDismiss, requestUpdateAction: { id, action in @@ -794,6 +830,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio let addedNode = Item( node: ContextControllerActionsListActionItemNode( + context: self.context, getController: self.getController, requestDismiss: self.requestDismiss, requestUpdateAction: { [weak self] id, action in @@ -981,12 +1018,14 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio } public func node( + context: AccountContext?, getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void ) -> ContextControllerActionsStackItemNode { return Node( + context: context, getController: getController, requestDismiss: requestDismiss, requestUpdate: requestUpdate, @@ -1082,6 +1121,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta } func node( + context: AccountContext?, getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, @@ -1246,6 +1286,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { private var tipDisposable: Disposable? init( + context: AccountContext?, getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, @@ -1262,6 +1303,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { self.requestUpdate = requestUpdate self.item = item self.node = item.node( + context: context, getController: getController, requestDismiss: requestDismiss, requestUpdate: requestUpdate, @@ -1396,6 +1438,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { } } + private let context: AccountContext? private let getController: () -> ContextControllerProtocol? private let requestDismiss: (ContextMenuActionResult) -> Void private let requestUpdate: (ContainedViewLayoutTransition) -> Void @@ -1423,10 +1466,12 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { } public init( + context: AccountContext?, getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void ) { + self.context = context self.getController = getController self.requestDismiss = requestDismiss self.requestUpdate = requestUpdate @@ -1534,6 +1579,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { itemContainer.storedScrollingState = currentScrollingState } let itemContainer = ItemContainer( + context: self.context, getController: self.getController, requestDismiss: self.requestDismiss, requestUpdate: self.requestUpdate, diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 188e0dcb82..f4d50da3b1 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -8,6 +8,7 @@ import TelegramCore import SwiftSignalKit import ReactionSelectionNode import UndoUI +import AccountContext private extension ContextControllerTakeViewInfo.ContainingItem { var contentRect: CGRect { @@ -227,6 +228,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo return self._ready.get() } + private let context: AccountContext? private let getController: () -> ContextControllerProtocol? private let requestUpdate: (ContainedViewLayoutTransition) -> Void private let requestUpdateOverlayWantsToBeBelowKeyboard: (ContainedViewLayoutTransition) -> Void @@ -268,6 +270,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private weak var currentUndoController: ViewController? init( + context: AccountContext?, getController: @escaping () -> ContextControllerProtocol?, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateOverlayWantsToBeBelowKeyboard: @escaping (ContainedViewLayoutTransition) -> Void, @@ -275,6 +278,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo requestAnimateOut: @escaping (ContextMenuActionResult, @escaping () -> Void) -> Void, source: ContentSource ) { + self.context = context self.getController = getController self.requestUpdate = requestUpdate self.requestUpdateOverlayWantsToBeBelowKeyboard = requestUpdateOverlayWantsToBeBelowKeyboard @@ -308,6 +312,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.actionsContainerNode = ASDisplayNode() self.actionsStackNode = ContextControllerActionsStackNode( + context: self.context, getController: getController, requestDismiss: { result in requestDismiss(result) @@ -316,6 +321,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo ) self.additionalActionsStackNode = ContextControllerActionsStackNode( + context: self.context, getController: getController, requestDismiss: { result in requestDismiss(result) diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift index c54999cb68..20e3adc346 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -9,6 +9,7 @@ import ComponentFlow import TabSelectorComponent import PlainButtonComponent import ComponentDisplayAdapters +import AccountContext final class ContextSourceContainer: ASDisplayNode { final class Source { @@ -16,6 +17,7 @@ final class ContextSourceContainer: ASDisplayNode { let id: AnyHashable let title: String + let context: AccountContext? let source: ContextContentSource let closeActionTitle: String? let closeAction: (() -> Void)? @@ -42,6 +44,7 @@ final class ContextSourceContainer: ASDisplayNode { controller: ContextController, id: AnyHashable, title: String, + context: AccountContext?, source: ContextContentSource, items: Signal, closeActionTitle: String? = nil, @@ -50,6 +53,7 @@ final class ContextSourceContainer: ASDisplayNode { self.controller = controller self.id = id self.title = title + self.context = context self.source = source self.closeActionTitle = closeActionTitle self.closeAction = closeAction @@ -65,6 +69,7 @@ final class ContextSourceContainer: ASDisplayNode { self.contentReady.set(.single(true)) let presentationNode = ContextControllerExtractedPresentationNode( + context: self.context, getController: { [weak self] in guard let self else { return nil @@ -105,6 +110,7 @@ final class ContextSourceContainer: ASDisplayNode { self.contentReady.set(.single(true)) let presentationNode = ContextControllerExtractedPresentationNode( + context: self.context, getController: { [weak self] in guard let self else { return nil @@ -145,6 +151,7 @@ final class ContextSourceContainer: ASDisplayNode { self.contentReady.set(.single(true)) let presentationNode = ContextControllerExtractedPresentationNode( + context: self.context, getController: { [weak self] in guard let self else { return nil @@ -188,6 +195,7 @@ final class ContextSourceContainer: ASDisplayNode { self.contentReady.set(source.controller.ready.get()) let presentationNode = ContextControllerExtractedPresentationNode( + context: self.context, getController: { [weak self] in guard let self else { return nil @@ -373,7 +381,7 @@ final class ContextSourceContainer: ASDisplayNode { return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false } - init(controller: ContextController, configuration: ContextController.Configuration) { + init(controller: ContextController, configuration: ContextController.Configuration, context: AccountContext?) { self.controller = controller self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false) @@ -389,6 +397,7 @@ final class ContextSourceContainer: ASDisplayNode { controller: controller, id: source.id, title: source.title, + context: context, source: source.source, items: source.items, closeActionTitle: source.closeActionTitle, diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index 201e0a91ce..7400fe91cd 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -80,6 +80,7 @@ final class PeekControllerNode: ViewControllerTracingNode { var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)? self.actionsStackNode = ContextControllerActionsStackNode( + context: nil, getController: { [weak controller] in return controller }, diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 6697c615f7..286813dbf3 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -304,7 +304,7 @@ private struct FolderInviteLinkListControllerState: Equatable { var isSaving: Bool = false } -public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filterId: Int32, title filterTitle: String, allPeerIds: [EnginePeer.Id], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void, presentController parentPresentController: ((ViewController) -> Void)?) -> ViewController { +public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filterId: Int32, title filterTitle: ChatFolderTitle, allPeerIds: [EnginePeer.Id], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void, presentController parentPresentController: ((ViewController) -> Void)?) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? let _ = pushControllerImpl var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? @@ -699,10 +699,11 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: doneButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + //TODO:release let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries( presentationData: presentationData, state: state, - title: filterTitle, + title: filterTitle.text, allPeers: allPeers ), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift index 86ad1635c5..61c141fdd7 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift @@ -28,7 +28,7 @@ public enum ItemListSectionHeaderActivityIndicator { case left case right - fileprivate var hasActivity: Bool { + public var hasActivity: Bool { switch self { case .left, .right: return true diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index af88b9dffa..14e5750201 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -230,9 +230,37 @@ public struct ChatListFilterData: Equatable, Hashable { } } +public struct ChatFolderTitle: Codable, Equatable { + public let text: String + public let entities: [MessageTextEntity] + public var enableAnimations: Bool + + public init(text: String, entities: [MessageTextEntity], enableAnimations: Bool) { + self.text = text + self.entities = entities + self.enableAnimations = enableAnimations + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.text = try container.decode(String.self, forKey: "text") + self.entities = try container.decode([MessageTextEntity].self, forKey: "entities") + self.enableAnimations = try container.decodeIfPresent(Bool.self, forKey: "enableAnimations") ?? true + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.text, forKey: "text") + try container.encode(self.entities, forKey: "entities") + try container.encode(self.enableAnimations, forKey: "enableAnimations") + } +} + public enum ChatListFilter: Codable, Equatable { case allChats - case filter(id: Int32, title: String, emoticon: String?, data: ChatListFilterData) + case filter(id: Int32, title: ChatFolderTitle, emoticon: String?, data: ChatListFilterData) public var id: Int32 { switch self { @@ -251,7 +279,14 @@ public enum ChatListFilter: Codable, Equatable { self = .allChats } else { let id = try container.decode(Int32.self, forKey: "id") - let title = try container.decode(String.self, forKey: "title") + + let title: ChatFolderTitle + if let titleWithEntities = try container.decodeIfPresent(ChatFolderTitle.self, forKey: "titleWithEntities") { + title = titleWithEntities + } else { + title = ChatFolderTitle(text: try container.decode(String.self, forKey: "title"), entities: [], enableAnimations: true) + } + let emoticon = try container.decodeIfPresent(String.self, forKey: "emoticon") let data = ChatListFilterData( @@ -284,7 +319,7 @@ public enum ChatListFilter: Codable, Equatable { try container.encode(type, forKey: "t") try container.encode(id, forKey: "id") - try container.encode(title, forKey: "title") + try container.encode(title, forKey: "titleWithEntities") try container.encodeIfPresent(emoticon, forKey: "emoticon") try container.encode(data.isShared, forKey: "isShared") @@ -309,7 +344,7 @@ extension ChatListFilter { case let .dialogFilter(flags, id, title, emoticon, color, pinnedPeers, includePeers, excludePeers): self = .filter( id: id, - title: title, + title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), emoticon: emoticon, data: ChatListFilterData( isShared: false, @@ -359,7 +394,7 @@ extension ChatListFilter { case let .dialogFilterChatlist(flags, id, title, emoticon, color, pinnedPeers, includePeers): self = .filter( id: id, - title: title, + title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), emoticon: emoticon, data: ChatListFilterData( isShared: true, @@ -400,9 +435,9 @@ extension ChatListFilter { func apiFilter(transaction: Transaction) -> Api.DialogFilter? { switch self { - case .allChats: - return nil - case let .filter(id, title, emoticon, data): + case .allChats: + return nil + case let .filter(id, title, emoticon, data): if data.isShared { var flags: Int32 = 0 if emoticon != nil { @@ -411,7 +446,7 @@ extension ChatListFilter { if data.color != nil { flags |= 1 << 27 } - return .dialogFilterChatlist(flags: flags, id: id, title: title, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + return .dialogFilterChatlist(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in if data.includePeers.pinnedPeers.contains(peerId) { @@ -437,7 +472,7 @@ extension ChatListFilter { if data.color != nil { flags |= 1 << 27 } - return .dialogFilter(flags: flags, id: id, title: title, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + return .dialogFilter(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in if data.includePeers.pinnedPeers.contains(peerId) { @@ -1099,12 +1134,12 @@ func updateChatListFiltersState(transaction: Transaction, _ f: (ChatListFiltersS } public struct ChatListFeaturedFilter: Codable, Equatable { - public var title: String + public var title: ChatFolderTitle public var description: String public var data: ChatListFilterData fileprivate init( - title: String, + title: ChatFolderTitle, description: String, data: ChatListFilterData ) { @@ -1116,7 +1151,11 @@ public struct ChatListFeaturedFilter: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) - self.title = try container.decode(String.self, forKey: "title") + if let title = try container.decodeIfPresent(ChatFolderTitle.self, forKey: "titleWithEntities") { + self.title = title + } else { + self.title = ChatFolderTitle(text: try container.decode(String.self, forKey: "title"), entities: [], enableAnimations: true) + } self.description = try container.decode(String.self, forKey: "description") self.data = ChatListFilterData( isShared: false, @@ -1137,7 +1176,7 @@ public struct ChatListFeaturedFilter: Codable, Equatable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - try container.encode(self.title, forKey: "title") + try container.encode(self.title, forKey: "titleWithEntities") try container.encode(self.description, forKey: "description") try container.encode(self.data.categories.rawValue, forKey: "categories") try container.encode((self.data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted") diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 31312c3b09..cb18ee9c3b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -244,14 +244,14 @@ public enum CheckChatFolderLinkError { public final class ChatFolderLinkContents { public let localFilterId: Int32? - public let title: String? + public let title: ChatFolderTitle? public let peers: [EnginePeer] public let alreadyMemberPeerIds: Set public let memberCounts: [EnginePeer.Id: Int] public init( localFilterId: Int32?, - title: String?, + title: ChatFolderTitle?, peers: [EnginePeer], alreadyMemberPeerIds: Set, memberCounts: [EnginePeer.Id: Int] @@ -301,7 +301,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Signal Signal { - /*#if DEBUG - if "".isEmpty { - return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in - return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) - } - |> castError(JoinChatFolderLinkError.self) - |> mapToSignal { appConfiguration, isPremium -> Signal in - let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) - let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) - - if isPremium { - return .fail(.tooManyChannelsInAccount(limit: userPremiumLimits.maxFolderChatsCount, premiumLimit: userPremiumLimits.maxFolderChatsCount)) - } else { - return .fail(.tooManyChannelsInAccount(limit: userDefaultLimits.maxFolderChatsCount, premiumLimit: userPremiumLimits.maxFolderChatsCount)) - } - } - } - #endif*/ - return account.postbox.transaction { transaction -> ([Api.InputPeer], Int) in var newChatCount = 0 for peerId in peerIds { @@ -522,7 +503,7 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi public final class ChatFolderUpdates: Equatable { public let folderId: Int32 - fileprivate let title: String + fileprivate let title: ChatFolderTitle fileprivate let missingPeers: [EnginePeer] fileprivate let memberCounts: [EnginePeer.Id: Int] @@ -536,7 +517,7 @@ public final class ChatFolderUpdates: Equatable { fileprivate init( folderId: Int32, - title: String, + title: ChatFolderTitle, missingPeers: [EnginePeer], memberCounts: [EnginePeer.Id: Int] ) { @@ -647,7 +628,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) -> Signal { struct InternalData: Equatable { - var title: String + var title: ChatFolderTitle var peerIds: [EnginePeer.Id] var memberCounts: [EnginePeer.Id: Int] } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 6dbf91c823..11ab8aac91 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -100,7 +100,7 @@ public extension TelegramEngine { case same case archived case unarchived - case folder(id: Int32, title: String) + case folder(id: Int32, title: ChatFolderTitle) } final class Peers { diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift index 68da9ab767..772bff6d42 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift @@ -6,6 +6,7 @@ import ComponentFlow import AccountContext import MultilineTextComponent import TelegramPresentationData +import TelegramCore final class BadgeComponent: Component { let fillColor: UIColor @@ -85,13 +86,13 @@ final class BadgeComponent: Component { final class ChatFolderLinkHeaderComponent: Component { let theme: PresentationTheme let strings: PresentationStrings - let title: String + let title: ChatFolderTitle let badge: String? init( theme: PresentationTheme, strings: PresentationStrings, - title: String, + title: ChatFolderTitle, badge: String? ) { self.theme = theme @@ -212,9 +213,10 @@ final class ChatFolderLinkHeaderComponent: Component { } contentWidth += spacing + //TODO:release let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(Text(text: component.title, font: Font.semibold(17.0), color: component.theme.list.itemAccentColor)), + component: AnyComponent(Text(text: component.title.text, font: Font.semibold(17.0), color: component.theme.list.itemAccentColor)), environment: {}, containerSize: CGSize(width: 200.0, height: 100.0) ) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 0de12c1e0b..540ec1764a 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -439,7 +439,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { component: AnyComponent(ChatFolderLinkHeaderComponent( theme: environment.theme, strings: environment.strings, - title: component.linkContents?.title ?? "Folder", + title: component.linkContents?.title ?? ChatFolderTitle(text: "Folder", entities: [], enableAnimations: true), badge: topBadge )), environment: {}, @@ -469,7 +469,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { text = environment.strings.FolderLinkPreview_TextAddFolder } else { let chatCountString: String = environment.strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount)) - text = environment.strings.FolderLinkPreview_TextAddChats(chatCountString, linkContents.title ?? "").string + //TODO:release + text = environment.strings.FolderLinkPreview_TextAddChats(chatCountString, linkContents.title?.text ?? "").string } } else { text = " " @@ -981,7 +982,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(self.selectedItems))) disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId])) - let folderTitle = linkContents.title ?? "" + //TODO:release + let folderTitle = linkContents.title?.text ?? "" let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) @@ -1110,7 +1112,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } if isUpdates { - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitle(result.title).string, text: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + //TODO:release + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitle(result.title.text).string, text: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } else if result.newChatCount != 0 { let animationBackgroundColor: UIColor if presentationData.theme.overallDarkAppearance { @@ -1118,7 +1121,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } else { animationBackgroundColor = UIColor(rgb: 0x474747) } - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title).string, text: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + //TODO:release + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title.text).string, text: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } else { let animationBackgroundColor: UIColor if presentationData.theme.overallDarkAppearance { @@ -1126,7 +1130,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } else { animationBackgroundColor = UIColor(rgb: 0x474747) } - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title).string, text: "", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + //TODO:release + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title.text).string, text: "", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } }) } @@ -1305,6 +1310,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { }) let navigationController = controller.navigationController + //TODO:release controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) })) diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 84a6c22509..b3b89b68ef 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -231,20 +231,18 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { let emoji: ChatTextInputTextCustomEmojiAttribute let cache: AnimationCache let renderer: MultiAnimationRenderer - let unique: Bool let placeholderColor: UIColor let loopCount: Int? let pointSize: CGSize let pixelSize: CGSize - init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool, placeholderColor: UIColor, loopCount: Int?, pointSize: CGSize, pixelSize: CGSize) { + init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, loopCount: Int?, pointSize: CGSize, pixelSize: CGSize) { self.context = context self.userLocation = userLocation self.emoji = emoji self.cache = cache self.renderer = renderer - self.unique = unique self.placeholderColor = placeholderColor self.loopCount = loopCount self.pointSize = pointSize @@ -373,6 +371,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private var currentLoopCount: Int = 0 + public var isUnique: Bool = false + private var isInHierarchyValue: Bool = false public var isVisibleForAnimations: Bool = false { didSet { @@ -440,13 +440,14 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { public init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) { let scale = min(2.0, UIScreenScale) + self.isUnique = unique + self.arguments = Arguments( context: context, userLocation: userLocation, emoji: emoji, cache: cache, renderer: renderer, - unique: unique, placeholderColor: placeholderColor, loopCount: loopCount, pointSize: pointSize, @@ -670,7 +671,6 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { if attemptSynchronousLoad { if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize) { - } self.loadAnimation() @@ -694,19 +694,27 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } let keyframeOnly = arguments.pixelSize.width >= 120.0 - self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: name, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: keyframeOnly, customColor: nil)) + self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: name, unique: self.isUnique, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: keyframeOnly, customColor: nil)) } private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { - guard let arguments = self.arguments else { - return - } - if self.file?.fileId == file.fileId { return } self.file = file + self.updateFile(attemptSynchronousLoad: attemptSynchronousLoad) + } + + private func updateFile(attemptSynchronousLoad: Bool) { + guard let arguments = self.arguments else { + return + } + guard let file = self.file else { + return + } + + self.loadDisposable?.dispose() if attemptSynchronousLoad { if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) { @@ -757,6 +765,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } + public func reloadAnimation() { + self.updateFile(attemptSynchronousLoad: false) + } + private func loadAnimation() { guard let arguments = self.arguments else { return @@ -768,13 +780,15 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { let isTemplate = file.isCustomTemplateEmoji + self.disposable?.dispose() + let context = arguments.context if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji { let keyframeOnly = arguments.pixelSize.width >= 120.0 - self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil)) + self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: self.isUnique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil)) } else { - self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: { options in + self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: self.isUnique, size: arguments.pixelSize, fetch: { options in let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in guard result.complete else { return @@ -846,6 +860,13 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { public final class EmojiTextAttachmentView: UIView { public let contentLayer: InlineStickerItemLayer + public var isUnique: Bool = false { + didSet { + if self.isActive != oldValue { + self.contentLayer.isUnique = self.isUnique + } + } + } public var isActive: Bool = true { didSet { if self.isActive != oldValue { @@ -867,8 +888,8 @@ public final class EmojiTextAttachmentView: UIView { ) } - public init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { - self.contentLayer = InlineStickerItemLayer(context: context, userLocation: userLocation, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: pointSize) + public init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize) { + self.contentLayer = InlineStickerItemLayer(context: context, userLocation: userLocation, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, unique: unique, placeholderColor: placeholderColor, pointSize: pointSize) super.init(frame: CGRect()) @@ -889,6 +910,10 @@ public final class EmojiTextAttachmentView: UIView { self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height)) } + + public func resetToFirstFrame() { + self.contentLayer.reloadAnimation() + } } public final class CustomEmojiContainerView: UIView { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift index d14432ef69..51729f2efc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift @@ -79,7 +79,7 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu f(.dismissWithoutContent) } }) - let actionNode = ContextControllerActionsListActionItemNode(getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action) + let actionNode = ContextControllerActionsListActionItemNode(context: nil, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action) actionNodes.append(actionNode) if actionNodes.count != item.packs.count { let separatorNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index ea74588420..3bb37cc827 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift @@ -190,7 +190,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon if params.hasFilters { self._ready.set(.never()) - self.tabContainerNode = ChatListFilterTabContainerNode() + self.tabContainerNode = ChatListFilterTabContainerNode(context: self.context) self.reloadFilters() self.peerSelectionNode.mainContainerNode?.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in diff --git a/submodules/TelegramUI/Components/TextFieldComponent/BUILD b/submodules/TelegramUI/Components/TextFieldComponent/BUILD index 045b43ad01..4d16bc5b8e 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/BUILD +++ b/submodules/TelegramUI/Components/TextFieldComponent/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", "//submodules/TextInputMenu", "//submodules/ObjCRuntimeUtils", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 35c5a4e6c3..62c09121a5 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -16,6 +16,7 @@ import ImageTransparency import ChatInputTextNode import TextInputMenu import ObjCRuntimeUtils +import MultilineTextComponent public final class EmptyInputView: UIView, UIInputViewAudioFeedback { public var enableInputClicksWhenVisible: Bool { @@ -128,10 +129,12 @@ public final class TextFieldComponent: Component { public let insets: UIEdgeInsets public let hideKeyboard: Bool public let customInputView: UIView? + public let placeholder: NSAttributedString? public let resetText: NSAttributedString? public let assumeIsEditing: Bool public let isOneLineWhenUnfocused: Bool public let characterLimit: Int? + public let enableInlineAnimations: Bool public let emptyLineHandling: EmptyLineHandling public let formatMenuAvailability: FormatMenuAvailability public let returnKeyType: UIReturnKeyType @@ -152,10 +155,12 @@ public final class TextFieldComponent: Component { insets: UIEdgeInsets, hideKeyboard: Bool, customInputView: UIView?, + placeholder: NSAttributedString? = nil, resetText: NSAttributedString?, assumeIsEditing: Bool = false, isOneLineWhenUnfocused: Bool, characterLimit: Int? = nil, + enableInlineAnimations: Bool = true, emptyLineHandling: EmptyLineHandling = .allowed, formatMenuAvailability: FormatMenuAvailability, returnKeyType: UIReturnKeyType = .default, @@ -175,10 +180,12 @@ public final class TextFieldComponent: Component { self.insets = insets self.hideKeyboard = hideKeyboard self.customInputView = customInputView + self.placeholder = placeholder self.resetText = resetText self.assumeIsEditing = assumeIsEditing self.isOneLineWhenUnfocused = isOneLineWhenUnfocused self.characterLimit = characterLimit + self.enableInlineAnimations = enableInlineAnimations self.emptyLineHandling = emptyLineHandling self.formatMenuAvailability = formatMenuAvailability self.returnKeyType = returnKeyType @@ -220,6 +227,9 @@ public final class TextFieldComponent: Component { if lhs.customInputView !== rhs.customInputView { return false } + if lhs.placeholder != rhs.placeholder { + return false + } if lhs.resetText != rhs.resetText { return false } @@ -232,6 +242,9 @@ public final class TextFieldComponent: Component { if lhs.characterLimit != rhs.characterLimit { return false } + if lhs.enableInlineAnimations != rhs.enableInlineAnimations { + return false + } if lhs.emptyLineHandling != rhs.emptyLineHandling { return false } @@ -261,6 +274,7 @@ public final class TextFieldComponent: Component { } public final class View: UIView, UIScrollViewDelegate, ChatInputTextNodeDelegate { + private var placeholder: ComponentView? private let textView: ChatInputTextView private let inputMenu: TextInputMenu @@ -1164,6 +1178,18 @@ public final class TextFieldComponent: Component { } customEmojiContainerView.update(fontSize: component.fontSize, textColor: component.textColor, emojiRects: customEmojiRects) + + for (_, emojiView) in customEmojiContainerView.emojiLayers { + if let emojiView = emojiView as? EmojiTextAttachmentView { + if emojiView.isActive != component.enableInlineAnimations { + emojiView.isUnique = !component.enableInlineAnimations + emojiView.isActive = component.enableInlineAnimations + if !emojiView.isActive { + emojiView.resetToFirstFrame() + } + } + } + } } else if let customEmojiContainerView = self.customEmojiContainerView { customEmojiContainerView.removeFromSuperview() self.customEmojiContainerView = nil @@ -1341,6 +1367,40 @@ public final class TextFieldComponent: Component { self.textView.updateLayout(size: textFrame.size) self.textView.panGestureRecognizer.isEnabled = isEditing + if let placeholderValue = component.placeholder { + var placeholderTransition = transition + let placeholder: ComponentView + if let current = self.placeholder { + placeholder = current + } else { + placeholderTransition = placeholderTransition.withAnimation(.none) + placeholder = ComponentView() + self.placeholder = placeholder + } + let placeholderSize = placeholder.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(placeholderValue) + )), + environment: {}, + containerSize: textFrame.size + ) + let placeholderFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.minY + floor((textFrame.height - placeholderSize.height) * 0.5) - 1.0), size: placeholderSize) + if let placeholderView = placeholder.view { + if placeholderView.superview == nil { + placeholderView.layer.anchorPoint = CGPoint() + self.insertSubview(placeholderView, belowSubview: self.textView) + } + placeholderTransition.setPosition(view: placeholderView, position: placeholderFrame.origin) + placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) + + placeholderView.isHidden = self.textView.textStorage.length != 0 + } + } else if let placeholder = self.placeholder { + self.placeholder = nil + placeholder.view?.removeFromSuperview() + } + self.updateEmojiSuggestion(transition: .immediate) if refreshScrolling { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index e9789487ef..b65c8f6967 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2334,8 +2334,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto swipeText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress, []) releaseText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction, []) case let .folder(_, title): - swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title)._tuple - releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title)._tuple + //TODO:release + swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title.text)._tuple + releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title.text)._tuple } if expandProgress < 0.1 { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 2c748bf009..31d8a65a08 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -133,7 +133,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { var chatListFilter: ChatListFilter? if chatSelection.onlyUsers { - chatListFilter = .filter(id: Int32.max, title: "", emoticon: nil, data: ChatListFilterData( + chatListFilter = .filter(id: Int32.max, title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), emoticon: nil, data: ChatListFilterData( isShared: false, hasSharedLinks: false, categories: [.contacts, .nonContacts], @@ -153,7 +153,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { categories.remove(.bots) } - chatListFilter = .filter(id: Int32.max, title: "", emoticon: nil, data: ChatListFilterData( + chatListFilter = .filter(id: Int32.max, title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), emoticon: nil, data: ChatListFilterData( isShared: false, hasSharedLinks: false, categories: categories,