Emoji in chat folders

This commit is contained in:
Isaac 2024-12-25 00:17:19 +08:00
parent 1d28e11879
commit 4bed1703a2
59 changed files with 962 additions and 389 deletions

View File

@ -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";

View File

@ -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
}
}

View File

@ -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",

View File

@ -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)
}

View File

@ -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>()
peerView.set(context.account.viewTracker.peerView(peerId))
var onlineMemberCount: Signal<Int32?, NoError> = .single(nil)
var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil))
let recentOnlineSignal: Signal<Int32?, NoError> = 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<Int32?, NoError> 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
) {

View File

@ -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()
}
}

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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() {

View File

@ -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<Empty>()
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 {

View File

@ -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
))
}

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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",

View File

@ -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<Empty>?
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<Void, NoError>?, (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)

View File

@ -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
})

View File

@ -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)

View File

@ -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))
}

View File

@ -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?

View File

@ -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):

View File

@ -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):

View File

@ -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 {

View File

@ -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)

View File

@ -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):

View File

@ -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) }

View File

@ -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

View File

@ -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]?

View File

@ -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?

View File

@ -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 {

View File

@ -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) {

View File

@ -273,9 +273,11 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
|> mapToSignal { result -> Signal<ChatFolderLinkContents, CheckChatFolderLinkError> 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<Cha
}
}
return ChatFolderLinkContents(localFilterId: nil, title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
let titleText: String
let titleEntities: [MessageTextEntity]
switch title {
case let .textWithEntities(text, entities):
titleText = text
titleEntities = messageTextEntitiesFromApiEntities(entities)
}
return ChatFolderLinkContents(localFilterId: nil, title: ChatFolderTitle(text: titleText, entities: titleEntities, enableAnimations: !disableTitleAnimation), peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
case let .chatlistInviteAlready(filterId, missingPeers, alreadyPeers, chats, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
var memberCounts: [PeerId: Int] = [:]

View File

@ -22,6 +22,7 @@ swift_library(
"//submodules/WallpaperBackgroundNode",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
],
visibility = [
"//visibility:public",

View File

@ -11,6 +11,7 @@ import Markdown
import WallpaperBackgroundNode
import EmojiStatusComponent
import TelegramPresentationData
import TextNodeWithEntities
final class BlurredRoundedRectangle: Component {
let color: UIColor
@ -1210,21 +1211,42 @@ public final class ChatOverscrollControl: CombinedComponent {
}
public final class ChatInputPanelOverscrollNode: ASDisplayNode {
public let text: (String, [(Int, NSRange)])
public let text: NSAttributedString
public let priority: Int
private let titleNode: ImmediateTextNode
private let titleNode: ImmediateTextNodeWithEntities
public init(text: (String, [(Int, NSRange)]), color: UIColor, priority: Int) {
public init(context: AccountContext, text: NSAttributedString, color: UIColor, priority: Int) {
self.text = text
self.priority = priority
self.titleNode = ImmediateTextNode()
self.titleNode = ImmediateTextNodeWithEntities()
super.init()
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: color)
let bold = MarkdownAttributeSet(font: Font.bold(14.0), textColor: color)
self.titleNode.attributedText = addAttributesToStringWithRanges(text, body: body, argumentAttributes: [0: bold])
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: color, 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.bold(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.titleNode.attributedText = attributedText
self.titleNode.visibility = true
self.titleNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: color.withMultipliedAlpha(0.1),
attemptSynchronous: true
)
self.addSubnode(self.titleNode)
}

View File

@ -35,6 +35,7 @@ swift_library(
"//submodules/PremiumUI",
"//submodules/QrCodeUI",
"//submodules/InviteLinksUI",
"//submodules/Components/MultilineTextWithEntitiesComponent",
],
visibility = [
"//visibility:public",

View File

@ -7,6 +7,7 @@ import AccountContext
import MultilineTextComponent
import TelegramPresentationData
import TelegramCore
import MultilineTextWithEntitiesComponent
final class BadgeComponent: Component {
let fillColor: UIColor
@ -84,17 +85,20 @@ final class BadgeComponent: Component {
}
final class ChatFolderLinkHeaderComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let title: ChatFolderTitle
let badge: String?
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
title: ChatFolderTitle,
badge: String?
) {
self.context = context
self.theme = theme
self.strings = strings
self.title = title
@ -213,10 +217,16 @@ final class ChatFolderLinkHeaderComponent: Component {
}
contentWidth += spacing
//TODO:release
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: component.title.text, font: Font.semibold(17.0), color: component.theme.list.itemAccentColor)),
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
text: .plain(component.title.attributedString(font: Font.semibold(17.0), textColor: component.theme.list.itemAccentColor)),
manualVisibilityControl: false
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)

View File

@ -10,6 +10,7 @@ import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import SolidRoundedButtonComponent
import PresentationDataUtils
import Markdown
@ -437,6 +438,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
let topIconSize = self.topIcon.update(
transition: contentTransition,
component: AnyComponent(ChatFolderLinkHeaderComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
title: component.linkContents?.title ?? ChatFolderTitle(text: "Folder", entities: [], enableAnimations: true),
@ -457,37 +459,53 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
contentHeight += topIconSize.height
contentHeight += 20.0
let text: String
let text: NSAttributedString
if case .linkList = component.subject {
text = environment.strings.FolderLinkPreview_TextLinkList
text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextLinkList)
} else if let linkContents = component.linkContents {
if case .remove = component.subject {
text = environment.strings.FolderLinkPreview_TextRemoveFolder
text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextRemoveFolder, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor)
} else if allChatsAdded {
text = environment.strings.FolderLinkPreview_TextAllAdded
text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextAllAdded, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor)
} else if linkContents.localFilterId == nil {
text = environment.strings.FolderLinkPreview_TextAddFolder
} else {
text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextAddFolder, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor)
} else if let title = linkContents.title {
let chatCountString: String = environment.strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount))
//TODO:release
text = environment.strings.FolderLinkPreview_TextAddChats(chatCountString, linkContents.title?.text ?? "").string
let textValue = NSMutableAttributedString(string: environment.strings.FolderLinkPreview_TextAddChatsV2)
textValue.addAttributes([
.font: Font.regular(15.0),
.foregroundColor: environment.theme.list.freeTextColor
], range: NSRange(location: 0, length: textValue.length))
let folderRange = (textValue.string as NSString).range(of: "{folder}")
if folderRange.location != NSNotFound {
textValue.replaceCharacters(in: folderRange, with: "")
textValue.insert(title.attributedString(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), at: folderRange.location)
}
let chatsRange = (textValue.string as NSString).range(of: "{chats}")
if chatsRange.location != NSNotFound {
textValue.replaceCharacters(in: chatsRange, with: "")
textValue.insert(NSAttributedString(string: chatCountString, font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), at: chatsRange.location)
}
text = textValue
} else {
text = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor)
}
} else {
text = " "
text = NSAttributedString(string: " ")
}
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor)
let descriptionTextSize = self.descriptionText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .markdown(text: text, attributes: MarkdownAttributes(
body: body,
bold: bold,
link: body,
linkAttribute: { _ in nil }
)),
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: environment.theme.list.freeTextColor.withMultipliedAlpha(0.1),
text: .plain(text),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
@ -982,8 +1000,12 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(self.selectedItems)))
disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId]))
//TODO:release
let folderTitle = linkContents.title?.text ?? ""
let folderTitle: ChatFolderTitle
if let title = linkContents.title {
folderTitle = title
} else {
folderTitle = ChatFolderTitle(text: "", entities: [], enableAnimations: true)
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
@ -1015,11 +1037,18 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
}
}
let undoText = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastLeftTitleV2)
let folderRange = (undoText.string as NSString).range(of: "{folder}")
if folderRange.location != NSNotFound {
undoText.replaceCharacters(in: folderRange, with: "")
undoText.insert(folderTitle.rawAttributedString, at: folderRange.location)
}
let context = component.context
let selectedItems = self.selectedItems
let undoOverlayController = UndoOverlayController(
presentationData: presentationData,
content: .removedChat(title: presentationData.strings.FolderLinkPreview_ToastLeftTitle(folderTitle).string, text: additionalText),
content: .removedChat(context: component.context, title: undoText, text: additionalText),
elevatedLayout: false,
action: { value in
if case .commit = value {
@ -1112,8 +1141,14 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
}
if isUpdates {
//TODO:release
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitle(result.title.text).string, text: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2)
let folderRange = (titleString.string as NSString).range(of: "{folder}")
if folderRange.location != NSNotFound {
titleString.replaceCharacters(in: folderRange, with: "")
titleString.insert(result.title.rawAttributedString, at: folderRange.location)
}
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
} else if result.newChatCount != 0 {
let animationBackgroundColor: UIColor
if presentationData.theme.overallDarkAppearance {
@ -1121,8 +1156,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
} else {
animationBackgroundColor = UIColor(rgb: 0x474747)
}
//TODO:release
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title.text).string, text: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2)
let folderRange = (titleString.string as NSString).range(of: "{folder}")
if folderRange.location != NSNotFound {
titleString.replaceCharacters(in: folderRange, with: "")
titleString.insert(result.title.rawAttributedString, at: folderRange.location)
}
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
} else {
let animationBackgroundColor: UIColor
if presentationData.theme.overallDarkAppearance {
@ -1130,8 +1172,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
} else {
animationBackgroundColor = UIColor(rgb: 0x474747)
}
//TODO:release
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title.text).string, text: "", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitleV2)
let folderRange = (titleString.string as NSString).range(of: "{folder}")
if folderRange.location != NSNotFound {
titleString.replaceCharacters(in: folderRange, with: "")
titleString.insert(result.title.rawAttributedString, at: folderRange.location)
}
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: ""), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
}
})
}
@ -1310,7 +1359,6 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
})
let navigationController = controller.navigationController
//TODO:release
controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in
(navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root))
}))

