From 4bed1703a2d9ac08533257bd3837251889d39361 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 25 Dec 2024 00:17:19 +0800 Subject: [PATCH] Emoji in chat folders --- .../Telegram-iOS/en.lproj/Localizable.strings | 14 ++ .../Sources/ChatController.swift | 51 ++++- submodules/ChatListUI/BUILD | 1 + .../ChatListUI/Sources/ChatContextMenus.swift | 11 +- .../Sources/ChatListController.swift | 66 +++--- .../ChatListFilterPresetController.swift | 68 +++--- .../ChatListFilterPresetListController.swift | 23 +- .../ChatListFilterPresetListItem.swift | 54 +++-- .../ChatListFilterTabContainerNode.swift | 4 + .../ChatListFilterTagSectionHeaderItem.swift | 6 +- .../Sources/ChatListSearchListPaneNode.swift | 20 +- .../ItemListFilterTitleInputItem.swift | 4 + .../Sources/Node/ChatListItem.swift | 55 ++++- .../Sources/Node/ChatListNode.swift | 38 ++-- .../MultilineTextWithEntitiesComponent.swift | 22 +- .../ListComposePollOptionComponent.swift | 14 +- .../Sources/ContactListNode.swift | 12 +- .../Sources/ContactsController.swift | 2 +- .../InviteContactsControllerNode.swift | 2 +- submodules/ContactsPeerItem/BUILD | 2 + .../Sources/ContactsPeerItem.swift | 69 ++++-- .../ContextControllerActionsStackNode.swift | 2 +- .../FolderInviteLinkListController.swift | 35 +++- .../Sources/InviteLinkHeaderItem.swift | 53 +++-- .../Sources/InviteLinkListController.swift | 4 +- .../Sources/InviteRequestsController.swift | 2 +- .../Sources/PaymentMethodListScreen.swift | 2 +- .../ChannelMembersSearchContainerNode.swift | 2 +- .../ChannelMembersSearchControllerNode.swift | 2 +- .../Sources/DeleteAccountDataController.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 8 +- submodules/TelegramApi/Sources/Api15.swift | 18 +- submodules/TelegramApi/Sources/Api30.swift | 12 +- submodules/TelegramApi/Sources/Api5.swift | 24 ++- .../ApiUtils/TelegramMediaAction.swift | 2 +- .../Peers/ChatListFiltering.swift | 31 ++- .../TelegramEngine/Peers/Communities.swift | 14 +- .../Chat/ChatOverscrollControl/BUILD | 1 + .../Sources/ChatOverscrollControl.swift | 38 +++- .../ChatFolderLinkPreviewScreen/BUILD | 1 + .../ChatFolderLinkHeaderComponent.swift | 14 +- .../Sources/ChatFolderLinkPreviewScreen.swift | 106 +++++++--- .../ChatTitleView/Sources/ChatTitleView.swift | 8 +- .../Sources/EmojiTextAttachmentView.swift | 2 + .../Sources/PeerInfoChatListPaneNode.swift | 2 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 22 +- .../Sources/OldChannelsController.swift | 2 +- .../Sources/OldChannelsSearch.swift | 2 +- .../Sources/PeerSelectionLoadingView.swift | 4 +- .../Sources/PeerSelectionScreen.swift | 2 +- .../Sources/TextNodeWithEntities.swift | 83 ++++++-- .../TelegramUI/Sources/ChatController.swift | 36 ++-- .../Sources/ChatControllerAdminBanUsers.swift | 4 +- .../ChatControllerOpenCalendarSearch.swift | 2 +- .../Sources/ChatHistoryListNode.swift | 55 +++-- ...annelMemberCategoriesContextsManager.swift | 4 +- .../Sources/ChatTextInputAttributes.swift | 7 +- .../Sources/UndoOverlayController.swift | 7 +- .../Sources/UndoOverlayControllerNode.swift | 198 +++++++++++++++--- 59 files changed, 962 insertions(+), 389 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 406c5d0d9a..5c14174c42 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13521,3 +13521,17 @@ Sorry for the inconvenience."; "PeerInfo.VerificationInfo.Channel" = "This channel is verified as official by the representatives of Telegram."; "PeerInfo.VerificationInfo.Group" = "This group is verified as official by the representatives of Telegram."; "PeerInfo.VerificationInfo.URL" = "https://telegram.org/verify"; + +"ChatList.ToastFolderMutedV2" = "All chats in {folder} are now muted"; +"ChatList.ToastFolderUnmutedV2" = "All chats in {folder} are now unmuted"; +"ChatList.AddedToFolderTooltipV2" = "{chat} has been added to folder {folder}"; +"ChatList.RemovedFromFolderTooltipV2" = "{chat} has been removed from folder {folder}"; + +"FolderLinkScreen.TitleDescriptionDeselectedV2" = "Anyone with this link can add {folder} folder and the chats selected below."; +"FolderLinkScreen.TitleDescriptionSelectedV2" = "Anyone with this link can add {folder} folder and {chats} selected below."; +"Chat.NextChannelFolderSwipeProgressV2" = "Swipe up to go to the {folder} folder"; +"Chat.NextChannelFolderSwipeActionV2" = "Release to go to the {folder} folder"; +"FolderLinkPreview.TextAddChatsV2" = "Do you want to add {chats} to the\nfolder {folder}?"; +"FolderLinkPreview.ToastLeftTitleV2" = "Folder {folder} deleted"; +"FolderLinkPreview.ToastChatsAddedTitleV2" = "Folder {folder} Updated"; +"FolderLinkPreview.ToastFolderAddedTitleV2" = "Folder {folder} Added"; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index b03a2b51e5..b44d75c0be 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -413,7 +413,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case monospace case textMention(EnginePeer.Id) case textUrl(String) - case customEmoji(stickerPack: StickerPackReference?, fileId: Int64) + case customEmoji(stickerPack: StickerPackReference?, fileId: Int64, enableAnimation: Bool) case strikethrough case underline case spoiler @@ -440,7 +440,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case 5: let stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s") let fileId = try container.decode(Int64.self, forKey: "f") - self = .customEmoji(stickerPack: stickerPack, fileId: fileId) + let enableAnimation = try container.decodeIfPresent(Bool.self, forKey: "ea") ?? true + self = .customEmoji(stickerPack: stickerPack, fileId: fileId, enableAnimation: enableAnimation) case 6: self = .strikethrough case 7: @@ -474,10 +475,11 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case let .textUrl(url): try container.encode(4 as Int32, forKey: "t") try container.encode(url, forKey: "url") - case let .customEmoji(stickerPack, fileId): + case let .customEmoji(stickerPack, fileId, enableAnimation): try container.encode(5 as Int32, forKey: "t") try container.encodeIfPresent(stickerPack, forKey: "s") try container.encode(fileId, forKey: "f") + try container.encode(enableAnimation, forKey: "ea") case .strikethrough: try container.encode(6 as Int32, forKey: "t") case .underline: @@ -560,7 +562,7 @@ public struct ChatTextInputStateText: Codable, Equatable { } else if key == ChatTextInputAttributes.textUrl, let value = value as? ChatTextInputTextUrlAttribute { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { - parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId), range: range.location ..< (range.location + range.length))) + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId, enableAnimation: value.enableAnimation), range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.strikethrough { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .strikethrough, range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.underline { @@ -618,8 +620,8 @@ public struct ChatTextInputStateText: Codable, Equatable { result.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: id), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case let .textUrl(url): result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) - case let .customEmoji(_, fileId): - result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case let .customEmoji(_, fileId, enableAnimation): + result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil, enableAnimation: enableAnimation), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case .strikethrough: result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case .underline: @@ -1204,3 +1206,40 @@ public protocol ChatHistoryListNode: ListView { var contentPositionChanged: (ListViewVisibleContentOffset) -> Void { get set } } + +public 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, enableAnimation: self.enableAnimations), range: entity.range) + } + return nil + }) + return inputStateText.attributedText() + } + + func attributedString(attributes: [NSAttributedString.Key: Any]) -> NSAttributedString { + let result = NSMutableAttributedString(attributedString: self.rawAttributedString) + result.addAttributes(attributes, range: NSRange(location: 0, length: result.length)) + return result + } + + 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 + } +} diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index b26a832fbb..3d020f0fea 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -103,6 +103,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/TextFieldComponent", diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 9b64ab804e..045a86eabc 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -223,8 +223,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } |> deliverOnMainQueue).startStandalone(completed: { c?.dismiss(completion: { - //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 + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) @@ -274,8 +273,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } let filterType = chatListFilterType(data) - //TODO:release - updatedItems.append(.action(ContextMenuActionItem(text: title.text, icon: { theme in + updatedItems.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, icon: { theme in let imageName: String switch filterType { case .generic: @@ -339,8 +337,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } return filters }).startStandalone() - //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 + chatListController?.present(UndoOverlayController( presentationData: presentationData, content: .chatAddedToFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) @@ -348,7 +345,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } } - c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) + c?.setItems(.single(ContextController.Items(content: .list(updatedItems), context: context)), minHeight: nil, animated: true) }))) items.append(.separator) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 68cc968256..0dc0f80c7a 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -51,6 +51,7 @@ import PeerInfoStoryGridScreen import ArchiveInfoScreen import BirthdayPickerScreen import OldChannelsController +import TextFormat private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController @@ -1453,7 +1454,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) dismissPreviewingImpl = { [weak contextController] in @@ -1526,7 +1527,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -1811,28 +1812,42 @@ 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.text).string - overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ + let text = NSMutableAttributedString(string: strongSelf.presentationData.strings.ChatList_ToastFolderMutedV2) + let folderNameRange = (text.string as NSString).range(of: "{folder}") + if folderNameRange.location != NSNotFound { + text.replaceCharacters(in: folderNameRange, with: "") + text.insert(title.attributedString(attributes: [ + ChatTextInputAttributes.bold: true + ]), at: folderNameRange.location) + } + + overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universalWithEntities(context: strongSelf.context, animation: "anim_profilemute", scale: 0.075, colors: [ "Middle.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) + ], title: nil, text: text, animateEntities: title.enableAnimations, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) } else { - //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: [ + let text = NSMutableAttributedString(string: strongSelf.presentationData.strings.ChatList_ToastFolderUnmutedV2) + let folderNameRange = (text.string as NSString).range(of: "{folder}") + if folderNameRange.location != NSNotFound { + text.replaceCharacters(in: folderNameRange, with: "") + text.insert(title.attributedString(attributes: [ + ChatTextInputAttributes.bold: true + ]), at: folderNameRange.location) + } + + overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universalWithEntities(context: strongSelf.context, animation: "anim_profileunmute", scale: 0.075, colors: [ "Middle.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) + ], title: nil, text: text, animateEntities: title.enableAnimations, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) } strongSelf.present(overlayController, in: .current) }) @@ -4736,7 +4751,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text = strongSelf.presentationData.strings.ChatList_DeletedThreads(Int32(threadIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -4842,7 +4857,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -4914,7 +4929,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -5274,7 +5289,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true }) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: strongSelf.presentationData.strings.Undo_ChatCleared, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: strongSelf.presentationData.strings.Undo_ChatCleared), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -5496,7 +5511,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let statusText = self.presentationData.strings.Undo_DeletedTopic - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let self else { return false } @@ -5793,7 +5808,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true }) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let strongSelf = self else { return false } @@ -5933,7 +5948,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive) } } - //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 { @@ -6347,9 +6361,9 @@ private final class ChatListLocationContext { let peerView = Promise() peerView.set(context.account.viewTracker.peerView(peerId)) - var onlineMemberCount: Signal = .single(nil) + var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) - let recentOnlineSignal: Signal = peerView.get() + let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView.get() |> map { view -> Bool? in if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { if case .broadcast = peer.info { @@ -6364,17 +6378,21 @@ private final class ChatListLocationContext { } } |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in + |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in if let isLarge = isLarge { if isLarge { return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (nil, value) + } } else { return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (value.total, value.recent) + } } } else { - return .single(nil) + return .single((nil, nil)) } } onlineMemberCount = recentOnlineSignal @@ -6783,7 +6801,7 @@ private final class ChatListLocationContext { private func updateForum( peerId: EnginePeer.Id, peerView: PeerView, - onlineMemberCount: Int32?, + onlineMemberCount: (total: Int32?, recent: Int32?), stateAndFilterId: (state: ChatListNodeState, filterId: Int32?), presentationData: PresentationData ) { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index b2b18055d9..5004a63bea 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -35,6 +35,7 @@ private final class ChatListFilterPresetControllerArguments { let context: AccountContext let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void let updateName: (ChatFolderTitle) -> Void + let toggleNameInputMode: () -> Void let toggleNameAnimations: () -> Void let openAddIncludePeer: () -> Void let openAddExcludePeer: () -> Void @@ -58,6 +59,7 @@ private final class ChatListFilterPresetControllerArguments { context: AccountContext, updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void, updateName: @escaping (ChatFolderTitle) -> Void, + toggleNameInputMode: @escaping () -> Void, toggleNameAnimations: @escaping () -> Void, openAddIncludePeer: @escaping () -> Void, openAddExcludePeer: @escaping () -> Void, @@ -80,6 +82,7 @@ private final class ChatListFilterPresetControllerArguments { self.context = context self.updateState = updateState self.updateName = updateName + self.toggleNameInputMode = toggleNameInputMode self.toggleNameAnimations = toggleNameAnimations self.openAddIncludePeer = openAddIncludePeer self.openAddExcludePeer = openAddExcludePeer @@ -228,7 +231,7 @@ private enum ChatListFilterRevealedItemId: Equatable { private enum ChatListFilterPresetEntry: ItemListNodeEntry { case screenHeader - case nameHeader(title: String, enableAnimations: Bool) + case nameHeader(title: String, enableAnimations: Bool?) case name(placeholder: String, value: NSAttributedString, inputMode: ListComposePollOptionComponent.InputMode?, enableAnimations: Bool) case includePeersHeader(String) case addIncludePeer(title: String) @@ -376,7 +379,11 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section) case let .nameHeader(title, enableAnimations): //TODO:localize - return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: enableAnimations ? "Disable Animations" : "Enable Animations", action: { + var actionText: String? + if let enableAnimations { + actionText = enableAnimations ? "Disable Animations" : "Enable Animations" + } + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: actionText, action: { arguments.toggleNameAnimations() }, sectionId: self.section) case let .name(placeholder, value, inputMode, enableAnimations): @@ -393,15 +400,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { 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 - } + arguments.toggleNameInputMode() } ) case .includePeersHeader(let text), .excludePeersHeader(let text): @@ -545,37 +544,6 @@ 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: ChatFolderTitle var changedName: Bool @@ -624,7 +592,7 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres entries.append(.screenHeader) } - entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.enableAnimations)) + entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.entities.isEmpty ? nil : 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)) @@ -1584,6 +1552,18 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi } } }, + toggleNameInputMode: { + updateState { current in + var state = current + if state.nameInputMode == .emoji { + state.nameInputMode = .keyboard + } else { + state.nameInputMode = .emoji + } + return state + } + focusOnNameImpl?() + }, toggleNameAnimations: { updateState { current in var name = current.name @@ -2145,7 +2125,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi return } controller.forEachItemNode { itemNode in - if let itemNode = itemNode as? ItemListSingleLineInputItemNode { + if let itemNode = itemNode as? ItemListFilterTitleInputItemNode { itemNode.focus() } } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index e1105affa4..8b0636d941 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -16,7 +16,7 @@ import ChatFolderLinkPreviewScreen private final class ChatListFilterPresetListControllerArguments { let context: AccountContext - let addSuggestedPressed: (String, ChatListFilterData) -> Void + let addSuggestedPressed: (ChatFolderTitle, ChatListFilterData) -> Void let openPreset: (ChatListFilter) -> Void let addNew: () -> Void let setItemWithRevealedOptions: (Int32?, Int32?) -> Void @@ -24,7 +24,7 @@ private final class ChatListFilterPresetListControllerArguments { let updateDisplayTags: (Bool) -> Void let updateDisplayTagsLocked: () -> Void - init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) { + init(context: AccountContext, addSuggestedPressed: @escaping (ChatFolderTitle, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) { self.context = context self.addSuggestedPressed = addSuggestedPressed self.openPreset = openPreset @@ -92,10 +92,10 @@ private struct PresetIndex: Equatable { private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case screenHeader(String) case suggestedListHeader(String) - case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData) + case suggestedPreset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilterData) case suggestedAddCustom(String) case listHeader(String) - case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool) + case preset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool) case addItem(text: String, isEditing: Bool) case listFooter(String) case displayTags(Bool?) @@ -176,7 +176,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case let .suggestedListHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) case let .suggestedPreset(_, title, label, preset): - return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, title: title, label: label, sectionId: self.section, style: .blocks, installAction: { + return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, title: title.text, label: label, sectionId: self.section, style: .blocks, installAction: { arguments.addSuggestedPressed(title, preset) }, tag: nil) case let .suggestedAddCustom(text): @@ -194,7 +194,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { } } - return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: { + return ChatListFilterPresetListItem(context: arguments.context, presentationData: presentationData, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: { if isDisabled { arguments.addNew() } else { @@ -285,12 +285,11 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present var folderCount = 0 for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { if case .allChats = filter { - entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true)) + entries.append(.preset(index: PresetIndex(value: entries.count), title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true)) } if case let .filter(_, title, _, _) = filter { folderCount += 1 - //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)) + 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)) } } @@ -300,8 +299,7 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present if !filteredSuggestedFilters.isEmpty && actualFilters.count < limits.maxFoldersCount { entries.append(.suggestedListHeader(presentationData.strings.ChatListFolderSettings_RecommendedFoldersSection)) for filter in filteredSuggestedFilters { - //TODO:release - entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title.text, label: filter.description, preset: filter.data)) + entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title, label: filter.description, preset: filter.data)) } if filters.isEmpty { entries.append(.suggestedAddCustom(presentationData.strings.ChatListFolderSettings_RecommendedNewFolder)) @@ -389,8 +387,7 @@ 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) - //TODO:release - filters.append(.filter(id: id, title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), emoticon: nil, data: data)) + filters.append(.filter(id: id, title: title, emoticon: nil, data: data)) return filters } |> deliverOnMainQueue).start(next: { _ in diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 91f2b2acfa..01d1063a17 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -7,6 +7,8 @@ import TelegramCore import TelegramPresentationData import ItemListUI import TelegramUIPreferences +import AccountContext +import TextNodeWithEntities struct ChatListFilterPresetListItemEditing: Equatable { let editable: Bool @@ -15,9 +17,10 @@ struct ChatListFilterPresetListItemEditing: Equatable { } final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { + let context: AccountContext let presentationData: ItemListPresentationData let preset: ChatListFilter - let title: String + let title: ChatFolderTitle let label: String let tagColor: UIColor? let editing: ChatListFilterPresetListItemEditing @@ -31,9 +34,10 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { let remove: () -> Void init( + context: AccountContext, presentationData: ItemListPresentationData, preset: ChatListFilter, - title: String, + title: ChatFolderTitle, label: String, tagColor: UIColor?, editing: ChatListFilterPresetListItemEditing, @@ -46,6 +50,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, remove: @escaping () -> Void ) { + self.context = context self.presentationData = presentationData self.preset = preset self.title = title @@ -124,7 +129,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { return self.containerNode } - private let titleNode: TextNode + private let titleNode: TextNodeWithEntities private let labelNode: TextNode private let arrowNode: ASImageNode private let sharedIconNode: ASImageNode @@ -145,6 +150,15 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { return true } + override var visibility: ListViewItemNodeVisibility { + didSet { + if self.visibility != oldValue { + let enableAnimations = self.item?.title.enableAnimations ?? true + self.titleNode.visibilityRect = (self.visibility == ListViewItemNodeVisibility.none || !enableAnimations) ? CGRect.zero : CGRect.infinite + } + } + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -160,10 +174,11 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false - self.titleNode = TextNode() - self.titleNode.isUserInteractionEnabled = false - self.titleNode.contentMode = .left - self.titleNode.contentsScale = UIScreen.main.scale + self.titleNode = TextNodeWithEntities() + self.titleNode.textNode.isUserInteractionEnabled = false + self.titleNode.textNode.contentMode = .left + self.titleNode.textNode.contentsScale = UIScreen.main.scale + self.titleNode.resetEmojiToFirstFrameAutomatically = true self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false @@ -186,7 +201,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.titleNode) + self.containerNode.addSubnode(self.titleNode.textNode) self.containerNode.addSubnode(self.labelNode) self.containerNode.addSubnode(self.arrowNode) self.containerNode.addSubnode(self.sharedIconNode) @@ -199,7 +214,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { } func asyncLayout() -> (_ item: ChatListFilterPresetListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) @@ -230,7 +245,11 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { } let titleAttributedString = NSMutableAttributedString() - titleAttributedString.append(NSAttributedString(string: item.isAllChats ? item.presentationData.strings.ChatList_FolderAllChats : item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + if item.isAllChats { + titleAttributedString.append(NSAttributedString(string: item.presentationData.strings.ChatList_FolderAllChats, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + } else { + titleAttributedString.append(item.title.attributedString(font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + } var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? @@ -337,9 +356,18 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { } strongSelf.editableControlNode?.isHidden = !item.canBeDeleted - let _ = titleApply() + let _ = titleApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: true + )) let _ = labelApply() + let enableAnimations = item.title.enableAnimations + strongSelf.titleNode.visibilityRect = (strongSelf.visibility == ListViewItemNodeVisibility.none || !enableAnimations) ? CGRect.zero : CGRect.infinite + if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } @@ -385,7 +413,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: 11.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame @@ -542,7 +570,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) + transition.updateFrame(node: self.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.textNode.frame.minY), size: self.titleNode.textNode.bounds.size)) var labelFrame = self.labelNode.frame labelFrame.origin.x = params.width - rightArrowInset - labelFrame.width + revealOffset diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 37f621cdfa..6cea53242d 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -109,11 +109,13 @@ private final class ItemNode: ASDisplayNode { self.titleNode = ImmediateTextNodeWithEntities() self.titleNode.displaysAsynchronously = false self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + self.titleNode.resetEmojiToFirstFrameAutomatically = true 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.titleActiveNode.resetEmojiToFirstFrameAutomatically = true self.shortTitleContainer = ASDisplayNode() @@ -121,12 +123,14 @@ private final class ItemNode: ASDisplayNode { self.shortTitleNode.displaysAsynchronously = false self.shortTitleNode.alpha = 0.0 self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + self.shortTitleNode.resetEmojiToFirstFrameAutomatically = true 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) self.shortTitleActiveNode.alpha = 0.0 + self.shortTitleActiveNode.resetEmojiToFirstFrameAutomatically = true self.badgeContainerNode = ASDisplayNode() diff --git a/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift b/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift index f6cca407a6..93438c9af5 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift @@ -239,7 +239,8 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode { cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, - attemptSynchronous: true + attemptSynchronous: true, + emojiOffset: CGPoint(x: 0.0, y: -1.0) )) let badgeSideInset: CGFloat = 4.0 let badgeBackgroundSize: CGSize @@ -248,7 +249,7 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode { } 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 badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize) let badgeBackgroundLayer: SimpleLayer if let current = strongSelf.badgeBackgroundLayer { @@ -262,6 +263,7 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode { if strongSelf.badgeTextNode !== badgeTextNode { strongSelf.badgeTextNode?.textNode.removeFromSupernode() strongSelf.badgeTextNode = badgeTextNode + badgeTextNode.resetEmojiToFirstFrameAutomatically = true strongSelf.addSubnode(badgeTextNode.textNode) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 50f850e96e..b796b20fa3 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -197,12 +197,12 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } else if case let .user(user) = primaryPeer { let servicePeer = isServicePeer(primaryPeer._asPeer()) if user.flags.contains(.isSupport) && !servicePeer { - status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Bot_GenericSupportStatus), multiline: false, isActive: false, icon: nil) } else if let _ = user.botInfo { if let subscriberCount = user.subscriberCount { - status = .custom(string: strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusBotSubscribers(subscriberCount)), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Bot_GenericBotStatus), multiline: false, isActive: false, icon: nil) } } else if user.id != context.account.peerId && !servicePeer { let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0) @@ -211,19 +211,19 @@ private enum ChatListRecentEntry: Comparable, Identifiable { status = .none } } else if case let .legacyGroup(group) = primaryPeer { - status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil) } else if case let .channel(channel) = primaryPeer { if case .group = channel.info { if let count = peer.subpeerSummary?.count, count > 0 { - status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.GroupInfo_ParticipantCount(Int32(count))), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Group_Status), multiline: false, isActive: false, icon: nil) } } else { if let count = peer.subpeerSummary?.count, count > 0 { - status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusSubscribers(Int32(count))), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Channel_Status), multiline: false, isActive: false, icon: nil) } } } else { @@ -870,9 +870,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var status: ContactsPeerItemStatus = .none if case let .user(user) = primaryPeer, let _ = user.botInfo, !primaryPeer.id.isVerificationCodes { if let subscriberCount = user.subscriberCount { - status = .custom(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount)), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: presentationData.strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: presentationData.strings.Bot_GenericBotStatus), multiline: false, isActive: false, icon: nil) } } diff --git a/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift b/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift index bc91aa74dd..b511eaced3 100644 --- a/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift +++ b/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift @@ -238,6 +238,7 @@ public class ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDele backspaceKeyAction: nil, selection: nil, inputMode: item.inputMode, + alwaysDisplayInputModeSelector: true, toggleInputMode: { [weak self] in guard let self else { return @@ -268,6 +269,9 @@ public class ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDele } public func focus() { + if let textFieldView = self.textField.view as? ListComposePollOptionComponent.View { + textFieldView.activateInput() + } } public func selectAll() { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 3b39041377..9b9934d923 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -28,6 +28,7 @@ import EmojiStatusComponent import AvatarVideoNode import AppBundle import MultilineTextComponent +import MultilineTextWithEntitiesComponent import ShimmerEffect public enum ChatListItemContent { @@ -82,10 +83,10 @@ public enum ChatListItemContent { public struct Tag: Equatable { public var id: Int32 - public var title: String + public var title: ChatFolderTitle public var colorId: Int32 - public init(id: Int32, title: String, colorId: Int32) { + public init(id: Int32, title: ChatFolderTitle, colorId: Int32) { self.id = id self.title = title self.colorId = colorId @@ -269,6 +270,8 @@ private final class ChatListItemTagListComponent: Component { let backgroundView: UIImageView let title = ComponentView() + private var currentTitle: ChatFolderTitle? + override init(frame: CGRect) { self.backgroundView = UIImageView(image: tagBackgroundImage) @@ -281,11 +284,20 @@ private final class ChatListItemTagListComponent: Component { preconditionFailure() } - func update(context: AccountContext, title: String, backgroundColor: UIColor, foregroundColor: UIColor, sizeFactor: CGFloat) -> CGSize { + func update(context: AccountContext, title: ChatFolderTitle, backgroundColor: UIColor, foregroundColor: UIColor, sizeFactor: CGFloat) -> CGSize { + self.currentTitle = title + + let titleValue = ChatFolderTitle(text: title.text.isEmpty ? " " : title.text, entities: title.entities, enableAnimations: title.enableAnimations) let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: title.isEmpty ? " " : title, font: Font.semibold(floor(11.0 * sizeFactor)), textColor: foregroundColor)) + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + placeholderColor: foregroundColor.withMultipliedAlpha(0.1), + text: .plain(titleValue.attributedString(font: Font.semibold(floor(11.0 * sizeFactor)), textColor: foregroundColor)), + manualVisibilityControl: true, + resetAnimationsOnVisibilityChange: true )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) @@ -309,11 +321,30 @@ private final class ChatListItemTagListComponent: Component { return backgroundSize } + + func updateVisibility(_ isVisible: Bool) { + guard let currentTitle = self.currentTitle else { + return + } + if let titleView = self.title.view as? MultilineTextWithEntitiesComponent.View { + titleView.updateVisibility(isVisible && currentTitle.enableAnimations) + } + } } final class View: UIView { private var itemViews: [Int32: ItemView] = [:] + var isVisible: Bool = false { + didSet { + if self.isVisible != oldValue { + for (_, itemView) in self.itemViews { + itemView.updateVisibility(self.isVisible) + } + } + } + } + override init(frame: CGRect) { super.init(frame: frame) } @@ -332,13 +363,13 @@ private final class ChatListItemTagListComponent: Component { } let itemId: Int32 - let itemTitle: String + let itemTitle: ChatFolderTitle let itemBackgroundColor: UIColor let itemForegroundColor: UIColor if validIds.count >= 3 { itemId = Int32.max - itemTitle = "+\(component.tags.count - validIds.count)" + itemTitle = ChatFolderTitle(text: "+\(component.tags.count - validIds.count)", entities: [], enableAnimations: true) itemForegroundColor = component.theme.chatList.dateTextColor itemBackgroundColor = itemForegroundColor.withMultipliedAlpha(0.1) } else { @@ -347,7 +378,7 @@ private final class ChatListItemTagListComponent: Component { let tagColor = PeerNameColor(rawValue: tag.colorId) let resolvedColor = component.context.peerNameColors.getChatFolderTag(tagColor, dark: component.theme.overallDarkAppearance) - itemTitle = tag.title.uppercased() + itemTitle = ChatFolderTitle(text: tag.title.text.uppercased(), entities: tag.title.entities, enableAnimations: tag.title.enableAnimations) itemBackgroundColor = resolvedColor.main.withMultipliedAlpha(0.1) itemForegroundColor = resolvedColor.main } @@ -364,6 +395,7 @@ private final class ChatListItemTagListComponent: Component { let itemSize = itemView.update(context: component.context, title: itemTitle, backgroundColor: itemBackgroundColor, foregroundColor: itemForegroundColor, sizeFactor: component.sizeFactor) let itemFrame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize) itemView.frame = itemFrame + itemView.updateVisibility(self.isVisible) validIds.append(itemId) nextX += itemSize.width @@ -1451,6 +1483,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { ) } self.authorNode.visibilityStatus = self.visibilityStatus + + if let itemTagListView = self.itemTagList?.view as? ChatListItemTagListComponent.View { + itemTagListView.isVisible = self.visibilityStatus + } } } } @@ -4243,13 +4279,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { environment: {}, containerSize: itemTagListFrame.size ) - if let itemTagListView = itemTagList.view { + if let itemTagListView = itemTagList.view as? ChatListItemTagListComponent.View { if itemTagListView.superview == nil { itemTagListView.isUserInteractionEnabled = false strongSelf.mainContentContainerNode.view.addSubview(itemTagListView) } itemTagListTransition.updateFrame(view: itemTagListView, frame: itemTagListFrame) + itemTagListView.isVisible = strongSelf.visibilityStatus && item.context.sharedContext.energyUsageSettings.loopEmoji } } else { if let itemTagList = strongSelf.itemTagList { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 49dc0ee934..3332600d84 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -4276,33 +4276,32 @@ public final class ChatListNode: ListView { } } -private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, autoremoveTimeout: Int32?) -> (String, Bool, Bool, ContactsPeerItemStatus.Icon?)? { +private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, autoremoveTimeout: Int32?) -> (NSAttributedString, Bool, Bool, ContactsPeerItemStatus.Icon?)? { if accountPeerId == peer.id { return nil } if displayAutoremoveTimeout { if let autoremoveTimeout = autoremoveTimeout { - return (strings.ChatList_LabelAutodeleteAfter(timeIntervalString(strings: strings, value: autoremoveTimeout, usage: .afterTime)).string, false, true, .autoremove) + return (NSAttributedString(string: strings.ChatList_LabelAutodeleteAfter(timeIntervalString(strings: strings, value: autoremoveTimeout, usage: .afterTime)).string), false, true, .autoremove) } else { - return (strings.ChatList_LabelAutodeleteDisabled, false, false, .autoremove) + return (NSAttributedString(string: strings.ChatList_LabelAutodeleteDisabled), false, false, .autoremove) } } if let chatListFilters = chatListFilters { - var result = "" + let result = NSMutableAttributedString(string: "") for case let .filter(_, title, _, data) in chatListFilters { let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId) if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { - if !result.isEmpty { - result.append(", ") + if result.length != 0 { + result.append(NSAttributedString(string: ", ")) } - //TODO:release - result.append(title.text) + result.append(title.rawAttributedString) } } - if result.isEmpty { + if result.length == 0 { return nil } else { return (result, true, false, nil) @@ -4313,30 +4312,30 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres return nil } else if case let .user(user) = peer { if user.botInfo != nil || user.flags.contains(.isSupport) { - return (strings.ChatList_PeerTypeBot, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeBot), false, false, nil) } else { if isContact { - return (strings.ChatList_PeerTypeContact, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeContact), false, false, nil) } else { - return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil) } } } else if case .secretChat = peer { if isContact { - return (strings.ChatList_PeerTypeContact, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeContact), false, false, nil) } else { - return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil) } } else if case .legacyGroup = peer { - return (strings.ChatList_PeerTypeGroup, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeGroup), false, false, nil) } else if case let .channel(channel) = peer { if case .group = channel.info { - return (strings.ChatList_PeerTypeGroup, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeGroup), false, false, nil) } else { - return (strings.ChatList_PeerTypeChannel, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeChannel), false, false, nil) } } - return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil) } public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { @@ -4425,10 +4424,9 @@ 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.text, + title: title, colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue )) } diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index e86e95faad..7949ea417e 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -31,6 +31,8 @@ public final class MultilineTextWithEntitiesComponent: Component { public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? public let handleSpoilers: Bool + public let manualVisibilityControl: Bool + public let resetAnimationsOnVisibilityChange: Bool public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -52,6 +54,8 @@ public final class MultilineTextWithEntitiesComponent: Component { textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, handleSpoilers: Bool = false, + manualVisibilityControl: Bool = false, + resetAnimationsOnVisibilityChange: Bool = false, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -73,6 +77,8 @@ public final class MultilineTextWithEntitiesComponent: Component { self.highlightColor = highlightColor self.highlightAction = highlightAction self.handleSpoilers = handleSpoilers + self.manualVisibilityControl = manualVisibilityControl + self.resetAnimationsOnVisibilityChange = resetAnimationsOnVisibilityChange self.tapAction = tapAction self.longTapAction = longTapAction } @@ -105,6 +111,12 @@ public final class MultilineTextWithEntitiesComponent: Component { if lhs.handleSpoilers != rhs.handleSpoilers { return false } + if lhs.manualVisibilityControl != rhs.manualVisibilityControl { + return false + } + if lhs.resetAnimationsOnVisibilityChange != rhs.resetAnimationsOnVisibilityChange { + return false + } if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { return false @@ -151,6 +163,10 @@ public final class MultilineTextWithEntitiesComponent: Component { fatalError("init(coder:) has not been implemented") } + public func updateVisibility(_ isVisible: Bool) { + self.textNode.visibility = isVisible + } + public func update(component: MultilineTextWithEntitiesComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let attributedString: NSAttributedString switch component.text { @@ -176,6 +192,8 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textNode.highlightAttributeAction = component.highlightAction self.textNode.tapAttributeAction = component.tapAction self.textNode.longTapAttributeAction = component.longTapAction + + self.textNode.resetEmojiToFirstFrameAutomatically = component.resetAnimationsOnVisibilityChange if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string { if let snapshotView = self.snapshotContentTree() { @@ -189,7 +207,9 @@ public final class MultilineTextWithEntitiesComponent: Component { } } - self.textNode.visibility = true + if !component.manualVisibilityControl { + self.textNode.visibility = true + } if let context = component.context, let animationCache = component.animationCache, let animationRenderer = component.animationRenderer, let placeholderColor = component.placeholderColor { self.textNode.arguments = TextNodeWithEntities.Arguments( context: context, diff --git a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift index 8e371e3220..6a0986e23d 100644 --- a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift +++ b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift @@ -82,6 +82,7 @@ public final class ListComposePollOptionComponent: Component { public let backspaceKeyAction: (() -> Void)? public let selection: Selection? public let inputMode: InputMode? + public let alwaysDisplayInputModeSelector: Bool public let toggleInputMode: (() -> Void)? public let tag: AnyObject? @@ -100,6 +101,7 @@ public final class ListComposePollOptionComponent: Component { backspaceKeyAction: (() -> Void)?, selection: Selection?, inputMode: InputMode?, + alwaysDisplayInputModeSelector: Bool = false, toggleInputMode: (() -> Void)?, tag: AnyObject? = nil ) { @@ -117,6 +119,7 @@ public final class ListComposePollOptionComponent: Component { self.backspaceKeyAction = backspaceKeyAction self.selection = selection self.inputMode = inputMode + self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector self.toggleInputMode = toggleInputMode self.tag = tag } @@ -158,6 +161,9 @@ public final class ListComposePollOptionComponent: Component { if lhs.inputMode != rhs.inputMode { return false } + if lhs.alwaysDisplayInputModeSelector != rhs.alwaysDisplayInputModeSelector { + return false + } return true } @@ -490,15 +496,17 @@ public final class ListComposePollOptionComponent: Component { ComponentTransition.immediate.setScale(view: modeSelectorView, scale: 0.001) } - if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View { - animationView.playOnce() + if let animationView = modeSelectorView.contentView as? LottieComponent.View { + if playAnimation { + animationView.playOnce() + } } modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center) modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size)) if let externalState = component.externalState { - let displaySelector = externalState.isEditing + let displaySelector = externalState.isEditing || component.alwaysDisplayInputModeSelector alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0) alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001) diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 77a44a2be6..f85944cfa9 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -158,19 +158,19 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if let _ = peer as? TelegramUser { status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat) } else if let group = peer as? TelegramGroup { - status = .custom(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil) } else if let channel = peer as? TelegramChannel { if case .group = channel.info { if let participantCount = participantCount, participantCount != 0 { - status = .custom(string: strings.Conversation_StatusMembers(participantCount), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(participantCount)), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Group_Status), multiline: false, isActive: false, icon: nil) } } else { if let participantCount = participantCount, participantCount != 0 { - status = .custom(string: strings.Conversation_StatusSubscribers(participantCount), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusSubscribers(participantCount)), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Channel_Status), multiline: false, isActive: false, icon: nil) } } } else { @@ -220,7 +220,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { let text: String text = presentationData.strings.ChatList_ArchiveStoryCount(Int32(storyData.count)) - status = .custom(string: text, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: text), multiline: false, isActive: false, icon: nil) } return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 4d28125285..661c3fdedf 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -674,7 +674,7 @@ public class ContactsController: ViewController { let text = self.presentationData.strings.ContactList_DeletedContacts(Int32(peerIds.count)) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let self else { return false } diff --git a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift index 7b74036cc8..77cbcec11a 100644 --- a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift @@ -52,7 +52,7 @@ private enum InviteContactsEntry: Comparable, Identifiable { case let .peer(_, id, contact, count, selection, theme, strings, nameSortOrder, nameDisplayOrder): let status: ContactsPeerItemStatus if count != 0 { - status = .custom(string: strings.Contacts_ImportersCount(count), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Contacts_ImportersCount(count)), multiline: false, isActive: false, icon: nil) } else { status = .none } diff --git a/submodules/ContactsPeerItem/BUILD b/submodules/ContactsPeerItem/BUILD index 921376ec47..e4edd69b3b 100644 --- a/submodules/ContactsPeerItem/BUILD +++ b/submodules/ContactsPeerItem/BUILD @@ -32,6 +32,8 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/MoreButtonNode", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 43847bb2a9..3becbf5f66 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -21,6 +21,8 @@ import AnimationCache import MultiAnimationRenderer import EmojiStatusComponent import MoreButtonNode +import TextFormat +import TextNodeWithEntities public final class ContactItemHighlighting { public var chatLocation: ChatLocation? @@ -39,7 +41,7 @@ public enum ContactsPeerItemStatus { case none case presence(EnginePeer.Presence, PresentationDateTimeFormat) case addressName(String) - case custom(string: String, multiline: Bool, isActive: Bool, icon: Icon?) + case custom(string: NSAttributedString, multiline: Bool, isActive: Bool, icon: Icon?) } public enum ContactsPeerItemSelection: Equatable { @@ -437,7 +439,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var credibilityIconComponent: EmojiStatusComponent? private var verifiedIconView: ComponentHostView? private var verifiedIconComponent: EmojiStatusComponent? - public let statusNode: TextNode + public let statusNode: TextNodeWithEntities private var statusIconNode: ASImageNode? private var badgeBackgroundNode: ASImageNode? private var badgeTextNode: TextNode? @@ -519,6 +521,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { containerSize: avatarIconView.bounds.size ) } + self.statusNode.visibilityRect = self.visibilityStatus == false ? CGRect.zero : CGRect.infinite } } } @@ -554,7 +557,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.avatarNode.isLayerBacked = false self.titleNode = TextNode() - self.statusNode = TextNode() + self.statusNode = TextNodeWithEntities() super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) @@ -575,7 +578,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.avatarNodeContainer.addSubnode(self.avatarNode) self.offsetContainerNode.addSubnode(self.avatarNodeContainer) self.offsetContainerNode.addSubnode(self.titleNode) - self.offsetContainerNode.addSubnode(self.statusNode) + self.offsetContainerNode.addSubnode(self.statusNode.textNode) self.addSubnode(self.maskNode) @@ -708,7 +711,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { public func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool, Bool) -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + let makeStatusLayout = TextNodeWithEntities.asyncLayout(self.statusNode) let currentSelectionNode = self.selectionNode let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) @@ -939,7 +942,24 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } case let .custom(text, multiline, isActive, icon): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) + let statusAttributedStringValue = NSMutableAttributedString(string: text.string) + statusAttributedStringValue.addAttribute(.font, value: statusFont, range: NSRange(location: 0, length: statusAttributedStringValue.length)) + statusAttributedStringValue.addAttribute(.foregroundColor, value: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor, range: NSRange(location: 0, length: statusAttributedStringValue.length)) + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + statusAttributedStringValue.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + statusAttributedStringValue.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + statusAttributedStringValue.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + statusAttributedStringValue.addAttribute(key, value: value, range: range) + } + } + }) + + statusAttributedString = statusAttributedStringValue statusIcon = icon statusIsActive = isActive multilineStatus = multiline @@ -964,7 +984,23 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { switch item.status { case let .custom(text, multiline, isActive, icon): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) + let statusAttributedStringValue = NSMutableAttributedString(string: "") + statusAttributedStringValue.addAttribute(.font, value: statusFont, range: NSRange(location: 0, length: text.length)) + statusAttributedStringValue.addAttribute(.foregroundColor, value: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor, range: NSRange(location: 0, length: text.length)) + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + statusAttributedStringValue.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + statusAttributedStringValue.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + statusAttributedStringValue.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + statusAttributedStringValue.addAttribute(key, value: value, range: range) + } + } + }) + statusAttributedString = statusAttributedStringValue multilineStatus = multiline statusIsActive = isActive statusIcon = icon @@ -1395,17 +1431,24 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.4 - strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0 + strongSelf.statusNode.textNode.alpha = item.enabled ? 1.0 : 1.0 - let _ = statusApply() + strongSelf.statusNode.visibilityRect = strongSelf.visibilityStatus == false ? CGRect.zero : CGRect.infinite + let _ = statusApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: false + )) var statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY - 1.0), size: statusLayout.size) if let statusIconImage { statusFrame.origin.x += statusIconImage.size.width + 1.0 } - let previousStatusFrame = strongSelf.statusNode.frame + let previousStatusFrame = strongSelf.statusNode.textNode.frame - strongSelf.statusNode.frame = statusFrame - transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) + strongSelf.statusNode.textNode.frame = statusFrame + transition.animatePositionAdditive(node: strongSelf.statusNode.textNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) if let statusIconImage { let statusIconNode: ASImageNode @@ -1413,7 +1456,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusIconNode = current } else { statusIconNode = ASImageNode() - strongSelf.statusNode.addSubnode(statusIconNode) + strongSelf.statusNode.textNode.addSubnode(statusIconNode) } statusIconNode.image = statusIconImage statusIconNode.frame = CGRect(origin: CGPoint(x: -statusIconImage.size.width - 1.0, y: floor((statusFrame.height - statusIconImage.size.height) / 2.0) + 1.0), size: statusIconImage.size) diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 717558faf1..2862676fbd 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -317,7 +317,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking 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 ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: true), range: entity.range) } return nil }) diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 286813dbf3..5aebfeb694 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -62,7 +62,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { case peer(EnginePeer.Id) } - case header(String) + case header(NSAttributedString) case mainLinkHeader(String) case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool) @@ -225,13 +225,13 @@ private enum InviteLinksListEntry: ItemListNodeEntry { private func folderInviteLinkListControllerEntries( presentationData: PresentationData, state: FolderInviteLinkListControllerState, - title: String, + title: ChatFolderTitle, allPeers: [EnginePeer] ) -> [InviteLinksListEntry] { var entries: [InviteLinksListEntry] = [] var infoString: String? - let chatCountString: String + let chatCountString: NSAttributedString let peersHeaderString: String let canShareChats = !allPeers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) @@ -241,16 +241,36 @@ private func folderInviteLinkListControllerEntries( if !canShareChats { infoString = presentationData.strings.FolderLinkScreen_TitleDescriptionUnavailable - chatCountString = presentationData.strings.FolderLinkScreen_ChatCountHeaderUnavailable + chatCountString = NSAttributedString(string: presentationData.strings.FolderLinkScreen_ChatCountHeaderUnavailable) peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderUnavailable } else if state.selectedPeerIds.isEmpty { - chatCountString = presentationData.strings.FolderLinkScreen_TitleDescriptionDeselected(title).string + let chatCountStringValue = NSMutableAttributedString(string: presentationData.strings.FolderLinkScreen_TitleDescriptionDeselectedV2) + let folderRange = (chatCountStringValue.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + chatCountStringValue.replaceCharacters(in: folderRange, with: "") + chatCountStringValue.insert(title.rawAttributedString, at: folderRange.location) + } + + chatCountString = chatCountStringValue peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeader if allPeers.count > 1 { selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll } } else { - chatCountString = presentationData.strings.FolderLinkScreen_TitleDescriptionSelected(title, presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedCount(Int32(state.selectedPeerIds.count))).string + let chatCountStringValue = NSMutableAttributedString(string: presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedV2) + let folderRange = (chatCountStringValue.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + chatCountStringValue.replaceCharacters(in: folderRange, with: "") + chatCountStringValue.insert(title.rawAttributedString, at: folderRange.location) + } + let chatsRange = (chatCountStringValue.string as NSString).range(of: "{chats}") + if chatsRange.location != NSNotFound { + chatCountStringValue.replaceCharacters(in: chatsRange, with: "") + let countValue = presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedCount(Int32(state.selectedPeerIds.count)) + chatCountStringValue.insert(NSAttributedString(string: countValue), at: chatsRange.location) + } + + chatCountString = chatCountStringValue peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderSelected(Int32(state.selectedPeerIds.count)) if allPeers.count > 1 { selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll @@ -699,11 +719,10 @@ 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.text, + title: filterTitle, allPeers: allPeers ), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index 60730c0c5c..d51c7950ae 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -11,18 +11,19 @@ import TelegramAnimatedStickerNode import AccountContext import Markdown import TextFormat +import TextNodeWithEntities public class InviteLinkHeaderItem: ListViewItem, ItemListItem { public let context: AccountContext public let theme: PresentationTheme public let title: String? - public let text: String + public let text: NSAttributedString public let animationName: String public let hideOnSmallScreens: Bool public let sectionId: ItemListSectionId public let linkAction: ((ItemListTextItemLinkAction) -> Void)? - public init(context: AccountContext, theme: PresentationTheme, title: String? = nil, text: String, animationName: String, hideOnSmallScreens: Bool = false, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { + public init(context: AccountContext, theme: PresentationTheme, title: String? = nil, text: NSAttributedString, animationName: String, hideOnSmallScreens: Bool = false, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { self.context = context self.theme = theme self.title = title @@ -75,7 +76,7 @@ private let textFont = Font.regular(14.0) class InviteLinkHeaderItemNode: ListViewItemNode { private let titleNode: TextNode - private let textNode: TextNode + private let textNode: TextNodeWithEntities private var animationNode: AnimatedStickerNode private var item: InviteLinkHeaderItem? @@ -86,17 +87,17 @@ class InviteLinkHeaderItemNode: ListViewItemNode { self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.contentMode = .left - self.textNode.contentsScale = UIScreen.main.scale + self.textNode = TextNodeWithEntities() + self.textNode.textNode.isUserInteractionEnabled = false + self.textNode.textNode.contentMode = .left + self.textNode.textNode.contentsScale = UIScreen.main.scale self.animationNode = DefaultAnimatedStickerNodeImpl() super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) + self.addSubnode(self.textNode.textNode) self.addSubnode(self.animationNode) } @@ -112,7 +113,7 @@ class InviteLinkHeaderItemNode: ListViewItemNode { func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) return { item, params, neighbors in let leftInset: CGFloat = 24.0 + params.leftInset @@ -131,9 +132,22 @@ class InviteLinkHeaderItemNode: ListViewItemNode { let attributedTitle = NSAttributedString(string: item.title ?? "", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) - let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - })) + let attributedText = NSMutableAttributedString(string: item.text.string) + attributedText.addAttribute(.font, value: Font.regular(14.0), range: NSRange(location: 0, length: attributedText.length)) + attributedText.addAttribute(.foregroundColor, value: item.theme.list.freeTextColor, range: NSRange(location: 0, length: attributedText.length)) + item.text.enumerateAttributes(in: NSRange(location: 0, length: item.text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + attributedText.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + attributedText.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + attributedText.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + attributedText.addAttribute(key, value: value, range: range) + } + } + }) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) @@ -168,8 +182,15 @@ class InviteLinkHeaderItemNode: ListViewItemNode { origin += titleLayout.size.height + spacing } - let _ = textApply() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size) + let _ = textApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: item.theme.list.mediaPlaceholderColor, + attemptSynchronous: true + )) + strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size) + strongSelf.textNode.visibilityRect = .infinite } }) } @@ -189,9 +210,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: - let textFrame = self.textNode.frame + let textFrame = self.textNode.textNode.frame if let item = self.item, textFrame.contains(location) { - if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) { + if let (_, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { item.linkAction?(.tap(url)) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index d2db511cc2..62acacd470 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -56,7 +56,7 @@ private enum InviteLinksListSection: Int32 { } private enum InviteLinksListEntry: ItemListNodeEntry { - case header(PresentationTheme, String) + case header(PresentationTheme, NSAttributedString) case mainLinkHeader(PresentationTheme, String) case mainLink(PresentationTheme, ExportedInvitation?, [EnginePeer], Int32, Bool) @@ -278,7 +278,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, } else { helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelp } - entries.append(.header(presentationData.theme, helpText)) + entries.append(.header(presentationData.theme, NSAttributedString(string: helpText))) } let mainInvite: ExportedInvitation? diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index 2b592ce5ab..5789250a20 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -100,7 +100,7 @@ private enum InviteRequestsEntry: ItemListNodeEntry { let arguments = arguments as! InviteRequestsControllerArguments switch self { case let .header(theme, text): - return InviteLinkHeaderItem(context: arguments.context, theme: theme, text: text, animationName: "Requests", sectionId: self.section, linkAction: { _ in + return InviteLinkHeaderItem(context: arguments.context, theme: theme, text: NSAttributedString(string: text), animationName: "Requests", sectionId: self.section, linkAction: { _ in arguments.openLinks() }) case let .requestsHeader(_, text): diff --git a/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift b/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift index 54e5ac3402..2a23bed3ce 100644 --- a/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift +++ b/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift @@ -112,7 +112,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { let arguments = arguments as! PaymentMethodListScreenArguments switch self { case let .header(text): - return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "Invite", sectionId: self.section) + return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: NSAttributedString(string: text), animationName: "Invite", sectionId: self.section) case let .methodsHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .addMethod(text): diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift index 6f6e2f63f5..0116746e4a 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift @@ -158,7 +158,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { case let .participant(participant, label, revealActions, revealed, enabled): let status: ContactsPeerItemStatus if let label = label { - status = .custom(string: label, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: label), multiline: false, isActive: false, icon: nil) } else if let presence = participant.presences[participant.peer.id], self.addIcon { status = .presence(EnginePeer.Presence(presence), dateTimeFormat) } else { diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index c85e72cd03..b9f3bf040d 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -129,7 +129,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { case let .peer(_, participant, editing, label, enabled, isChannel, isContact): let status: ContactsPeerItemStatus if let label = label { - status = .custom(string: label, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: label), multiline: false, isActive: false, icon: nil) } else if participant.peer.id != context.account.peerId { let presence = participant.presences[participant.peer.id] ?? TelegramUserPresence(status: .none, lastActivity: 0) status = .presence(EnginePeer.Presence(presence), presentationData.dateTimeFormat) diff --git a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift index 194d60b171..f7517c7427 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift @@ -117,7 +117,7 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable { let arguments = arguments as! DeleteAccountDataArguments switch self { case let .header(theme, animation, title, text, hideOnSmallScreens): - return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: text, animationName: animation, hideOnSmallScreens: hideOnSmallScreens, sectionId: self.section, linkAction: nil) + return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: NSAttributedString(string: text), animationName: animation, hideOnSmallScreens: hideOnSmallScreens, sectionId: self.section, linkAction: nil) case let .peers(_, peers): return DeleteAccountPeersItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, peers: peers, sectionId: self.section) case let .info(_, text): diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 3e5b6d6795..93e8576801 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -234,8 +234,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1135897376] = { return Api.DefaultHistoryTTL.parse_defaultHistoryTTL($0) } dict[-712374074] = { return Api.Dialog.parse_dialog($0) } dict[1908216652] = { return Api.Dialog.parse_dialogFolder($0) } - dict[1605718587] = { return Api.DialogFilter.parse_dialogFilter($0) } - dict[-1612542300] = { return Api.DialogFilter.parse_dialogFilterChatlist($0) } + dict[-1438177711] = { return Api.DialogFilter.parse_dialogFilter($0) } + dict[-1772913705] = { return Api.DialogFilter.parse_dialogFilterChatlist($0) } dict[909284270] = { return Api.DialogFilter.parse_dialogFilterDefault($0) } dict[2004110666] = { return Api.DialogFilterSuggested.parse_dialogFilterSuggested($0) } dict[-445792507] = { return Api.DialogPeer.parse_dialogPeer($0) } @@ -582,7 +582,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1434950843] = { return Api.MessageAction.parse_messageActionSetChatTheme($0) } dict[1348510708] = { return Api.MessageAction.parse_messageActionSetChatWallPaper($0) } dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } - dict[139818551] = { return Api.MessageAction.parse_messageActionStarGift($0) } + dict[-1253342558] = { return Api.MessageAction.parse_messageActionStarGift($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } @@ -1225,7 +1225,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1044107055] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultAdsHidden($0) } dict[-2073059774] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultChooseOption($0) } dict[-1384544183] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultReported($0) } - dict[500007837] = { return Api.chatlists.ChatlistInvite.parse_chatlistInvite($0) } + dict[-250687953] = { return Api.chatlists.ChatlistInvite.parse_chatlistInvite($0) } dict[-91752871] = { return Api.chatlists.ChatlistInvite.parse_chatlistInviteAlready($0) } dict[-1816295539] = { return Api.chatlists.ChatlistUpdates.parse_chatlistUpdates($0) } dict[283567014] = { return Api.chatlists.ExportedChatlistInvite.parse_exportedChatlistInvite($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 237a8c7d33..64cc3bdac5 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -373,7 +373,7 @@ public extension Api { case messageActionSetChatTheme(emoticon: String) case messageActionSetChatWallPaper(flags: Int32, wallpaper: Api.WallPaper) case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?) - case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?) + case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, canExportAt: Int32?) case messageActionSuggestProfilePhoto(photo: Api.Photo) case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?) @@ -719,14 +719,15 @@ public extension Api { serializeInt32(period, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(autoSettingFrom!, buffer: buffer, boxed: false)} break - case .messageActionStarGift(let flags, let gift, let message, let convertStars): + case .messageActionStarGift(let flags, let gift, let message, let convertStars, let canExportAt): if boxed { - buffer.appendInt32(139818551) + buffer.appendInt32(-1253342558) } serializeInt32(flags, buffer: buffer, boxed: false) gift.serialize(buffer, true) if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} if Int(flags) & Int(1 << 4) != 0 {serializeInt64(convertStars!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 7) != 0 {serializeInt32(canExportAt!, buffer: buffer, boxed: false)} break case .messageActionSuggestProfilePhoto(let photo): if boxed { @@ -853,8 +854,8 @@ public extension Api { return ("messageActionSetChatWallPaper", [("flags", flags as Any), ("wallpaper", wallpaper as Any)]) case .messageActionSetMessagesTTL(let flags, let period, let autoSettingFrom): return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)]) - case .messageActionStarGift(let flags, let gift, let message, let convertStars): - return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any)]) + case .messageActionStarGift(let flags, let gift, let message, let convertStars, let canExportAt): + return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("canExportAt", canExportAt as Any)]) case .messageActionSuggestProfilePhoto(let photo): return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)]) case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): @@ -1515,12 +1516,15 @@ public extension Api { } } var _4: Int64? if Int(_1!) & Int(1 << 4) != 0 {_4 = reader.readInt64() } + var _5: Int32? + if Int(_1!) & Int(1 << 7) != 0 {_5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4) + let _c5 = (Int(_1!) & Int(1 << 7) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4, canExportAt: _5) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index ea00fe9e2c..8fff493cf4 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -476,17 +476,17 @@ public extension Api.channels { } public extension Api.chatlists { enum ChatlistInvite: TypeConstructorDescription { - case chatlistInvite(flags: Int32, title: String, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) + case chatlistInvite(flags: Int32, title: Api.TextWithEntities, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) case chatlistInviteAlready(filterId: Int32, missingPeers: [Api.Peer], alreadyPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .chatlistInvite(let flags, let title, let emoticon, let peers, let chats, let users): if boxed { - buffer.appendInt32(500007837) + buffer.appendInt32(-250687953) } serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) + title.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(peers.count)) @@ -545,8 +545,10 @@ public extension Api.chatlists { public static func parse_chatlistInvite(_ reader: BufferReader) -> ChatlistInvite? { var _1: Int32? _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) + var _2: Api.TextWithEntities? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } var _3: String? if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } var _4: [Api.Peer]? diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index 4b53dff4ca..609de6bbae 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -1166,19 +1166,19 @@ public extension Api { } public extension Api { enum DialogFilter: TypeConstructorDescription { - case dialogFilter(flags: Int32, id: Int32, title: String, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer]) - case dialogFilterChatlist(flags: Int32, id: Int32, title: String, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer]) + case dialogFilter(flags: Int32, id: Int32, title: Api.TextWithEntities, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer]) + case dialogFilterChatlist(flags: Int32, id: Int32, title: Api.TextWithEntities, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer]) case dialogFilterDefault public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .dialogFilter(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers, let excludePeers): if boxed { - buffer.appendInt32(1605718587) + buffer.appendInt32(-1438177711) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) + title.serialize(buffer, true) if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 27) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) @@ -1199,11 +1199,11 @@ public extension Api { break case .dialogFilterChatlist(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers): if boxed { - buffer.appendInt32(-1612542300) + buffer.appendInt32(-1772913705) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) + title.serialize(buffer, true) if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 27) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) @@ -1242,8 +1242,10 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) + var _3: Api.TextWithEntities? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } var _4: String? if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) } var _5: Int32? @@ -1280,8 +1282,10 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) + var _3: Api.TextWithEntities? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } var _4: String? if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) } var _5: Int32? diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 1fafc7c3c5..43b6dd748f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -171,7 +171,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId)) case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId): return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId))) - case let .messageActionStarGift(flags, apiGift, message, convertStars): + case let .messageActionStarGift(flags, apiGift, message, convertStars, _): let text: String? let entities: [MessageTextEntity]? switch message { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 14e5750201..1c9193d8bf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -342,9 +342,17 @@ extension ChatListFilter { case .dialogFilterDefault: self = .allChats case let .dialogFilter(flags, id, title, emoticon, color, pinnedPeers, includePeers, excludePeers): + let titleText: String + let titleEntities: [MessageTextEntity] + switch title { + case let .textWithEntities(text, entities): + titleText = text + titleEntities = messageTextEntitiesFromApiEntities(entities) + } + let disableTitleAnimations = (flags & (1 << 28)) != 0 self = .filter( id: id, - title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), + title: ChatFolderTitle(text: titleText, entities: titleEntities, enableAnimations: !disableTitleAnimations), emoticon: emoticon, data: ChatListFilterData( isShared: false, @@ -392,9 +400,18 @@ extension ChatListFilter { ) ) case let .dialogFilterChatlist(flags, id, title, emoticon, color, pinnedPeers, includePeers): + let titleText: String + let titleEntities: [MessageTextEntity] + switch title { + case let .textWithEntities(text, entities): + titleText = text + titleEntities = messageTextEntitiesFromApiEntities(entities) + } + let disableTitleAnimations = (flags & (1 << 28)) != 0 + self = .filter( id: id, - title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), + title: ChatFolderTitle(text: titleText, entities: titleEntities, enableAnimations: !disableTitleAnimations), emoticon: emoticon, data: ChatListFilterData( isShared: true, @@ -446,7 +463,10 @@ extension ChatListFilter { if data.color != nil { flags |= 1 << 27 } - return .dialogFilterChatlist(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + if !title.enableAnimations { + flags |= 1 << 28 + } + return .dialogFilterChatlist(flags: flags, id: id, title: .textWithEntities(text: title.text, entities: apiEntitiesFromMessageTextEntities(title.entities, associatedPeers: SimpleDictionary())), 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) { @@ -472,7 +492,10 @@ extension ChatListFilter { if data.color != nil { flags |= 1 << 27 } - return .dialogFilter(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + if !title.enableAnimations { + flags |= 1 << 28 + } + return .dialogFilter(flags: flags, id: id, title: .textWithEntities(text: title.text, entities: apiEntitiesFromMessageTextEntities(title.entities, associatedPeers: SimpleDictionary())), 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) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 7d6345288d..a755647731 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -273,9 +273,11 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> ChatFolderLinkContents in switch result { - case let .chatlistInvite(_, title, emoticon, peers, chats, users): + case let .chatlistInvite(flags, title, emoticon, peers, chats, users): let _ = emoticon + let disableTitleAnimation = (flags & (1 << 1)) != 0 + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) var memberCounts: [PeerId: Int] = [:] @@ -301,7 +303,15 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal 1 { + if case .group = channel.info, let onlineMemberCount = onlineMemberCount.recent, onlineMemberCount > 1 { let string = NSMutableAttributedString() string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index dd5215544e..ad10e82bc8 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -382,6 +382,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } + public var enableAnimation: Bool = true + public weak var mirrorLayer: CALayer? { didSet { if let mirrorLayer = self.mirrorLayer { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index 2b85d1e5f8..6a191d60a3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -330,7 +330,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS } let context = self.context - let undoController = UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: self.presentationData.strings.SavedMessages_SubChatDeleted, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + let undoController = UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: self.presentationData.strings.SavedMessages_SubChatDeleted), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in if value == .commit { let _ = context.engine.messages.clearHistoryInteractively(peerId: context.account.peerId, threadId: peer.id.toInt64(), type: .forLocalPeer).startStandalone(completed: { guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 420b7a100b..cb57844c76 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1659,7 +1659,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen ) } case let .group(groupId): - var onlineMemberCount: Signal = .single(nil) + var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) if peerId.namespace == Namespaces.Peer.CloudChannel { onlineMemberCount = context.account.viewTracker.peerView(groupId, updateData: false) |> map { view -> Bool? in @@ -1676,17 +1676,21 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } } |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in + |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in if let isLarge = isLarge { if isLarge { return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (nil, value) + } } else { return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (value.total, value.recent) + } } } else { - return .single(nil) + return .single((nil, nil)) } } } @@ -1695,9 +1699,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen context.account.viewTracker.peerView(groupId, updateData: false), onlineMemberCount ) - |> map { peerView, onlineMemberCount -> PeerInfoStatusData? in - if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - if let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { + |> map { peerView, memberCountData -> PeerInfoStatusData? in + let (preciseTotalMemberCount, onlineMemberCount) = memberCountData + + if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = preciseTotalMemberCount ?? cachedChannelData.participantsSummary.memberCount { + if let onlineMemberCount, onlineMemberCount > 1 { var string = "" string.append("\(strings.Conversation_StatusMembers(Int32(memberCount))), ") diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift index 96e46e2053..7b29fcea7a 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift @@ -172,7 +172,7 @@ private enum OldChannelsEntry: ItemListNodeEntry { case let .peersHeader(title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .peer(_, peer, selected): - return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in + return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in arguments.togglePeer(peer.peer.id, true) }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) } diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift index 1575419d2e..bfb63c09c7 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift @@ -154,7 +154,7 @@ private enum OldChannelsSearchEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem { switch self { case let .peer(_, peer, selected): - return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in + return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in interaction.togglePeer(peer.peer.id) }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) } diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift index 55fff93b96..eb5a0a6221 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift @@ -171,7 +171,7 @@ final class PeerSelectionLoadingView: UIView { context: context, peerMode: .peer, peer: .peer(peer: peer1, chatPeer: peer1), - status: .custom(string: "status", multiline: false, isActive: false, icon: nil), + status: .custom(string: NSAttributedString(string: "status"), multiline: false, isActive: false, icon: nil), badge: nil, requiresPremiumForMessaging: false, enabled: true, @@ -242,7 +242,7 @@ final class PeerSelectionLoadingView: UIView { let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY) fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 100.0) - let textFrame = itemNodes[sampleIndex].statusNode.frame.offsetBy(dx: 0.0, dy: currentY) + let textFrame = itemNodes[sampleIndex].statusNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY) fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 40.0) context.setBlendMode(.normal) diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index c095335e97..b99ea5e371 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -119,7 +119,7 @@ final class PeerSelectionScreenComponent: Component { context: listNode.context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), - status: .custom(string: statusText, multiline: false, isActive: false, icon: nil), + status: .custom(string: NSAttributedString(string: statusText), multiline: false, isActive: false, icon: nil), badge: nil, requiresPremiumForMessaging: false, enabled: true, diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index dc3db47119..054b0fc279 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -20,11 +20,13 @@ private final class InlineStickerItem: Hashable { let emoji: ChatTextInputTextCustomEmojiAttribute let file: TelegramMediaFile? let fontSize: CGFloat + let enableAnimation: Bool - init(emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, fontSize: CGFloat) { + init(emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, fontSize: CGFloat, enableAnimation: Bool) { self.emoji = emoji self.file = file self.fontSize = fontSize + self.enableAnimation = enableAnimation } func hash(into hasher: inout Hasher) { @@ -42,6 +44,9 @@ private final class InlineStickerItem: Hashable { if lhs.fontSize != rhs.fontSize { return false } + if lhs.enableAnimation != rhs.enableAnimation { + return false + } return true } } @@ -65,19 +70,25 @@ public final class TextNodeWithEntities { public let renderer: MultiAnimationRenderer public let placeholderColor: UIColor public let attemptSynchronous: Bool + public let emojiOffset: CGPoint + public let fontSizeNorm: CGFloat public init( context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, - attemptSynchronous: Bool + attemptSynchronous: Bool, + emojiOffset: CGPoint = CGPoint(), + fontSizeNorm: CGFloat = 17.0 ) { self.context = context self.cache = cache self.renderer = renderer self.placeholderColor = placeholderColor self.attemptSynchronous = attemptSynchronous + self.emojiOffset = emojiOffset + self.fontSizeNorm = fontSizeNorm } public func withUpdatedPlaceholderColor(_ color: UIColor) -> Arguments { @@ -86,7 +97,8 @@ public final class TextNodeWithEntities { cache: self.cache, renderer: self.renderer, placeholderColor: color, - attemptSynchronous: self.attemptSynchronous + attemptSynchronous: self.attemptSynchronous, + emojiOffset: self.emojiOffset ) } } @@ -96,6 +108,8 @@ public final class TextNodeWithEntities { private var enableLooping: Bool = true + public var resetEmojiToFirstFrameAutomatically: Bool = false + public var visibilityRect: CGRect? { didSet { if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibilityRect { @@ -110,7 +124,13 @@ public final class TextNodeWithEntities { } else { isItemVisible = false } - itemLayer.isVisibleForAnimations = self.enableLooping && isItemVisible + let isVisibleForAnimations = self.enableLooping && isItemVisible && itemLayer.enableAnimation + if itemLayer.isVisibleForAnimations != isVisibleForAnimations { + itemLayer.isVisibleForAnimations = isVisibleForAnimations + if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { + itemLayer.reloadAnimation() + } + } } } } @@ -141,7 +161,7 @@ public final class TextNodeWithEntities { let replacementRange = NSRange(location: 0, length: updatedSubstring.length) updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) - updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange) + updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize, enableAnimation: value.enableAnimation), range: replacementRange) updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange) originalTextId += 1 @@ -197,7 +217,7 @@ public final class TextNodeWithEntities { if let maybeNode = maybeNode { if let applyArguments = applyArguments { - maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false) + maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, emojiOffset: applyArguments.emojiOffset, fontSizeNorm: applyArguments.fontSizeNorm) } return maybeNode @@ -205,7 +225,7 @@ public final class TextNodeWithEntities { let resultNode = TextNodeWithEntities(textNode: result) if let applyArguments = applyArguments { - resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false) + resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, emojiOffset: applyArguments.emojiOffset, fontSizeNorm: applyArguments.fontSizeNorm) } return resultNode @@ -222,7 +242,7 @@ public final class TextNodeWithEntities { } } - private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, attemptSynchronousLoad: Bool) { + private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, attemptSynchronousLoad: Bool, emojiOffset: CGPoint, fontSizeNorm: CGFloat) { self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji var nextIndexById: [Int64: Int] = [:] @@ -241,9 +261,9 @@ public final class TextNodeWithEntities { let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) validIds.append(id) - let itemSize = floorToScreenPixels(stickerItem.fontSize * 24.0 / 17.0) + let itemSize = floorToScreenPixels(stickerItem.fontSize * 24.0 / fontSizeNorm) - var itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 1.0).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) + var itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left + emojiOffset.x, dy: textLayout.insets.top + 1.0 + emojiOffset.y).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) @@ -256,8 +276,13 @@ public final class TextNodeWithEntities { itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) - - itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame) + } + itemLayer.enableAnimation = stickerItem.enableAnimation + let isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame) && itemLayer.enableAnimation + if itemLayer.isVisibleForAnimations != isVisibleForAnimations { + if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { + itemLayer.reloadAnimation() + } } itemLayer.frame = itemFrame @@ -301,12 +326,19 @@ public class ImmediateTextNodeWithEntities: TextNode { private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] public private(set) var dustNode: InvisibleInkDustNode? + public var resetEmojiToFirstFrameAutomatically: Bool = false + public var visibility: Bool = false { didSet { if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibility { for (_, itemLayer) in self.inlineStickerItemLayers { - let isItemVisible: Bool = self.visibility - itemLayer.isVisibleForAnimations = self.enableLooping && isItemVisible + let isVisibleForAnimations = self.enableLooping && self.visibility && itemLayer.enableAnimation + if itemLayer.isVisibleForAnimations != isVisibleForAnimations { + itemLayer.isVisibleForAnimations = isVisibleForAnimations + if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { + itemLayer.reloadAnimation() + } + } } } } @@ -375,7 +407,7 @@ public class ImmediateTextNodeWithEntities: TextNode { let replacementRange = NSRange(location: 0, length: updatedSubstring.length) updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) - updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange) + updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize, enableAnimation: value.enableAnimation), range: replacementRange) updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange) originalTextId += 1 @@ -437,7 +469,7 @@ public class ImmediateTextNodeWithEntities: TextNode { var enableAnimations = true if let arguments = self.arguments { - self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor, fontSizeNorm: arguments.fontSizeNorm) enableAnimations = arguments.context.sharedContext.energyUsageSettings.fullTranslucency } self.updateSpoilers(enableAnimations: enableAnimations, textLayout: layout) @@ -450,7 +482,7 @@ public class ImmediateTextNodeWithEntities: TextNode { return layout.size } - private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) { + private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, fontSizeNorm: CGFloat) { self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji var nextIndexById: [Int64: Int] = [:] @@ -469,7 +501,7 @@ public class ImmediateTextNodeWithEntities: TextNode { let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) validIds.append(id) - let itemSide = floor(stickerItem.fontSize * 24.0 / 17.0) + let itemSide = floor(stickerItem.fontSize * 24.0 / fontSizeNorm) var itemSize = CGSize(width: itemSide, height: itemSide) if let file = stickerItem.file, let customItemLayout = self.customItemLayout { itemSize = customItemLayout(itemSize, file) @@ -486,8 +518,15 @@ public class ImmediateTextNodeWithEntities: TextNode { itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.layer.addSublayer(itemLayer) - - itemLayer.isVisibleForAnimations = self.enableLooping && self.visibility + } + + itemLayer.enableAnimation = stickerItem.enableAnimation + let isVisibleForAnimations = self.enableLooping && self.visibility && itemLayer.enableAnimation + if itemLayer.isVisibleForAnimations != isVisibleForAnimations { + itemLayer.isVisibleForAnimations = isVisibleForAnimations + if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { + itemLayer.reloadAnimation() + } } itemLayer.frame = itemFrame @@ -535,7 +574,7 @@ public class ImmediateTextNodeWithEntities: TextNode { let _ = apply() if let arguments = self.arguments { - self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor, fontSizeNorm: arguments.fontSizeNorm) } return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated, numberOfLines: layout.numberOfLines) @@ -550,7 +589,7 @@ public class ImmediateTextNodeWithEntities: TextNode { let _ = apply() if let arguments = self.arguments { - self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor, fontSizeNorm: arguments.fontSizeNorm) } return layout diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 12dcba72d8..0aaf337472 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5221,11 +5221,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let peerId = chatLocationPeerId if case let .peer(peerView) = self.chatLocationInfoData, let peerId = peerId { peerView.set(context.account.viewTracker.peerView(peerId)) - var onlineMemberCount: Signal = .single(nil) + var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) var hasScheduledMessages: Signal = .single(false) if peerId.namespace == Namespaces.Peer.CloudChannel { - let recentOnlineSignal: Signal = peerView.get() + let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView.get() |> map { view -> Bool? in if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { if case .broadcast = peer.info { @@ -5240,17 +5240,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in + |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in if let isLarge = isLarge { if isLarge { return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (nil, value) + } } else { return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (value.total, value.recent) + } } } else { - return .single(nil) + return .single((nil, nil)) } } onlineMemberCount = recentOnlineSignal @@ -6192,9 +6196,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - var onlineMemberCount: Signal = .single(nil) + var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) if peerId.namespace == Namespaces.Peer.CloudChannel { - let recentOnlineSignal: Signal = peerView + let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView |> map { view -> Bool? in if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { if case .broadcast = peer.info { @@ -6209,17 +6213,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in + |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in if let isLarge = isLarge { if isLarge { return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (nil, value) + } } else { return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (value.total, value.recent) + } } } else { - return .single(nil) + return .single((nil, nil)) } } onlineMemberCount = recentOnlineSignal @@ -6321,7 +6329,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G peerPresences: [:], cachedData: nil ) - strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: nil, isScheduledMessages: false, isMuted: false, customMessageCount: savedMessagesPeer?.messageCount ?? 0, isEnabled: true) + strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: savedMessagesPeer?.messageCount ?? 0, isEnabled: true) strongSelf.peerView = peerView @@ -8551,7 +8559,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G statusText = self.presentationData.strings.Undo_ChatCleared } - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, action: { [weak self] value in guard let strongSelf = self else { return false } diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index ef5d3f69b1..cfcd03c411 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -94,7 +94,7 @@ extension ChatControllerImpl { self.present( UndoOverlayController( presentationData: self.presentationData, - content: undoRights.isEmpty ? .actionSucceeded(title: title, text: text, cancel: nil, destructive: false) : .removedChat(title: title ?? text, text: title == nil ? nil : text), + content: undoRights.isEmpty ? .actionSucceeded(title: title, text: text, cancel: nil, destructive: false) : .removedChat(context: self.context, title: NSAttributedString(string: title ?? text), text: title == nil ? nil : text), elevatedLayout: false, action: { [weak self] action in guard let self else { @@ -357,7 +357,7 @@ extension ChatControllerImpl { self.chatDisplayNode.historyNode.ignoreMessageIds = Set(messageIds) let undoTitle = self.presentationData.strings.Chat_MessagesDeletedToast_Text(Int32(messageIds.count)) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: undoTitle, text: nil), elevatedLayout: false, position: .top, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: undoTitle), text: nil), elevatedLayout: false, position: .top, action: { [weak self] value in guard let self else { return false } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index 8fa4517666..e58f51508a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -153,7 +153,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = range - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, action: { value in guard let strongSelf = self else { return false } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index b65c8f6967..67b5531abc 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -35,6 +35,7 @@ import ChatMessageTransitionNode import ChatControllerInteraction import DustEffect import UrlHandling +import TextFormat struct ChatTopVisibleMessageRange: Equatable { var lowerBound: MessageIndex @@ -2313,41 +2314,57 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.currentOverscrollExpandProgress = expandProgress if let nextChannelToRead = self.nextChannelToRead { - let swipeText: (String, [(Int, NSRange)]) - let releaseText: (String, [(Int, NSRange)]) + let swipeText: NSAttributedString + let releaseText: NSAttributedString switch nextChannelToRead.location { case .same: if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl, chatController.customChatNavigationStack != nil { - swipeText = (self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeAction) } else if nextChannelToRead.threadData != nil { - swipeText = (self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeAction) } else { - swipeText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction) } case .archived: - swipeText = (self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeAction) case .unarchived: - swipeText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction) case let .folder(_, title): - //TODO:release - swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title.text)._tuple - releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title.text)._tuple + let swipeTextValue = NSMutableAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgressV2) + let swipeFolderRange = (swipeTextValue.string as NSString).range(of: "{folder}") + if swipeFolderRange.location != NSNotFound { + swipeTextValue.replaceCharacters(in: swipeFolderRange, with: "") + swipeTextValue.insert(title.attributedString(attributes: [ + ChatTextInputAttributes.bold: true + ]), at: swipeFolderRange.location) + } + swipeText = swipeTextValue + + let releaseTextValue = NSMutableAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelFolderSwipeActionV2) + let releaseTextFolderRange = (releaseTextValue.string as NSString).range(of: "{folder}") + if releaseTextFolderRange.location != NSNotFound { + releaseTextValue.replaceCharacters(in: releaseTextFolderRange, with: "") + releaseTextValue.insert(title.attributedString(attributes: [ + ChatTextInputAttributes.bold: true + ]), at: releaseTextFolderRange.location) + } + releaseText = releaseTextValue } if expandProgress < 0.1 { chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil) } else if expandProgress >= 1.0 { - if chatControllerNode.inputPanelOverscrollNode?.text.0 != releaseText.0 { - chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(text: releaseText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 1)) + if chatControllerNode.inputPanelOverscrollNode?.text.string != releaseText.string { + chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(context: self.context, text: releaseText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 1)) } } else { - if chatControllerNode.inputPanelOverscrollNode?.text.0 != swipeText.0 { - chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(text: swipeText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 2)) + if chatControllerNode.inputPanelOverscrollNode?.text.string != swipeText.string { + chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(context: self.context, text: swipeText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 2)) } } } else { diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index 4c25ab81ff..e60a75f17b 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -550,7 +550,7 @@ public final class PeerChannelMemberCategoriesContextsManager { |> runOn(Queue.mainQueue()) } - public func recentOnlineSmall(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal { + public func recentOnlineSmall(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal<(total: Int32, recent: Int32), NoError> { return Signal { [weak self] subscriber in var previousIds: Set? let statusesDisposable = MetaDisposable() @@ -587,7 +587,7 @@ public final class PeerChannelMemberCategoriesContextsManager { } |> distinctUntilChanged |> deliverOnMainQueue).start(next: { count in - subscriber.putNext(count) + subscriber.putNext((Int32(updatedIds.count), count)) })) } }) diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 91266ba908..98531b6385 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -402,6 +402,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { case file case topicId case topicInfo + case enableAnimation } public enum Custom: Codable { @@ -417,12 +418,14 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { public let fileId: Int64 public let file: TelegramMediaFile? public let custom: Custom? + public let enableAnimation: Bool - public init(interactivelySelectedFromPackId: ItemCollectionId?, fileId: Int64, file: TelegramMediaFile?, custom: Custom? = nil) { + public init(interactivelySelectedFromPackId: ItemCollectionId?, fileId: Int64, file: TelegramMediaFile?, custom: Custom? = nil, enableAnimation: Bool = true) { self.interactivelySelectedFromPackId = interactivelySelectedFromPackId self.fileId = fileId self.file = file self.custom = custom + self.enableAnimation = enableAnimation super.init() } @@ -433,6 +436,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { self.fileId = try container.decode(Int64.self, forKey: .fileId) self.file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) self.custom = nil + self.enableAnimation = try container.decodeIfPresent(Bool.self, forKey: .enableAnimation) ?? true } public func encode(to encoder: Encoder) throws { @@ -440,6 +444,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { try container.encodeIfPresent(self.interactivelySelectedFromPackId, forKey: .interactivelySelectedFromPackId) try container.encode(self.fileId, forKey: .fileId) try container.encodeIfPresent(self.file, forKey: .file) + try container.encode(self.enableAnimation, forKey: .enableAnimation) } override public func isEqual(_ object: Any?) -> Bool { diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 0a4f70825d..acbe58cbcc 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -8,7 +8,7 @@ import ComponentFlow import AnimatedTextComponent public enum UndoOverlayContent { - case removedChat(title: String, text: String?) + case removedChat(context: AccountContext, title: NSAttributedString, text: String?) case archivedChat(peerId: Int64, title: String, text: String, undo: Bool) case hidArchive(title: String, text: String, undo: Bool) case revealedArchive(title: String, text: String, undo: Bool) @@ -19,8 +19,8 @@ public enum UndoOverlayContent { case actionSucceeded(title: String?, text: String, cancel: String?, destructive: Bool) case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: StickerPackItem?, context: AccountContext) case dice(dice: TelegramMediaDice, context: AccountContext, text: String, action: String?) - case chatAddedToFolder(chatTitle: String, folderTitle: String) - case chatRemovedFromFolder(chatTitle: String, folderTitle: String) + case chatAddedToFolder(context: AccountContext, chatTitle: String, folderTitle: NSAttributedString) + case chatRemovedFromFolder(context: AccountContext, chatTitle: String, folderTitle: NSAttributedString) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) case setProximityAlert(title: String, text: String, cancelled: Bool) case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, title: String?, text: String, action: String?, duration: Double) @@ -45,6 +45,7 @@ public enum UndoOverlayContent { case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?, timeout: Double?) + case universalWithEntities(context: AccountContext, animation: String, scale: CGFloat, colors: [String: UIColor], title: NSAttributedString?, text: NSAttributedString, animateEntities: Bool, customUndoText: String?, timeout: Double?) case universalImage(image: UIImage, size: CGSize?, title: String?, text: String, customUndoText: String?, timeout: Double?) case premiumPaywall(title: String?, text: String, customUndoText: String?, timeout: Double?, linkAction: ((String) -> Void)?) case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index bf5973ab97..80204a9663 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -46,7 +46,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var stickerSourceSize: CGSize? private var stickerOffset: CGPoint? private var emojiStatus: ComponentView? - private let titleNode: ImmediateTextNode + private let titleNode: ImmediateTextNodeWithEntities private let textNode: ImmediateTextNodeWithEntities private var textComponent: ComponentView? private var animatedTextItems: [AnimatedTextComponent.Item]? @@ -92,7 +92,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.timerTextNode = ImmediateTextNode() self.timerTextNode.displaysAsynchronously = false - self.titleNode = ImmediateTextNode() + self.titleNode = ImmediateTextNodeWithEntities() self.titleNode.layer.anchorPoint = CGPoint() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 0 @@ -115,22 +115,51 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var isUserInteractionEnabled = false switch content { - case let .removedChat(title, text): + case let .removedChat(context, title, text): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil self.animationNode = nil self.animatedStickerNode = nil + + let attributedTitle = NSMutableAttributedString(string: title.string) + attributedTitle.addAttribute(.font, value: Font.semibold(14.0), range: NSRange(location: 0, length: title.length)) + attributedTitle.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(location: 0, length: title.length)) + title.enumerateAttributes(in: NSRange(location: 0, length: title.length), using: { attributes, range, _ in + for (key, value) in attributes { + attributedTitle.addAttribute(key, value: value, range: range) + } + }) + if let text { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + self.titleNode.attributedText = attributedTitle let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText } else { - self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + self.textNode.attributedText = attributedTitle } + + self.titleNode.visibility = true + self.titleNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + + self.textNode.visibility = true + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + displayUndo = true self.originalRemainingSeconds = 5 self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) @@ -343,38 +372,67 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) displayUndo = false self.originalRemainingSeconds = 5 - case let .chatAddedToFolder(chatTitle, folderTitle): + case let .chatAddedToFolder(context, chatTitle, folderTitle), let .chatRemovedFromFolder(context, chatTitle, folderTitle): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil - self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + + let baseString: String + if case .chatAddedToFolder = content { + baseString = presentationData.strings.ChatList_AddedToFolderTooltipV2 + self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + } else { + baseString = presentationData.strings.ChatList_RemovedFromFolderTooltipV2 + self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + } self.animatedStickerNode = nil - let formattedString = presentationData.strings.ChatList_AddedToFolderTooltip(chatTitle, folderTitle) - - let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString.string, font: Font.regular(14.0), textColor: .white)) - for range in formattedString.ranges { - string.addAttribute(.font, value: Font.regular(14.0), range: range.range) - } - - self.textNode.attributedText = string - displayUndo = false - self.originalRemainingSeconds = 5 - case let .chatRemovedFromFolder(chatTitle, folderTitle): - self.avatarNode = nil - self.iconNode = nil - self.iconCheckNode = nil - self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) - self.animatedStickerNode = nil - - let formattedString = presentationData.strings.ChatList_RemovedFromFolderTooltip(chatTitle, folderTitle) - - let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString.string, font: Font.regular(14.0), textColor: .white)) - for range in formattedString.ranges { - string.addAttribute(.font, value: Font.regular(14.0), range: range.range) + let string = NSMutableAttributedString(string: baseString) + string.addAttributes([ + .font: Font.regular(14.0), + .foregroundColor: UIColor.white + ], range: NSRange(location: 0, length: string.length)) + + let folderRange = (string.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + string.replaceCharacters(in: folderRange, with: "") + let processedFolderTitle = NSMutableAttributedString(string: folderTitle.string) + processedFolderTitle.addAttributes([ + .font: Font.semibold(14.0), + .foregroundColor: UIColor.white + ], range: NSRange(location: 0, length: processedFolderTitle.length)) + folderTitle.enumerateAttributes(in: NSRange(location: 0, length: folderTitle.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + processedFolderTitle.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + processedFolderTitle.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + processedFolderTitle.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + processedFolderTitle.addAttribute(key, value: value, range: range) + } + } + }) + string.insert(processedFolderTitle, at: folderRange.location) + } + + let chatRange = (string.string as NSString).range(of: "{chat}") + if chatRange.location != NSNotFound { + string.replaceCharacters(in: chatRange, with: "") + string.insert(NSAttributedString(string: chatTitle, font: Font.semibold(14.0), textColor: .white), at: chatRange.location) } self.textNode.attributedText = string + self.textNode.visibility = true + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + displayUndo = false self.originalRemainingSeconds = 5 case let .paymentSent(currencyValue, itemTitle): @@ -1072,6 +1130,86 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 5 + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } + case let .universalWithEntities(context, animation, scale, colors, title, text, animateEntities, customUndoText, timeout): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = AnimationNode(animation: animation, colors: colors, scale: scale) + self.animatedStickerNode = nil + + var attributedTitle: NSAttributedString? + if let title { + let attributedTitleValue = NSMutableAttributedString(string: title.string) + attributedTitleValue.addAttribute(.font, value: Font.semibold(14.0), range: NSRange(location: 0, length: title.length)) + attributedTitleValue.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(location: 0, length: title.length)) + title.enumerateAttributes(in: NSRange(location: 0, length: title.length), using: { attributes, range, _ in + for (key, value) in attributes { + attributedTitleValue.addAttribute(key, value: value, range: range) + } + }) + attributedTitle = attributedTitleValue + } + + if let attributedTitle, text.length == 0 { + self.titleNode.attributedText = nil + self.textNode.attributedText = attributedTitle + } else { + if let attributedTitle { + self.titleNode.attributedText = attributedTitle + } else { + self.titleNode.attributedText = nil + } + + let attributedText = NSMutableAttributedString(string: text.string) + attributedText.addAttribute(.font, value: Font.regular(14.0), range: NSRange(location: 0, length: text.length)) + attributedText.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(location: 0, length: text.length)) + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + attributedText.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + attributedText.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + attributedText.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + attributedText.addAttribute(key, value: value, range: range) + } + } + }) + + self.textNode.attributedText = attributedText + } + + if text.string.contains("](") { + isUserInteractionEnabled = true + } + self.originalRemainingSeconds = timeout ?? (isUserInteractionEnabled ? 5 : 3) + + self.titleNode.visibility = animateEntities + self.titleNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + + self.textNode.maximumNumberOfLines = 5 + self.textNode.visibility = animateEntities + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + if let customUndoText = customUndoText { undoText = customUndoText displayUndo = true @@ -1431,7 +1569,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { self.isUserInteractionEnabled = false } - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal,. universalImage, .premiumPaywall, .peers, .messageTagged: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .universalWithEntities, .universalImage, .premiumPaywall, .peers, .messageTagged: if self.textNode.tapAttributeAction != nil || displayUndo { self.isUserInteractionEnabled = true } else {