View File

@ -90,7 +90,7 @@ public enum ChatTitleContent: Equatable {
case replies
}
case peer(peerView: PeerData, customTitle: String?, onlineMemberCount: Int32?, isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool)
case peer(peerView: PeerData, customTitle: String?, onlineMemberCount: (total: Int32?, recent: Int32?), isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool)
case replyThread(type: ReplyThreadType, count: Int)
case custom(String, String?, Bool)
@ -104,7 +104,7 @@ public enum ChatTitleContent: Equatable {
if customTitle != rhsCustomTitle {
return false
}
if onlineMemberCount != rhsOnlineMemberCount {
if onlineMemberCount.0 != rhsOnlineMemberCount.0 || onlineMemberCount.1 != rhsOnlineMemberCount.1 {
return false
}
if isScheduledMessages != rhsIsScheduledMessages {
@ -616,7 +616,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
if channel.flags.contains(.isForum), customTitle != nil {
let string = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic)
} else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
} else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = onlineMemberCount.total ?? cachedChannelData.participantsSummary.memberCount {
if memberCount == 0 {
let string: NSAttributedString
if case .group = channel.info {
@ -626,7 +626,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
}
state = .info(string, .generic)
} else {
if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 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))

View File

@ -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 {

View File

@ -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 {

View File

@ -1659,7 +1659,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
)
}
case let .group(groupId):
var onlineMemberCount: Signal<Int32?, NoError> = .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<Int32?, NoError> 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))), ")

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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<Int32?, NoError> = .single(nil)
var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil))
var hasScheduledMessages: Signal<Bool, NoError> = .single(false)
if peerId.namespace == Namespaces.Peer.CloudChannel {
let recentOnlineSignal: Signal<Int32?, NoError> = 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<Int32?, NoError> 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<Int32?, NoError> = .single(nil)
var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil))
if peerId.namespace == Namespaces.Peer.CloudChannel {
let recentOnlineSignal: Signal<Int32?, NoError> = 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<Int32?, NoError> 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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<Int32, NoError> {
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<PeerId>?
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))
}))
}
})

View File

@ -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 {

View File

@ -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?)

View File

@ -46,7 +46,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
private var stickerSourceSize: CGSize?
private var stickerOffset: CGPoint?
private var emojiStatus: ComponentView<Empty>?
private let titleNode: ImmediateTextNode
private let titleNode: ImmediateTextNodeWithEntities
private let textNode: ImmediateTextNodeWithEntities
private var textComponent: ComponentView<Empty>?
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 {