From 31d56bc12ebc449127f4fa982566409bf4efc571 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sun, 21 Aug 2022 18:39:51 +0300 Subject: [PATCH 01/11] Update API --- .../ContextControllerActionsStackNode.swift | 39 +++++++--- .../PeerAllowedReactionListController.swift | 54 ++++++++----- submodules/TelegramApi/Sources/Api0.swift | 9 ++- submodules/TelegramApi/Sources/Api29.swift | 27 +++++-- submodules/TelegramApi/Sources/Api3.swift | 32 +++----- submodules/TelegramApi/Sources/Api4.swift | 74 ++++++++++++++++++ .../Sources/State/MessageReactions.swift | 26 ++++--- .../SyncCore/SyncCore_CachedChannelData.swift | 32 ++++---- .../SyncCore/SyncCore_CachedGroupData.swift | 76 ++++++++++++++----- .../TelegramEngine/Data/PeersData.swift | 23 +++++- .../Peers/TelegramEnginePeers.swift | 2 +- .../Peers/UpdateCachedPeerData.swift | 32 +++++++- .../TelegramUI/Sources/ChatController.swift | 16 ++-- .../Sources/PeerInfo/PeerInfoScreen.swift | 48 ++++++++---- 14 files changed, 360 insertions(+), 130 deletions(-) diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 538e4ea2cf..0f26d0e7b8 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -1156,6 +1156,11 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let animateAppearingContainers = transition.isAnimated && !self.dismissingItemContainers.isEmpty + struct TipLayout { + var tipNode: ASDisplayNode + var tipHeight: CGFloat + } + struct ItemLayout { var size: CGSize var apparentHeight: CGFloat @@ -1163,6 +1168,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { var alphaTransitionFraction: CGFloat var itemTransition: ContainedViewLayoutTransition var animateAppearingContainer: Bool + var tip: TipLayout? } var topItemSize = CGSize() @@ -1204,13 +1210,19 @@ final class ContextControllerActionsStackNode: ASDisplayNode { topItemSize = itemSize.size } + var tip: TipLayout? + if let (tipNode, tipHeight) = itemContainer.updateTip(presentationData: presentationData, width: itemSize.size.width, transition: itemContainerTransition) { + tip = TipLayout(tipNode: tipNode, tipHeight: tipHeight) + } + itemLayouts.append(ItemLayout( size: itemSize.size, apparentHeight: itemSize.apparentHeight, transitionFraction: transitionFraction, alphaTransitionFraction: alphaTransitionFraction, itemTransition: itemContainerTransition, - animateAppearingContainer: animateAppearingContainer + animateAppearingContainer: animateAppearingContainer, + tip: tip )) } @@ -1232,6 +1244,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight))) + let previousNavigationContainerFrame = self.navigationContainer.frame transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true) self.navigationContainer.update(presentationData: presentationData, presentation: presentation, size: navigationContainerFrame.size, transition: transition) @@ -1258,20 +1271,28 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.itemContainers[i].updateDimNode(presentationData: presentationData, size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.size.height), transitionFraction: itemLayouts[i].alphaTransitionFraction, transition: transition) - if let (tipNode, tipHeight) = self.itemContainers[i].updateTip(presentationData: presentationData, width: itemLayouts[i].size.width, transition: transition) { - var tipTransition = transition - if tipNode.supernode == nil { - tipTransition = .immediate - self.addSubnode(tipNode) + if let tip = itemLayouts[i].tip { + let tipTransition = transition + var animateTipIn = false + if tip.tipNode.supernode == nil { + self.addSubnode(tip.tipNode) + animateTipIn = transition.isAnimated + tip.tipNode.frame = CGRect(origin: CGPoint(x: previousNavigationContainerFrame.minX, y: previousNavigationContainerFrame.maxY + tipSpacing), size: CGSize(width: itemLayouts[i].size.width, height: tip.tipHeight)) } let tipAlpha: CGFloat = itemLayouts[i].alphaTransitionFraction - tipTransition.updateFrame(node: tipNode, frame: CGRect(origin: CGPoint(x: navigationContainerFrame.minX, y: navigationContainerFrame.maxY + tipSpacing), size: CGSize(width: itemLayouts[i].size.width, height: tipHeight)), beginWithCurrentState: true) - tipTransition.updateAlpha(node: tipNode, alpha: tipAlpha, beginWithCurrentState: true) + tipTransition.updateFrame(node: tip.tipNode, frame: CGRect(origin: CGPoint(x: navigationContainerFrame.minX, y: navigationContainerFrame.maxY + tipSpacing), size: CGSize(width: itemLayouts[i].size.width, height: tip.tipHeight)), beginWithCurrentState: true) + + if animateTipIn { + tip.tipNode.alpha = tipAlpha + tip.tipNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + tipTransition.updateAlpha(node: tip.tipNode, alpha: tipAlpha, beginWithCurrentState: true) + } if i == self.itemContainers.count - 1 { - topItemSize.height += tipSpacing + tipHeight + topItemSize.height += tipSpacing + tip.tipHeight } } } diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift index 7de6a36d20..a4f1d2737a 100644 --- a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -323,23 +323,21 @@ public func peerAllowedReactionListController( updateState { state in var state = state - if allowedReactions == nil { - state.updatedMode = .all - if let availableReactions = availableReactions { - let updatedAllowedReactions = availableReactions.reactions.map { $0.value } - state.updatedAllowedReactions = Set(updatedAllowedReactions) - } - } else if let allowedReactions = allowedReactions, !allowedReactions.isEmpty { - if let availableReactions = availableReactions, Set(allowedReactions) == Set(availableReactions.reactions.map(\.value)) { + switch allowedReactions { + case .unknown: + break + case let .known(value): + switch value { + case .all: state.updatedMode = .all - } else { + state.updatedAllowedReactions = Set() + case let .limited(reactions): state.updatedMode = .some + state.updatedAllowedReactions = Set(reactions) + case .empty: + state.updatedMode = .empty + state.updatedAllowedReactions = Set() } - let updatedAllowedReactions = Set(allowedReactions) - state.updatedAllowedReactions = updatedAllowedReactions - } else { - state.updatedMode = .empty - state.updatedAllowedReactions = Set() } return state @@ -371,6 +369,12 @@ public func peerAllowedReactionListController( } case .some: updatedAllowedReactions.removeAll() + if let thumbsUp = availableReactions.reactions.first(where: { $0.value == .builtin("👍") }) { + updatedAllowedReactions.insert(thumbsUp.value) + } + if let thumbsDown = availableReactions.reactions.first(where: { $0.value == .builtin("👎") }) { + updatedAllowedReactions.insert(thumbsDown.value) + } case .empty: updatedAllowedReactions.removeAll() } @@ -443,12 +447,24 @@ public func peerAllowedReactionListController( let controller = ItemListController(context: context, state: signal) controller.willDisappear = { _ in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId)) - |> deliverOnMainQueue).start(next: { initialAllowedReactionList in - let initialAllowedReactions = initialAllowedReactionList.flatMap(Set.init) + |> deliverOnMainQueue).start(next: { initialAllowedReactions in + let state = stateValue.with({ $0 }) + guard let updatedMode = state.updatedMode, let updatedAllowedReactions = state.updatedAllowedReactions else { + return + } - let updatedAllowedReactions = stateValue.with({ $0 }).updatedAllowedReactions - if let updatedAllowedReactions = updatedAllowedReactions, initialAllowedReactions != updatedAllowedReactions { - let _ = context.engine.peers.updatePeerAllowedReactions(peerId: peerId, allowedReactions: Array(updatedAllowedReactions)).start() + let updatedValue: PeerAllowedReactions + switch updatedMode { + case .all: + updatedValue = .all + case .some: + updatedValue = .limited(Array(updatedAllowedReactions)) + case .empty: + updatedValue = .empty + } + + if initialAllowedReactions != .known(updatedValue) { + let _ = context.engine.peers.updatePeerAllowedReactions(peerId: peerId, allowedReactions: updatedValue).start() } }) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index f5efaafca9..6807723a58 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -151,8 +151,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1605510357] = { return Api.ChatAdminRights.parse_chatAdminRights($0) } dict[-219353309] = { return Api.ChatAdminWithInvites.parse_chatAdminWithInvites($0) } dict[-1626209256] = { return Api.ChatBannedRights.parse_chatBannedRights($0) } - dict[-362240487] = { return Api.ChatFull.parse_channelFull($0) } - dict[-779165146] = { return Api.ChatFull.parse_chatFull($0) } + dict[-231385849] = { return Api.ChatFull.parse_channelFull($0) } + dict[-908914376] = { return Api.ChatFull.parse_chatFull($0) } dict[806110401] = { return Api.ChatInvite.parse_chatInvite($0) } dict[1516793212] = { return Api.ChatInvite.parse_chatInviteAlready($0) } dict[1634294960] = { return Api.ChatInvite.parse_chatInvitePeek($0) } @@ -165,6 +165,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-2023500831] = { return Api.ChatParticipants.parse_chatParticipantsForbidden($0) } dict[476978193] = { return Api.ChatPhoto.parse_chatPhoto($0) } dict[935395612] = { return Api.ChatPhoto.parse_chatPhotoEmpty($0) } + dict[1385335754] = { return Api.ChatReactions.parse_chatReactionsAll($0) } + dict[-352570692] = { return Api.ChatReactions.parse_chatReactionsNone($0) } + dict[1713193015] = { return Api.ChatReactions.parse_chatReactionsSome($0) } dict[-1973130814] = { return Api.CodeSettings.parse_codeSettings($0) } dict[589653676] = { return Api.Config.parse_config($0) } dict[341499403] = { return Api.Contact.parse_contact($0) } @@ -1207,6 +1210,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.ChatPhoto: _1.serialize(buffer, boxed) + case let _1 as Api.ChatReactions: + _1.serialize(buffer, boxed) case let _1 as Api.CodeSettings: _1.serialize(buffer, boxed) case let _1 as Api.Config: diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 9157386322..8856435340 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -5321,6 +5321,23 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func reportReaction(peer: Api.InputPeer, id: Int32, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1631726152) + peer.serialize(buffer, true) + serializeInt32(id, buffer: buffer, boxed: false) + userId.serialize(buffer, true) + return (FunctionDescription(name: "messages.reportReaction", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func reportSpam(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5898,15 +5915,11 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func setChatAvailableReactions(peer: Api.InputPeer, availableReactions: [String]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func setChatAvailableReactions(peer: Api.InputPeer, availableReactions: Api.ChatReactions) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(335875750) + buffer.appendInt32(-21928079) peer.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(availableReactions.count)) - for item in availableReactions { - serializeString(item, buffer: buffer, boxed: false) - } + availableReactions.serialize(buffer, true) return (FunctionDescription(name: "messages.setChatAvailableReactions", parameters: [("peer", String(describing: peer)), ("availableReactions", String(describing: availableReactions))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index cad9b775f2..be885f4680 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -880,14 +880,14 @@ public extension Api { } public extension Api { enum ChatFull: TypeConstructorDescription { - case channelFull(flags: Int32, flags2: Int32, id: Int64, about: String, participantsCount: Int32?, adminsCount: Int32?, kickedCount: Int32?, bannedCount: Int32?, onlineCount: Int32?, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, chatPhoto: Api.Photo, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo], migratedFromChatId: Int64?, migratedFromMaxId: Int32?, pinnedMsgId: Int32?, stickerset: Api.StickerSet?, availableMinId: Int32?, folderId: Int32?, linkedChatId: Int64?, location: Api.ChannelLocation?, slowmodeSeconds: Int32?, slowmodeNextSendDate: Int32?, statsDc: Int32?, pts: Int32, call: Api.InputGroupCall?, ttlPeriod: Int32?, pendingSuggestions: [String]?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, defaultSendAs: Api.Peer?, availableReactions: [String]?) - case chatFull(flags: Int32, id: Int64, about: String, participants: Api.ChatParticipants, chatPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo]?, pinnedMsgId: Int32?, folderId: Int32?, call: Api.InputGroupCall?, ttlPeriod: Int32?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, availableReactions: [String]?) + case channelFull(flags: Int32, flags2: Int32, id: Int64, about: String, participantsCount: Int32?, adminsCount: Int32?, kickedCount: Int32?, bannedCount: Int32?, onlineCount: Int32?, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, chatPhoto: Api.Photo, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo], migratedFromChatId: Int64?, migratedFromMaxId: Int32?, pinnedMsgId: Int32?, stickerset: Api.StickerSet?, availableMinId: Int32?, folderId: Int32?, linkedChatId: Int64?, location: Api.ChannelLocation?, slowmodeSeconds: Int32?, slowmodeNextSendDate: Int32?, statsDc: Int32?, pts: Int32, call: Api.InputGroupCall?, ttlPeriod: Int32?, pendingSuggestions: [String]?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, defaultSendAs: Api.Peer?, availableReactions: Api.ChatReactions?) + case chatFull(flags: Int32, id: Int64, about: String, participants: Api.ChatParticipants, chatPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo]?, pinnedMsgId: Int32?, folderId: Int32?, call: Api.InputGroupCall?, ttlPeriod: Int32?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, availableReactions: Api.ChatReactions?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .channelFull(let flags, let flags2, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs, let availableReactions): if boxed { - buffer.appendInt32(-362240487) + buffer.appendInt32(-231385849) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -937,15 +937,11 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) }} if Int(flags) & Int(1 << 29) != 0 {defaultSendAs!.serialize(buffer, true)} - if Int(flags) & Int(1 << 30) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(availableReactions!.count)) - for item in availableReactions! { - serializeString(item, buffer: buffer, boxed: false) - }} + if Int(flags) & Int(1 << 30) != 0 {availableReactions!.serialize(buffer, true)} break case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call, let ttlPeriod, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let availableReactions): if boxed { - buffer.appendInt32(-779165146) + buffer.appendInt32(-908914376) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -971,11 +967,7 @@ public extension Api { for item in recentRequesters! { serializeInt64(item, buffer: buffer, boxed: false) }} - if Int(flags) & Int(1 << 18) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(availableReactions!.count)) - for item in availableReactions! { - serializeString(item, buffer: buffer, boxed: false) - }} + if Int(flags) & Int(1 << 18) != 0 {availableReactions!.serialize(buffer, true)} break } } @@ -1084,9 +1076,9 @@ public extension Api { if Int(_1!) & Int(1 << 29) != 0 {if let signature = reader.readInt32() { _36 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _37: [String]? - if Int(_1!) & Int(1 << 30) != 0 {if let _ = reader.readInt32() { - _37 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + var _37: Api.ChatReactions? + if Int(_1!) & Int(1 << 30) != 0 {if let signature = reader.readInt32() { + _37 = Api.parse(reader, signature: signature) as? Api.ChatReactions } } let _c1 = _1 != nil let _c2 = _2 != nil @@ -1181,9 +1173,9 @@ public extension Api { if Int(_1!) & Int(1 << 17) != 0 {if let _ = reader.readInt32() { _16 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } - var _17: [String]? - if Int(_1!) & Int(1 << 18) != 0 {if let _ = reader.readInt32() { - _17 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + var _17: Api.ChatReactions? + if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() { + _17 = Api.parse(reader, signature: signature) as? Api.ChatReactions } } let _c1 = _1 != nil let _c2 = _2 != nil diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 36e39fc393..a382f35afc 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -318,6 +318,80 @@ public extension Api { } } +public extension Api { + enum ChatReactions: TypeConstructorDescription { + case chatReactionsAll(flags: Int32) + case chatReactionsNone + case chatReactionsSome(reactions: [Api.Reaction]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .chatReactionsAll(let flags): + if boxed { + buffer.appendInt32(1385335754) + } + serializeInt32(flags, buffer: buffer, boxed: false) + break + case .chatReactionsNone: + if boxed { + buffer.appendInt32(-352570692) + } + + break + case .chatReactionsSome(let reactions): + if boxed { + buffer.appendInt32(1713193015) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .chatReactionsAll(let flags): + return ("chatReactionsAll", [("flags", String(describing: flags))]) + case .chatReactionsNone: + return ("chatReactionsNone", []) + case .chatReactionsSome(let reactions): + return ("chatReactionsSome", [("reactions", String(describing: reactions))]) + } + } + + public static func parse_chatReactionsAll(_ reader: BufferReader) -> ChatReactions? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.ChatReactions.chatReactionsAll(flags: _1!) + } + else { + return nil + } + } + public static func parse_chatReactionsNone(_ reader: BufferReader) -> ChatReactions? { + return Api.ChatReactions.chatReactionsNone + } + public static func parse_chatReactionsSome(_ reader: BufferReader) -> ChatReactions? { + var _1: [Api.Reaction]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.ChatReactions.chatReactionsSome(reactions: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum CodeSettings: TypeConstructorDescription { case codeSettings(flags: Int32, logoutTokens: [Buffer]?) diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index cd50840a64..6af7bd12b4 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -558,7 +558,7 @@ public enum UpdatePeerAllowedReactionsError { case generic } -func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allowedReactions: [MessageReaction.Reaction]) -> Signal { +func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allowedReactions: PeerAllowedReactions) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } @@ -567,14 +567,18 @@ func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allo guard let inputPeer = inputPeer else { return .fail(.generic) } - return account.network.request(Api.functions.messages.setChatAvailableReactions(peer: inputPeer, availableReactions: allowedReactions.compactMap { item -> String? in - switch item { - case let .builtin(value): - return value - case .custom: - return nil - } - })) + + let mappedReactions: Api.ChatReactions + switch allowedReactions { + case .all: + mappedReactions = .chatReactionsAll(flags: 0) + case let .limited(array): + mappedReactions = .chatReactionsSome(reactions: array.map(\.apiReaction)) + case .empty: + mappedReactions = .chatReactionsNone + } + + return account.network.request(Api.functions.messages.setChatAvailableReactions(peer: inputPeer, availableReactions: mappedReactions)) |> mapError { _ -> UpdatePeerAllowedReactionsError in return .generic } @@ -584,9 +588,9 @@ func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allo return account.postbox.transaction { transaction -> Void in transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in if let current = current as? CachedChannelData { - return current.withUpdatedAllowedReactions(allowedReactions) + return current.withUpdatedAllowedReactions(.known(allowedReactions)) } else if let current = current as? CachedGroupData { - return current.withUpdatedAllowedReactions(allowedReactions) + return current.withUpdatedAllowedReactions(.known(allowedReactions)) } else { return current } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 85c823bd45..35c772d39c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -238,7 +238,7 @@ public final class CachedChannelData: CachedPeerData { public let themeEmoticon: String? public let inviteRequestsPending: Int32? public let sendAsPeerId: PeerId? - public let allowedReactions: [MessageReaction.Reaction]? + public let allowedReactions: EnginePeerCachedInfoItem public let peerIds: Set public let messageIds: Set @@ -276,7 +276,7 @@ public final class CachedChannelData: CachedPeerData { self.themeEmoticon = nil self.inviteRequestsPending = nil self.sendAsPeerId = nil - self.allowedReactions = nil + self.allowedReactions = .unknown } public init( @@ -307,7 +307,7 @@ public final class CachedChannelData: CachedPeerData { themeEmoticon: String?, inviteRequestsPending: Int32?, sendAsPeerId: PeerId?, - allowedReactions: [MessageReaction.Reaction]? + allowedReactions: EnginePeerCachedInfoItem ) { self.isNotAccessible = isNotAccessible self.flags = flags @@ -471,7 +471,7 @@ public final class CachedChannelData: CachedPeerData { return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions) } - public func withUpdatedAllowedReactions(_ allowedReactions: [MessageReaction.Reaction]?) -> CachedChannelData { + public func withUpdatedAllowedReactions(_ allowedReactions: EnginePeerCachedInfoItem) -> CachedChannelData { return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions) } @@ -562,10 +562,12 @@ public final class CachedChannelData: CachedPeerData { self.sendAsPeerId = decoder.decodeOptionalInt64ForKey("sendAsPeerId").flatMap(PeerId.init) - if let allowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") { - self.allowedReactions = allowedReactions.map(MessageReaction.Reaction.builtin) + if let legacyAllowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") { + self.allowedReactions = .known(.limited(legacyAllowedReactions.map(MessageReaction.Reaction.builtin))) + } else if let allowedReactions = decoder.decode(PeerAllowedReactions.self, forKey: "allowedReactionSet") { + self.allowedReactions = .known(allowedReactions) } else { - self.allowedReactions = nil + self.allowedReactions = .unknown } if case let .known(linkedDiscussionPeerIdValue) = self.linkedDiscussionPeerId { @@ -712,17 +714,11 @@ public final class CachedChannelData: CachedPeerData { encoder.encodeNil(forKey: "sendAsPeerId") } - if let allowedReactions = self.allowedReactions { - encoder.encodeStringArray(allowedReactions.compactMap { item -> String? in - switch item { - case let .builtin(value): - return value - case .custom: - return nil - } - }, forKey: "allowedReactions") - } else { - encoder.encodeNil(forKey: "allowedReactions") + switch self.allowedReactions { + case .unknown: + encoder.encodeNil(forKey: "allowedReactionSet") + case let .known(value): + encoder.encode(value, forKey: "allowedReactionSet") } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift index 5c1862a2e6..1095bf9a41 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift @@ -39,6 +39,49 @@ public struct CachedGroupFlags: OptionSet { public static let canChangeUsername = CachedGroupFlags(rawValue: 1 << 0) } +public enum PeerAllowedReactions: Equatable, Codable { + private enum Discriminant: Int32 { + case all = 0 + case limited = 1 + case empty = 2 + } + + case all + case limited([MessageReaction.Reaction]) + case empty + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + let discriminant = try container.decode(Int32.self, forKey: "_d") + switch discriminant { + case Discriminant.all.rawValue: + self = .all + case Discriminant.limited.rawValue: + self = .limited(try container.decode([MessageReaction.Reaction].self, forKey: "r")) + case Discriminant.empty.rawValue: + self = .empty + default: + assertionFailure() + self = .all + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + switch self { + case .all: + try container.encode(Discriminant.all.rawValue, forKey: "_d") + case let .limited(reactions): + try container.encode(Discriminant.limited.rawValue, forKey: "_d") + try container.encode(reactions, forKey: "r") + case .empty: + try container.encode(Discriminant.empty.rawValue, forKey: "_d") + } + } +} + public final class CachedGroupData: CachedPeerData { public let participants: CachedGroupParticipants? public let exportedInvitation: ExportedInvitation? @@ -55,7 +98,8 @@ public final class CachedGroupData: CachedPeerData { public let callJoinPeerId: PeerId? public let themeEmoticon: String? public let inviteRequestsPending: Int32? - public let allowedReactions: [MessageReaction.Reaction]? + + public let allowedReactions: EnginePeerCachedInfoItem public let peerIds: Set public let messageIds: Set @@ -79,7 +123,7 @@ public final class CachedGroupData: CachedPeerData { self.callJoinPeerId = nil self.themeEmoticon = nil self.inviteRequestsPending = nil - self.allowedReactions = nil + self.allowedReactions = .unknown } public init( @@ -98,7 +142,7 @@ public final class CachedGroupData: CachedPeerData { callJoinPeerId: PeerId?, themeEmoticon: String?, inviteRequestsPending: Int32?, - allowedReactions: [MessageReaction.Reaction]? + allowedReactions: EnginePeerCachedInfoItem ) { self.participants = participants self.exportedInvitation = exportedInvitation @@ -180,10 +224,12 @@ public final class CachedGroupData: CachedPeerData { self.inviteRequestsPending = decoder.decodeOptionalInt32ForKey("irp") - if let allowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") { - self.allowedReactions = allowedReactions.map(MessageReaction.Reaction.builtin) + if let legacyAllowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") { + self.allowedReactions = .known(.limited(legacyAllowedReactions.map(MessageReaction.Reaction.builtin))) + } else if let allowedReactions = decoder.decode(PeerAllowedReactions.self, forKey: "allowedReactionSet") { + self.allowedReactions = .known(allowedReactions) } else { - self.allowedReactions = nil + self.allowedReactions = .unknown } var messageIds = Set() @@ -276,17 +322,11 @@ public final class CachedGroupData: CachedPeerData { encoder.encodeNil(forKey: "irp") } - if let allowedReactions = self.allowedReactions { - encoder.encodeStringArray(allowedReactions.compactMap { item -> String? in - switch item { - case let .builtin(value): - return value - case .custom: - return nil - } - }, forKey: "allowedReactions") - } else { - encoder.encodeNil(forKey: "allowedReactions") + switch self.allowedReactions { + case .unknown: + encoder.encodeNil(forKey: "allowedReactionSet") + case let .known(value): + encoder.encode(value, forKey: "allowedReactionSet") } } @@ -370,7 +410,7 @@ public final class CachedGroupData: CachedPeerData { return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, allowedReactions: self.allowedReactions) } - public func withUpdatedAllowedReactions(_ allowedReactions: [MessageReaction.Reaction]?) -> CachedGroupData { + public func withUpdatedAllowedReactions(_ allowedReactions: EnginePeerCachedInfoItem) -> CachedGroupData { return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: allowedReactions) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 21ed971f09..047fefea4e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -9,6 +9,25 @@ public enum EnginePeerCachedInfoItem { case unknown } +extension EnginePeerCachedInfoItem: Equatable where T: Equatable { + public static func ==(lhs: EnginePeerCachedInfoItem, rhs: EnginePeerCachedInfoItem) -> Bool { + switch lhs { + case let .known(value): + if case .known(value) = rhs { + return true + } else { + return false + } + case .unknown: + if case .unknown = rhs { + return true + } else { + return false + } + } + } +} + public enum EngineChannelParticipant: Equatable { case creator(id: EnginePeer.Id, adminInfo: ChannelParticipantAdminInfo?, rank: String?) case member(id: EnginePeer.Id, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?) @@ -452,7 +471,7 @@ public extension TelegramEngine.EngineData.Item { } public struct AllowedReactions: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { - public typealias Result = [MessageReaction.Reaction]? + public typealias Result = EnginePeerCachedInfoItem fileprivate var id: EnginePeer.Id public var mapKey: EnginePeer.Id { @@ -476,7 +495,7 @@ public extension TelegramEngine.EngineData.Item { } else if let cachedData = view.cachedPeerData as? CachedGroupData { return cachedData.allowedReactions } else { - return nil + return .unknown } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 05d601451a..741e3446da 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -694,7 +694,7 @@ public extension TelegramEngine { return _internal_updatePeerSendAsPeer(account: self.account, peerId: peerId, sendAs: sendAs) } - public func updatePeerAllowedReactions(peerId: PeerId, allowedReactions: [MessageReaction.Reaction]) -> Signal { + public func updatePeerAllowedReactions(peerId: PeerId, allowedReactions: PeerAllowedReactions) -> Signal { return _internal_updatePeerAllowedReactions(account: account, peerId: peerId, allowedReactions: allowedReactions) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 2e123e9ec3..7cfe3db726 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -376,6 +376,20 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } } + let mappedAllowedReactions: PeerAllowedReactions + if let allowedReactions = allowedReactions { + switch allowedReactions { + case .chatReactionsAll: + mappedAllowedReactions = .all + case let .chatReactionsSome(reactions): + mappedAllowedReactions = .limited(reactions.compactMap(MessageReaction.Reaction.init(apiReaction:))) + case .chatReactionsNone: + mappedAllowedReactions = .empty + } + } else { + mappedAllowedReactions = .empty + } + return previous.withUpdatedParticipants(participants) .withUpdatedExportedInvitation(exportedInvitation) .withUpdatedBotInfos(botInfos) @@ -389,7 +403,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedCallJoinPeerId(groupCallDefaultJoinAs?.peerId) .withUpdatedThemeEmoticon(chatFullThemeEmoticon) .withUpdatedInviteRequestsPending(chatFullRequestsPending) - .withUpdatedAllowedReactions(allowedReactions.flatMap({ $0.map(MessageReaction.Reaction.builtin) }) ?? []) + .withUpdatedAllowedReactions(.known(mappedAllowedReactions)) }) case .channelFull: break @@ -602,6 +616,20 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } } + let mappedAllowedReactions: PeerAllowedReactions + if let allowedReactions = allowedReactions { + switch allowedReactions { + case .chatReactionsAll: + mappedAllowedReactions = .all + case let .chatReactionsSome(reactions): + mappedAllowedReactions = .limited(reactions.compactMap(MessageReaction.Reaction.init(apiReaction:))) + case .chatReactionsNone: + mappedAllowedReactions = .empty + } + } else { + mappedAllowedReactions = .empty + } + return previous.withUpdatedFlags(channelFlags) .withUpdatedAbout(about) .withUpdatedParticipantsSummary(CachedChannelParticipantsSummary(memberCount: participantsCount, adminCount: adminsCount, bannedCount: bannedCount, kickedCount: kickedCount)) @@ -627,7 +655,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedThemeEmoticon(themeEmoticon) .withUpdatedInviteRequestsPending(requestsPending) .withUpdatedSendAsPeerId(sendAsPeerId) - .withUpdatedAllowedReactions(allowedReactions.flatMap({ $0.map(MessageReaction.Reaction.builtin) }) ?? []) + .withUpdatedAllowedReactions(.known(mappedAllowedReactions)) }) if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6d9cda265c..36a1f16c82 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -16963,12 +16963,18 @@ func peerAllowedReactions(context: AccountContext, peerId: PeerId) -> Signal map { peer, allowedReactions -> AllowedReactions? in - if let allowedReactions = allowedReactions { - return .set(Set(allowedReactions)) - } else if case .user = peer { - return .all - } else { + switch allowedReactions { + case .unknown: return nil + case let .known(value): + switch value { + case .all: + return .all + case let .limited(reactions): + return .set(Set(reactions)) + case .empty: + return .set(Set()) + } } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 52dfe58514..f023e5fdf4 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1277,11 +1277,15 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { let label: String - if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { - if allowedReactions.isEmpty { + if let cachedData = data.cachedData as? CachedChannelData, case let .known(allowedReactions) = cachedData.allowedReactions { + switch allowedReactions { + case .all: + //TODO:localize + label = "Enabled" + case .empty: label = presentationData.strings.PeerInfo_ReactionsDisabled - } else { - label = "\(allowedReactions.count)" + case let .limited(reactions): + label = "\(reactions.count)" } } else { label = "" @@ -1439,11 +1443,15 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { let label: String - if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { - if allowedReactions.isEmpty { + if let cachedData = data.cachedData as? CachedChannelData, case let .known(allowedReactions) = cachedData.allowedReactions { + switch allowedReactions { + case .all: + //TODO:localize + label = "Enabled" + case .empty: label = presentationData.strings.PeerInfo_ReactionsDisabled - } else { - label = "\(allowedReactions.count)" + case let .limited(reactions): + label = "\(reactions.count)" } } else { label = "" @@ -1461,11 +1469,15 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr } else { if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { let label: String - if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { - if allowedReactions.isEmpty { + if let cachedData = data.cachedData as? CachedChannelData, case let .known(allowedReactions) = cachedData.allowedReactions { + switch allowedReactions { + case .all: + //TODO:localize + label = "Enabled" + case .empty: label = presentationData.strings.PeerInfo_ReactionsDisabled - } else { - label = "\(allowedReactions.count)" + case let .limited(reactions): + label = "\(reactions.count)" } } else { label = "" @@ -1571,11 +1583,15 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr do { let label: String - if let cachedData = data.cachedData as? CachedGroupData, let allowedReactions = cachedData.allowedReactions { - if allowedReactions.isEmpty { + if let cachedData = data.cachedData as? CachedGroupData, case let .known(allowedReactions) = cachedData.allowedReactions { + switch allowedReactions { + case .all: + //TODO:localize + label = "Enabled" + case .empty: label = presentationData.strings.PeerInfo_ReactionsDisabled - } else { - label = "\(allowedReactions.count)" + case let .limited(reactions): + label = "\(reactions.count)" } } else { label = "" From 30c018c4a299f20b51e8fdca36ca655eb6ec8eb7 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sun, 21 Aug 2022 19:39:22 +0300 Subject: [PATCH 02/11] Fix context menu tip layout --- .../ReactionListContextMenuContent.swift | 25 +++++++-- .../ContextControllerActionsStackNode.swift | 56 ++++++++++++++++--- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 3ce490102c..68a7782cbc 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -658,6 +658,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private let scrollNode: ASScrollNode private var ignoreScrolling: Bool = false private var animateIn: Bool = false + private var bottomScrollInset: CGFloat = 0.0 private var presentationData: PresentationData? private var currentSize: CGSize? @@ -846,7 +847,18 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } - func update(presentationData: PresentationData, constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (height: CGFloat, apparentHeight: CGFloat) { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var extendedScrollNodeFrame = self.scrollNode.frame + extendedScrollNodeFrame.size.height += self.bottomScrollInset + + if extendedScrollNodeFrame.contains(point) { + return self.scrollNode.view.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) + } + + return super.hitTest(point, with: event) + } + + func update(presentationData: PresentationData, constrainedSize: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (height: CGFloat, apparentHeight: CGFloat) { let itemHeight: CGFloat = 44.0 if self.presentationData?.theme !== presentationData.theme { @@ -895,8 +907,13 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.scrollNode.frame != CGRect(origin: CGPoint(), size: containerSize) { self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize) } - if self.scrollNode.view.contentSize != size { - self.scrollNode.view.contentSize = size + if self.scrollNode.view.contentInset.bottom != bottomInset { + self.scrollNode.view.contentInset.bottom = bottomInset + } + self.bottomScrollInset = bottomInset + let scrollContentSize = CGSize(width: size.width, height: size.height) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize } self.ignoreScrolling = false @@ -1216,7 +1233,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent tabTransition = .immediate } - let tabLayout = tabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: tabTransition) + let tabLayout = tabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), bottomInset: bottomInset, transition: tabTransition) tabLayouts[index] = tabLayout let currentFractionalTabIndex: CGFloat if let interactiveTransitionState = self.interactiveTransitionState { diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 0f26d0e7b8..71d3887b54 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -14,11 +14,14 @@ import AnimationCache import MultiAnimationRenderer public protocol ContextControllerActionsStackItemNode: ASDisplayNode { + var wantsFullWidth: Bool { get } + func update( presentationData: PresentationData, constrainedSize: CGSize, standardMinWidth: CGFloat, standardMaxWidth: CGFloat, + additionalBottomInset: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) @@ -411,6 +414,10 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack private var hapticFeedback: HapticFeedback? private var highlightedItemNode: Item? + var wantsFullWidth: Bool { + return false + } + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -507,6 +514,7 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack constrainedSize: CGSize, standardMinWidth: CGFloat, standardMaxWidth: CGFloat, + additionalBottomInset: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { var itemNodeLayouts: [(minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void)] = [] @@ -677,18 +685,23 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta self.addSubnode(self.contentNode) } + var wantsFullWidth: Bool { + return true + } + func update( presentationData: PresentationData, constrainedSize: CGSize, standardMinWidth: CGFloat, standardMaxWidth: CGFloat, + additionalBottomInset: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { let contentLayout = self.contentNode.update( presentationData: presentationData, constrainedWidth: constrainedSize.width, maxHeight: constrainedSize.height, - bottomInset: 0.0, + bottomInset: additionalBottomInset, transition: transition ) transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentLayout.cleanSize), beginWithCurrentState: true) @@ -925,6 +938,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { constrainedSize: CGSize, standardMinWidth: CGFloat, standardMaxWidth: CGFloat, + additionalBottomInset: CGFloat, transitionFraction: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { @@ -933,6 +947,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { constrainedSize: constrainedSize, standardMinWidth: standardMinWidth, standardMaxWidth: standardMaxWidth, + additionalBottomInset: additionalBottomInset, transition: transition ) @@ -1198,11 +1213,37 @@ final class ContextControllerActionsStackNode: ASDisplayNode { alphaTransitionFraction = 0.0 } + var tip: TipLayout? + + let itemContainerConstrainedSize: CGSize + let standardMinWidth: CGFloat + let standardMaxWidth: CGFloat + let additionalBottomInset: CGFloat + + if itemContainer.node.wantsFullWidth { + itemContainerConstrainedSize = CGSize(width: constrainedSize.width, height: itemConstrainedHeight) + standardMaxWidth = 240.0 + standardMinWidth = standardMaxWidth + + if let (tipNode, tipHeight) = itemContainer.updateTip(presentationData: presentationData, width: standardMaxWidth, transition: itemContainerTransition) { + tip = TipLayout(tipNode: tipNode, tipHeight: tipHeight) + additionalBottomInset = tipHeight + 10.0 + } else { + additionalBottomInset = 0.0 + } + } else { + itemContainerConstrainedSize = CGSize(width: constrainedSize.width, height: itemConstrainedHeight) + standardMinWidth = 220.0 + standardMaxWidth = 240.0 + additionalBottomInset = 0.0 + } + let itemSize = itemContainer.update( presentationData: presentationData, - constrainedSize: CGSize(width: constrainedSize.width, height: itemConstrainedHeight), - standardMinWidth: 220.0, - standardMaxWidth: 240.0, + constrainedSize: itemContainerConstrainedSize, + standardMinWidth: standardMinWidth, + standardMaxWidth: standardMaxWidth, + additionalBottomInset: additionalBottomInset, transitionFraction: alphaTransitionFraction, transition: itemContainerTransition ) @@ -1210,9 +1251,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode { topItemSize = itemSize.size } - var tip: TipLayout? - if let (tipNode, tipHeight) = itemContainer.updateTip(presentationData: presentationData, width: itemSize.size.width, transition: itemContainerTransition) { - tip = TipLayout(tipNode: tipNode, tipHeight: tipHeight) + if !itemContainer.node.wantsFullWidth { + if let (tipNode, tipHeight) = itemContainer.updateTip(presentationData: presentationData, width: itemSize.size.width, transition: itemContainerTransition) { + tip = TipLayout(tipNode: tipNode, tipHeight: tipHeight) + } } itemLayouts.append(ItemLayout( From 94dc335d044c0b257d09aefc682fc962b8fd4432 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sun, 21 Aug 2022 19:47:35 +0300 Subject: [PATCH 03/11] Fix reactions --- submodules/TelegramUI/Sources/ChatController.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 36a1f16c82..4f40c690bd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1645,12 +1645,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G messageAlreadyHasThisReaction = currentReactions.contains(where: { $0.value == chosenReaction }) } - guard let allowedReactions = allowedReactions else { - itemNode.openMessageContextMenu() - return - } - if removedReaction == nil { + guard let allowedReactions = allowedReactions else { + itemNode.openMessageContextMenu() + return + } + switch allowedReactions { case let .set(set): if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { @@ -16965,7 +16965,7 @@ func peerAllowedReactions(context: AccountContext, peerId: PeerId) -> Signal map { peer, allowedReactions -> AllowedReactions? in switch allowedReactions { case .unknown: - return nil + return .all case let .known(value): switch value { case .all: From e450862adfb92a9126031e796f3ca89bc4a8f67f Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 22 Aug 2022 02:20:05 +0300 Subject: [PATCH 04/11] Reaction improvements --- .../Sources/AccountContext.swift | 1 + ...tControllerExtractedPresentationNode.swift | 103 ++++++++++++++---- .../Sources/ReactionContextNode.swift | 64 +++++++++-- .../TelegramEngine/Peers/ReportPeer.swift | 22 ++++ .../Peers/TelegramEnginePeers.swift | 6 +- .../TelegramUI/Sources/ChatBotInfoItem.swift | 2 +- .../Sources/ChatButtonKeyboardInputNode.swift | 4 +- .../TelegramUI/Sources/ChatController.swift | 21 ++-- .../Sources/ChatControllerInteraction.swift | 4 +- .../ChatInterfaceStateContextMenus.swift | 4 +- .../ChatMessageAnimatedStickerItemNode.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 6 +- .../Sources/ChatMessageDateHeader.swift | 4 +- .../ChatMessageInstantVideoItemNode.swift | 2 +- .../Sources/ChatMessageItemView.swift | 2 +- .../ChatMessageWebpageBubbleContentNode.swift | 2 +- .../ChatPinnedMessageTitlePanelNode.swift | 2 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../Sources/DrawingStickersScreen.swift | 2 +- .../OverlayAudioPlayerControllerNode.swift | 2 +- .../PeerInfoGroupsInCommonPaneNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 85 ++++++++++----- .../Sources/SharedAccountContext.swift | 13 ++- .../Sources/TelegramRootController.swift | 2 +- 24 files changed, 264 insertions(+), 95 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 9821fd95de..bf2c51d228 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -467,6 +467,7 @@ public enum PeerInfoControllerMode { case calls(messages: [Message]) case nearbyPeer(distance: Int32) case group(PeerId) + case reaction(MessageId) } public enum ContactListActionItemInlineIconPosition { diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 859d8b0dad..4e56f650c0 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -179,7 +179,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private let dismissTapNode: ASDisplayNode private let dismissAccessibilityArea: AccessibilityAreaNode private let clippingNode: ASDisplayNode - private let scrollNode: ASScrollNode + private let scroller: UIScrollView + private let scrollNode: ASDisplayNode private var reactionContextNode: ReactionContextNode? private var reactionContextNodeIsAnimatingOut: Bool = false @@ -192,6 +193,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private var strings: PresentationStrings? + private enum OverscrollMode { + case unrestricted + case topOnly + case disabled + } + + private var overscrollMode: OverscrollMode = .unrestricted + init( getController: @escaping () -> ContextControllerProtocol?, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, @@ -215,13 +224,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true - self.scrollNode = ASScrollNode() - self.scrollNode.canCancelAllTouchesInViews = true - self.scrollNode.view.delaysContentTouches = false - self.scrollNode.view.showsVerticalScrollIndicator = false + self.scroller = UIScrollView() + self.scroller.canCancelContentTouches = true + self.scroller.delaysContentTouches = false + self.scroller.showsVerticalScrollIndicator = false if #available(iOS 11.0, *) { - self.scrollNode.view.contentInsetAdjustmentBehavior = .never + self.scroller.contentInsetAdjustmentBehavior = .never } + self.scroller.alwaysBounceVertical = true + + self.scrollNode = ASDisplayNode() + self.scrollNode.view.addGestureRecognizer(self.scroller.panGestureRecognizer) self.contentRectDebugNode = ASDisplayNode() self.contentRectDebugNode.isUserInteractionEnabled = false @@ -244,11 +257,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.scrollNode.addSubnode(self.dismissAccessibilityArea) self.scrollNode.addSubnode(self.actionsStackNode) - /*#if DEBUG - self.scrollNode.addSubnode(self.contentRectDebugNode) - #endif*/ - - self.scrollNode.view.delegate = self + self.scroller.delegate = self self.dismissTapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapGesture(_:)))) @@ -292,10 +301,65 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if let reactionContextNode = self.reactionContextNode, (reactionContextNode.isExpanded || !reactionContextNode.canBeExpanded) { + self.overscrollMode = .disabled + self.scroller.alwaysBounceVertical = false + } else { + if scrollView.contentSize.height > scrollView.bounds.height { + self.overscrollMode = .unrestricted + self.scroller.alwaysBounceVertical = true + } else { + if self.reactionContextNode != nil { + self.overscrollMode = .topOnly + self.scroller.alwaysBounceVertical = true + } else { + self.overscrollMode = .disabled + self.scroller.alwaysBounceVertical = false + } + } + } + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { + var adjustedBounds = scrollView.bounds + var topOverscroll: CGFloat = 0.0 + switch self.overscrollMode { + case .unrestricted: + if adjustedBounds.origin.y < 0.0 { + topOverscroll = -adjustedBounds.origin.y + } + case .disabled: + break + case .topOnly: + if scrollView.contentSize.height <= scrollView.bounds.height { + if adjustedBounds.origin.y > 0.0 { + adjustedBounds.origin.y = 0.0 + } else { + topOverscroll = -adjustedBounds.origin.y + } + } else { + if adjustedBounds.origin.y < 0.0 { + topOverscroll = -adjustedBounds.origin.y + } else if adjustedBounds.origin.y + adjustedBounds.height > scrollView.contentSize.height { + adjustedBounds.origin.y = scrollView.contentSize.height - adjustedBounds.height + } + } + } + self.scrollNode.bounds = adjustedBounds + if let reactionContextNode = self.reactionContextNode { - let isIntersectingContent = scrollView.contentOffset.y >= 10.0 + let isIntersectingContent = adjustedBounds.minY >= 10.0 reactionContextNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: .animated(duration: 0.25, curve: .easeInOut)) + + if !reactionContextNode.isExpanded && reactionContextNode.canBeExpanded { + if topOverscroll > 60.0 && self.scroller.isDragging { + self.scroller.panGestureRecognizer.state = .cancelled + reactionContextNode.expand() + } else { + reactionContextNode.updateExtension(distance: topOverscroll) + } + } } } @@ -341,7 +405,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } private func getCurrentScrollingState() -> CGFloat { - return self.scrollNode.view.contentOffset.y + return self.scrollNode.bounds.minY } private func getActionsStackPositionLock() -> CGFloat? { @@ -410,6 +474,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) { transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) + transition.updateFrame(view: self.scroller, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) } if let current = self.contentNode { @@ -684,16 +749,16 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } let contentSize = CGSize(width: layout.size.width, height: contentHeight) - if self.scrollNode.view.contentSize != contentSize { - let previousContentOffset = self.scrollNode.view.contentOffset - self.scrollNode.view.contentSize = contentSize + if self.scroller.contentSize != contentSize { + let previousContentOffset = self.scroller.contentOffset + self.scroller.contentSize = contentSize if let storedScrollingState = self.actionsStackNode.storedScrollingState { self.actionsStackNode.clearStoredScrollingState() - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: storedScrollingState) + self.scroller.contentOffset = CGPoint(x: 0.0, y: storedScrollingState) } if case .none = stateTransition, transition.isAnimated { - let contentOffset = self.scrollNode.view.contentOffset + let contentOffset = self.scroller.contentOffset transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: previousContentOffset.y - contentOffset.y) } } @@ -718,7 +783,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let duration: Double = 0.42 let springDamping: CGFloat = 104.0 - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: defaultScrollY) + self.scroller.contentOffset = CGPoint(x: 0.0, y: defaultScrollY) self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 2ed1bca108..1031daf415 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -112,7 +112,7 @@ private final class ExpandItemView: UIView { self.tintView.layer.cornerRadius = size.width / 2.0 if let image = self.arrowView.image { - transition.updateFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0)), size: image.size)) + transition.updateFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels(size.height - size.width + (size.width - image.size.height) / 2.0)), size: image.size)) } } } @@ -168,6 +168,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public private(set) var currentContentHeight: CGFloat = 46.0 public private(set) var isExpanded: Bool = false + public private(set) var canBeExpanded: Bool = false + + private var animateFromExtensionDistance: CGFloat = 0.0 + private var extensionDistance: CGFloat = 0.0 private var emojiContent: EmojiPagerContentComponent? private var emojiContentDisposable: Disposable? @@ -252,6 +256,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.contentContainer.view.addSubview(expandItemView) self.contentTintContainer.view.addSubview(expandItemView.tintView) + + self.canBeExpanded = true } else { self.expandItemView = nil } @@ -289,6 +295,16 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.backgroundNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: transition) } + public func updateExtension(distance: CGFloat) { + if self.extensionDistance != distance { + self.extensionDistance = distance + + if let (size, insets, anchorRect) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + } + } + } + private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (backgroundFrame: CGRect, visualBackgroundFrame: CGRect, isLeftAligned: Bool, cloudSourcePoint: CGFloat) { var contentSize = contentSize contentSize.width = max(46.0, contentSize.width) @@ -325,7 +341,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { cloudSourcePoint = max(rect.minX + 46.0 / 2.0, anchorRect.minX) } - let visualRect = rect + var visualRect = rect + visualRect.size.height += self.extensionDistance return (rect, visualRect, isLeftAligned, cloudSourcePoint) } @@ -478,7 +495,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } if let expandItemView = self.expandItemView { - let baseNextFrame = CGRect(origin: CGPoint(x: nextX + 3.0, y: containerHeight - contentHeight + floor((contentHeight - 30.0) / 2.0) + (self.isExpanded ? 46.0 : 0.0)), size: CGSize(width: 30.0, height: 30.0)) + let baseNextFrame = CGRect(origin: CGPoint(x: nextX + 3.0, y: containerHeight - contentHeight + floor((contentHeight - 30.0) / 2.0) + (self.isExpanded ? 46.0 : 0.0)), size: CGSize(width: 30.0, height: 30.0 + self.extensionDistance)) transition.updateFrame(view: expandItemView, frame: baseNextFrame) transition.updateFrame(view: expandItemView.tintView, frame: baseNextFrame) @@ -488,10 +505,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let currentMaskFrame = currentMaskFrame { let transition = maskTransition ?? transition - transition.updateFrame(node: self.leftBackgroundMaskNode, frame: CGRect(x: -1000.0 + currentMaskFrame.minX, y: 0.0, width: 1000.0, height: self.currentContentHeight)) - transition.updateFrame(node: self.rightBackgroundMaskNode, frame: CGRect(x: currentMaskFrame.maxX, y: 0.0, width: 1000.0, height: self.currentContentHeight)) + transition.updateFrame(node: self.leftBackgroundMaskNode, frame: CGRect(x: -1000.0 + currentMaskFrame.minX, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance)) + transition.updateFrame(node: self.rightBackgroundMaskNode, frame: CGRect(x: currentMaskFrame.maxX, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance)) } else { - self.leftBackgroundMaskNode.frame = CGRect(x: 0.0, y: 0.0, width: 1000.0, height: self.currentContentHeight) + self.leftBackgroundMaskNode.frame = CGRect(x: 0.0, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance) self.rightBackgroundMaskNode.frame = CGRect(origin: .zero, size: .zero) } @@ -569,12 +586,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.isLeftAligned = isLeftAligned self.visibleItemCount = itemCount + var scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: self.isExpanded ? 46.0 : 0.0), size: actualBackgroundFrame.size) + scrollFrame.origin.y += floorToScreenPixels(self.extensionDistance / 2.0) + transition.updateFrame(node: self.contentContainer, frame: visualBackgroundFrame, beginWithCurrentState: true) transition.updateFrame(node: self.contentTintContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: visualBackgroundFrame.size), beginWithCurrentState: true) transition.updateFrame(view: self.contentContainerMask, frame: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size), beginWithCurrentState: true) - transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isExpanded ? 46.0 : 0.0), size: actualBackgroundFrame.size), beginWithCurrentState: true) + transition.updateFrame(node: self.scrollNode, frame: scrollFrame, beginWithCurrentState: true) transition.updateFrame(node: self.previewingItemContainer, frame: visualBackgroundFrame, beginWithCurrentState: true) - self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: visualBackgroundFrame.size.height) + self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: scrollFrame.size.height) self.updateScrolling(transition: transition) @@ -607,9 +627,23 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { guard let strongSelf = self else { return } + strongSelf.emojiContent = emojiContent - if let (size, insets, anchorRect) = strongSelf.validLayout { - strongSelf.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + + if let reactionSelectionComponentHost = strongSelf.reactionSelectionComponentHost, let componentView = reactionSelectionComponentHost.view { + let _ = reactionSelectionComponentHost.update( + transition: .immediate, + component: AnyComponent(EmojiStatusSelectionComponent( + theme: strongSelf.presentationData.theme, + strings: strongSelf.presentationData.strings, + deviceMetrics: DeviceMetrics.iPhone13, + emojiContent: emojiContent, + backgroundColor: .clear, + separatorColor: strongSelf.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) + )), + environment: {}, + containerSize: CGSize(width: componentView.bounds.width, height: 300.0) + ) } }) } @@ -754,7 +788,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { componentTransition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) if animateIn { - transition.animatePositionAdditive(layer: componentView.layer, offset: CGPoint(x: 0.0, y: -46.0)) + transition.animatePositionAdditive(layer: componentView.layer, offset: CGPoint(x: 0.0, y: -46.0 + floorToScreenPixels(self.animateFromExtensionDistance / 2.0))) } } } @@ -1250,6 +1284,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + public func expand() { + self.animateFromExtensionDistance = self.extensionDistance + self.extensionDistance = 0.0 + self.currentContentHeight = 300.0 + self.isExpanded = true + self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring)) + } + public func highlightGestureMoved(location: CGPoint, hover: Bool) { let highlightedReaction = self.previewReaction(at: location)?.reaction if self.highlightedReaction != highlightedReaction { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift index 0777db7d45..b8a0a66f18 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift @@ -170,6 +170,28 @@ func _internal_reportPeerMessages(account: Account, messageIds: [MessageId], rea } |> switchToLatest } +func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: MessageId) -> Signal { + return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputUser)? in + guard let peer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { + return nil + } + guard let author = transaction.getPeer(authorId).flatMap(apiInputUser) else { + return nil + } + return (peer, author) + } + |> mapToSignal { inputData -> Signal in + guard let (inputPeer, inputUser) = inputData else { + return .complete() + } + return account.network.request(Api.functions.messages.reportReaction(peer: inputPeer, id: messageId.id, userId: inputUser)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues + } +} + func _internal_dismissPeerStatusOptions(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 741e3446da..cb5e667345 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -160,7 +160,11 @@ public extension TelegramEngine { } public func reportPeerMessages(messageIds: [MessageId], reason: ReportReason, message: String) -> Signal { - return _internal_reportPeerMessages(account: account, messageIds: messageIds, reason: reason, message: message) + return _internal_reportPeerMessages(account: self.account, messageIds: messageIds, reason: reason, message: message) + } + + public func reportPeerReaction(authorId: PeerId, messageId: MessageId) -> Signal { + return _internal_reportPeerReaction(account: self.account, authorId: authorId, messageId: messageId) } public func dismissPeerStatusOptions(peerId: PeerId) -> Signal { diff --git a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Sources/ChatBotInfoItem.swift index 67c3eb0d1a..e380b8dcd3 100644 --- a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Sources/ChatBotInfoItem.swift @@ -408,7 +408,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { case let .url(url, concealed): self.item?.controllerInteraction.openUrl(url, concealed, nil, nil) case let .peerMention(peerId, _): - self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil, peekData: nil), nil, nil) + self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil, peekData: nil), nil, false, nil) case let .textMention(name): self.item?.controllerInteraction.openPeerMention(name) case let .botCommand(command): diff --git a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift index 5e1898e644..753e2f949b 100644 --- a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift @@ -247,7 +247,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { peerId = message.id.peerId } if let botPeer = botPeer, let addressName = botPeer.addressName { - self.controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), subject: nil, peekData: nil), nil, nil) + self.controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), subject: nil, peekData: nil), nil, false, nil) } } case .payment: @@ -259,7 +259,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { case let .setupPoll(isQuiz): self.controllerInteraction.openPollCreation(isQuiz) case let .openUserProfile(peerId): - self.controllerInteraction.openPeer(peerId, .info, nil, nil) + self.controllerInteraction.openPeer(peerId, .info, nil, false, nil) case let .openWebView(url, simple): self.controllerInteraction.openWebView(markupButton.title, url, simple, false) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 4f40c690bd..191c4968ce 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -881,7 +881,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, openPeer: { [weak self] peerId in if let strongSelf = self { - strongSelf.controllerInteraction?.openPeer(peerId, .default, nil, nil) + strongSelf.controllerInteraction?.openPeer(peerId, .default, nil, false, nil) } }, openHashtag: { [weak self] peerName, hashtag in if let strongSelf = self { @@ -948,8 +948,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) }))) - }, openPeer: { [weak self] id, navigation, fromMessage, _ in - self?.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage) + }, openPeer: { [weak self] id, navigation, fromMessage, isReaction, _ in + self?.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: isReaction ? fromMessage?.id : nil) }, openPeerMention: { [weak self] name in self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in @@ -1437,7 +1437,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - strongSelf.openPeer(peerId: id, navigation: .default, fromMessage: MessageReference(message)) + strongSelf.openPeer(peerId: id, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: message.id) }) }))) @@ -1552,7 +1552,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ), file: items.first?.file, action: action) - return .single(tip) |> delay(1.0, queue: .mainQueue()) + return .single(tip) } else { return .complete() } @@ -12707,7 +12707,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .mention(peerId, mention): switch action { case .tap: - strongSelf.controllerInteraction?.openPeer(peerId, .default, nil, nil) + strongSelf.controllerInteraction?.openPeer(peerId, .default, nil, false, nil) case .longTap: strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil) } @@ -12795,7 +12795,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .mention(peerId, mention): switch action { case .tap: - strongSelf.controllerInteraction?.openPeer(peerId, .default, nil, nil) + strongSelf.controllerInteraction?.openPeer(peerId, .default, nil, false, nil) case .longTap: strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil) } @@ -12904,7 +12904,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .mention(peerId, mention): switch action { case .tap: - strongSelf.controllerInteraction?.openPeer(peerId, .default, nil, nil) + strongSelf.controllerInteraction?.openPeer(peerId, .default, nil, false, nil) case .longTap: strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil) } @@ -14887,7 +14887,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, expandAvatar: Bool = false) { + private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, fromReactionMessageId: MessageId? = nil, expandAvatar: Bool = false) { let _ = self.presentVoiceMessageDiscardAlert(action: { if case let .peer(currentPeerId) = self.chatLocation, peerId == currentPeerId { switch navigation { @@ -14936,6 +14936,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = fromMessage, let chatPeerId = chatPeerId { mode = .group(chatPeerId) } + if let fromReactionMessageId = fromReactionMessageId { + mode = .reaction(fromReactionMessageId) + } var expandAvatar = expandAvatar if peer.smallProfileImage == nil { expandAvatar = false diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index b84ff770ef..eaa2413067 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -59,7 +59,7 @@ struct UnreadMessageRangeKey: Hashable { public final class ChatControllerInteraction { let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool - let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, MessageReference?, Peer?) -> Void + let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, MessageReference?, Bool, Peer?) -> Void let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void @@ -164,7 +164,7 @@ public final class ChatControllerInteraction { init( openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, - openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageReference?, Peer?) -> Void, + openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageReference?, Bool, Peer?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void, openMessageReactionContextMenu: @escaping (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void, diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 59d683228b..e0e079d971 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1584,7 +1584,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats, customReactionEmojiPacks, firstCustomEmojiReaction in if reactionCount == 0, let stats = stats, stats.peers.count == 1 { c.dismiss(completion: { - controllerInteraction.openPeer(stats.peers[0].id, .default, nil, nil) + controllerInteraction.openPeer(stats.peers[0].id, .default, nil, false, nil) }) } else if (stats != nil && !stats!.peers.isEmpty) || reactionCount != 0 { var tip: ContextController.Tip? @@ -1625,7 +1625,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }, openPeer: { [weak c] id in c?.dismiss(completion: { - controllerInteraction.openPeer(id, .default, nil, nil) + controllerInteraction.openPeer(id, .default, MessageReference(message), true, nil) }) } )), tip: tip))) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 4da4fc9b83..7fc44dc54d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1941,7 +1941,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) } else if let peer = forwardInfo.source ?? forwardInfo.author { - item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, nil) + item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, false, nil) } else if let _ = forwardInfo.authorSignature { item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil) } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 768a16c21b..a7bbf79ded 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -3221,7 +3221,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode }) } else { return .optionalAction({ - item.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil, peekData: nil), nil, item.message.peers[peerId]) + item.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil, peekData: nil), nil, false, item.message.peers[peerId]) }) } } @@ -3253,7 +3253,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) } else if let peer = forwardInfo.source ?? forwardInfo.author { - item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, nil) + item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, false, nil) } else if let _ = forwardInfo.authorSignature { item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil) } @@ -3292,7 +3292,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode }) case let .peerMention(peerId, _): return .action({ - self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil, peekData: nil), nil, nil) + self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil, peekData: nil), nil, false, nil) }) case let .textMention(name): return .action({ diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index 939444c7a0..9c59e4d874 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -612,9 +612,9 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { self.controllerInteraction.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame) } else { if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { - self.controllerInteraction.openPeer(self.peerId, .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, nil) + self.controllerInteraction.openPeer(self.peerId, .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, false, nil) } else { - self.controllerInteraction.openPeer(self.peerId, .info, self.messageReference, nil) + self.controllerInteraction.openPeer(self.peerId, .info, self.messageReference, false, nil) } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index c10623445e..632c3f2152 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -918,7 +918,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) } else if let peer = forwardInfo.source ?? forwardInfo.author { - item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, nil) + item.controllerInteraction.openPeer(peer.id, peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, false, nil) } else if let _ = forwardInfo.authorSignature { item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil) } diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 7f48d56237..91ec438ad8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -847,7 +847,7 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol case .setupPoll: break case let .openUserProfile(peerId): - item.controllerInteraction.openPeer(peerId, .info, nil, nil) + item.controllerInteraction.openPeer(peerId, .info, nil, false, nil) case let .openWebView(url, simple): item.controllerInteraction.openWebView(button.title, url, simple, false) } diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 4f527c518d..a8f8915fe1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -76,7 +76,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } navigationData = .chat(textInputState: nil, subject: subject, peekData: nil) } - item.controllerInteraction.openPeer(id, navigationData, nil, nil) + item.controllerInteraction.openPeer(id, navigationData, nil, false, nil) case let .join(_, joinHash): item.controllerInteraction.openJoinLink(joinHash) } diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index cb4bcc5e94..62bd25fd6e 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -851,7 +851,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { case .setupPoll: break case let .openUserProfile(peerId): - controllerInteraction.openPeer(peerId, .info, nil, nil) + controllerInteraction.openPeer(peerId, .info, nil, false, nil) case let .openWebView(url, simple): controllerInteraction.openWebView(button.title, url, simple, false) } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 26672d5613..eb66759d35 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -248,7 +248,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, gallerySource: gallerySource)) } return false - }, openPeer: { [weak self] peerId, _, message, peer in + }, openPeer: { [weak self] peerId, _, message, _, peer in if let peerId = peerId, peerId != context.account.peerId { self?.openPeer(peerId: peerId, peer: peer) } diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index ecfa7ff302..53c2385a20 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -109,7 +109,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { var selectStickerImpl: ((FileMediaReference, UIView, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + return false }, openPeer: { _, _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect, _ in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 96d9135a80..deabae37bc 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -67,7 +67,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } else { return false } - }, openPeer: { _, _, _, _ in + }, openPeer: { _, _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift index 5b8f1fbdf2..306880c43c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -186,7 +186,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { } } let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in - self?.chatControllerInteraction.openPeer(peer.id, .default, nil, nil) + self?.chatControllerInteraction.openPeer(peer.id, .default, nil, false, nil) }, openPeerContextAction: { [weak self] peer, node, gesture in self?.openPeerContextAction(peer, node, gesture) }) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index fc6eab38f0..0b4cf3e348 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -449,6 +449,12 @@ private enum PeerInfoSettingsSection { case emojiStatus } +private enum PeerInfoReportType { + case `default` + case user + case reaction(MessageId) +} + private final class PeerInfoInteraction { let openChat: () -> Void let openUsername: (String) -> Void @@ -459,7 +465,7 @@ private final class PeerInfoInteraction { let requestDeleteContact: () -> Void let openAddContact: () -> Void let updateBlocked: (Bool) -> Void - let openReport: (Bool) -> Void + let openReport: (PeerInfoReportType) -> Void let openShareBot: () -> Void let openAddBotToGroup: () -> Void let performBotCommand: (PeerInfoBotCommand) -> Void @@ -503,7 +509,7 @@ private final class PeerInfoInteraction { openChat: @escaping () -> Void, openAddContact: @escaping () -> Void, updateBlocked: @escaping (Bool) -> Void, - openReport: @escaping (Bool) -> Void, + openReport: @escaping (PeerInfoReportType) -> Void, openShareBot: @escaping () -> Void, openAddBotToGroup: @escaping () -> Void, performBotCommand: @escaping (PeerInfoBotCommand) -> Void, @@ -900,7 +906,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat return result } -private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, callMessages: [Message]) -> [(AnyHashable, [PeerInfoScreenItem])] { +private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message]) -> [(AnyHashable, [PeerInfoScreenItem])] { guard let data = data else { return [] } @@ -965,13 +971,21 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } } - if let _ = nearbyPeerDistance { + if let reactionSourceMessageId = reactionSourceMessageId { items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { interaction.openChat() })) items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_Report, color: .destructive, action: { - interaction.openReport(true) + interaction.openReport(.reaction(reactionSourceMessageId)) + })) + } else if let _ = nearbyPeerDistance { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { + interaction.openChat() + })) + + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_Report, color: .destructive, action: { + interaction.openReport(.user) })) } else { if !data.isContact { @@ -1010,7 +1024,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if user.botInfo != nil, !user.isVerified { items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 6, text: presentationData.strings.ReportPeer_Report, action: { - interaction.openReport(false) + interaction.openReport(.default) })) } @@ -1717,6 +1731,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate highlightedButton: nil ) private let nearbyPeerDistance: Int32? + private let reactionSourceMessageId: MessageId? private var dataDisposable: Disposable? private let activeActionDisposable = MetaDisposable() @@ -1757,7 +1772,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } private var didSetReady = false - init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, callMessages: [Message], isSettings: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?) { + init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?) { self.controller = controller self.context = context self.peerId = peerId @@ -1765,6 +1780,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.videoCallsEnabled = true self.presentationData = controller.presentationData self.nearbyPeerDistance = nearbyPeerDistance + self.reactionSourceMessageId = reactionSourceMessageId self.callMessages = callMessages self.isSettings = isSettings self.isMediaOnly = context.account.peerId == peerId && !isSettings @@ -1808,8 +1824,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate updateBlocked: { [weak self] block in self?.updateBlocked(block: block) }, - openReport: { [weak self] user in - self?.openReport(user: user, contextController: nil, backAction: nil) + openReport: { [weak self] type in + self?.openReport(type: type, contextController: nil, backAction: nil) }, openShareBot: { [weak self] in self?.openShareBot() @@ -1914,7 +1930,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate return false } return strongSelf.openMessage(id: message.id) - }, openPeer: { [weak self] id, navigation, _, _ in + }, openPeer: { [weak self] id, navigation, _, _, _ in if let id = id { self?.openPeer(peerId: id, navigation: navigation) } @@ -2443,7 +2459,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let items: [ContextMenuItem] = [ .action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in f(.dismissWithoutContent) - self?.chatInterfaceInteraction.openPeer(peer.id, .default, nil, nil) + self?.chatInterfaceInteraction.openPeer(peer.id, .default, nil, false, nil) })) ] let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) @@ -4296,7 +4312,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate items.append(.action(ContextMenuActionItem(text: presentationData.strings.ReportPeer_Report, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in - self?.openReport(user: false, contextController: c, backAction: { c in + self?.openReport(type: .default, contextController: c, backAction: { c in if let mainItemsImpl = mainItemsImpl { c.setItems(mainItemsImpl() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) } @@ -5354,26 +5370,37 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }) } - private func openReport(user: Bool, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)?) { + private func openReport(type: PeerInfoReportType, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)?) { guard let controller = self.controller else { return } self.view.endEditing(true) - let options: [PeerReportOption] - if user { - options = [.spam, .fake, .violence, .pornography, .childAbuse] - } else { - options = [.spam, .fake, .violence, .pornography, .childAbuse, .copyright, .other] - } - - presentPeerReportOptions(context: self.context, parent: controller, contextController: contextController, backAction: backAction, subject: .peer(self.peerId), options: options, passthrough: true, completion: { [weak self] reason, _ in - if let reason = reason { - DispatchQueue.main.async { - self?.openChatForReporting(reason) + switch type { + case let .reaction(sourceMessageId): + let _ = (self.context.engine.peers.reportPeerReaction(authorId: self.peerId, messageId: sourceMessageId) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return } + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .emoji(name: "PoliceCar", text: strongSelf.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) + }) + default: + let options: [PeerReportOption] + if case .user = type { + options = [.spam, .fake, .violence, .pornography, .childAbuse] + } else { + options = [.spam, .fake, .violence, .pornography, .childAbuse, .copyright, .other] } - }) + + presentPeerReportOptions(context: self.context, parent: controller, contextController: contextController, backAction: backAction, subject: .peer(self.peerId), options: options, passthrough: true, completion: { [weak self] reason, _ in + if let reason = reason { + DispatchQueue.main.async { + self?.openChatForReporting(reason) + } + } + }) + } } private func openEncryptionKey() { @@ -7258,7 +7285,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate insets.left += sectionInset insets.right += sectionInset - let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, callMessages: self.callMessages) + let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages) contentHeight += headerHeight if !(self.isSettings && self.state.isEditing) { @@ -7873,6 +7900,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private let avatarInitiallyExpanded: Bool private let isOpenedFromChat: Bool private let nearbyPeerDistance: Int32? + private let reactionSourceMessageId: MessageId? private let callMessages: [Message] private let isSettings: Bool private let hintGroupInCommon: PeerId? @@ -7911,13 +7939,14 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, callMessages: [Message], isSettings: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId self.avatarInitiallyExpanded = avatarInitiallyExpanded self.isOpenedFromChat = isOpenedFromChat self.nearbyPeerDistance = nearbyPeerDistance + self.reactionSourceMessageId = reactionSourceMessageId self.callMessages = callMessages self.isSettings = isSettings self.hintGroupInCommon = hintGroupInCommon @@ -8204,7 +8233,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } override public func loadDisplayNode() { - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, callMessages: self.callMessages, isSettings: self.isSettings, hintGroupInCommon: self.hintGroupInCommon, requestsContext: requestsContext) + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, hintGroupInCommon: self.hintGroupInCommon, requestsContext: requestsContext) self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 }) self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get()) self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get()) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b5c891a183..97dfa1c9e3 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1274,7 +1274,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + return false }, openPeer: { _, _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: { message in @@ -1514,11 +1514,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { if let _ = peer as? TelegramGroup { - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, callMessages: []) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } else if let _ = peer as? TelegramChannel { - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, callMessages: []) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } else if peer is TelegramUser { var nearbyPeerDistance: Int32? + var reactionSourceMessageId: MessageId? var callMessages: [Message] = [] var hintGroupInCommon: PeerId? switch mode { @@ -1530,10 +1531,12 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation break case let .group(id): hintGroupInCommon = id + case let .reaction(messageId): + reactionSourceMessageId = messageId } - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, callMessages: callMessages, hintGroupInCommon: hintGroupInCommon) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, hintGroupInCommon: hintGroupInCommon) } else if peer is TelegramSecretChat { - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, callMessages: []) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } return nil } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index ab664bfbc6..6a6e66e61b 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -124,7 +124,7 @@ public final class TelegramRootController: NavigationController { sharedContext.switchingData = (nil, nil, nil) } - let accountSettingsController = PeerInfoScreenImpl(context: self.context, updatedPresentationData: nil, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, callMessages: [], isSettings: true) + let accountSettingsController = PeerInfoScreenImpl(context: self.context, updatedPresentationData: nil, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], isSettings: true) accountSettingsController.tabBarItemDebugTapAction = { [weak self] in guard let strongSelf = self else { return From 5b12540c44046fee003547cef2ced95ea88baaa9 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 22 Aug 2022 11:46:47 +0300 Subject: [PATCH 05/11] Fix reaction selection --- .../Sources/ReactionContextNode.swift | 191 +++++++++++------- 1 file changed, 123 insertions(+), 68 deletions(-) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1031daf415..1b0d0aa5ad 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -173,9 +173,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var animateFromExtensionDistance: CGFloat = 0.0 private var extensionDistance: CGFloat = 0.0 + private var emojiContentLayout: EmojiPagerContentComponent.CustomLayout? private var emojiContent: EmojiPagerContentComponent? private var emojiContentDisposable: Disposable? + private var horizontalExpandRecognizer: UIPanGestureRecognizer? + private var horizontalExpandStartLocation: CGPoint? + private var horizontalExpandDistance: CGFloat = 0.0 + public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void) { self.context = context self.presentationData = presentationData @@ -270,6 +275,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.contentContainer) self.addSubnode(self.previewingItemContainer) + + if self.canBeExpanded { + let horizontalExpandRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.horizontalExpandGesture(_:))) + //self.view.addGestureRecognizer(horizontalExpandRecognizer) + self.horizontalExpandRecognizer = horizontalExpandRecognizer + } } deinit { @@ -287,6 +298,34 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.view.addGestureRecognizer(longPressRecognizer) } + @objc private func horizontalExpandGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.horizontalExpandStartLocation = recognizer.location(in: self.view) + case .changed: + if let horizontalExpandStartLocation = self.horizontalExpandStartLocation { + let currentLocation = recognizer.location(in: self.view) + + let distance = min(0.0, currentLocation.x - horizontalExpandStartLocation.x) + self.horizontalExpandDistance = distance + + if let (size, insets, anchorRect) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + } + } + case .cancelled, .ended: + if self.horizontalExpandDistance != 0.0 { + self.horizontalExpandDistance = 0.0 + + if let (size, insets, anchorRect) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .animated(duration: 0.3, curve: .spring), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + } + } + default: + break + } + } + public func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, isAnimatingOut: Bool, transition: ContainedViewLayoutTransition) { self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: isAnimatingOut, transition: transition, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) } @@ -422,7 +461,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if appearBounds.intersects(baseItemFrame) || (self.visibleItemNodes[i] != nil && visibleBounds.intersects(baseItemFrame)) { validIndices.insert(i) - let itemFrame = baseItemFrame + var itemFrame = baseItemFrame + itemFrame.origin.x -= self.horizontalExpandDistance + var isPreviewing = false if let highlightedReaction = self.highlightedReaction, highlightedReaction == self.items[i].reaction { isPreviewing = true @@ -598,6 +639,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.updateScrolling(transition: transition) + self.emojiContentLayout = EmojiPagerContentComponent.CustomLayout( + itemsPerRow: itemCount, + itemSize: itemSize, + sideInset: sideInset, + itemSpacing: itemSpacing + ) + if (self.isExpanded || self.reactionSelectionComponentHost != nil), let getEmojiContent = self.getEmojiContent { let reactionSelectionComponentHost: ComponentView var componentTransition = Transition(transition) @@ -621,6 +669,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { semaphore.wait() self.emojiContent = emojiContent + if let emojiContent = emojiContent { + self.updateEmojiContent(emojiContent) + } self.emojiContentDisposable = (getEmojiContent(self.animationCache, self.animationRenderer) |> deliverOnMainQueue).start(next: { [weak self] emojiContent in @@ -629,6 +680,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } strongSelf.emojiContent = emojiContent + strongSelf.updateEmojiContent(emojiContent) if let reactionSelectionComponentHost = strongSelf.reactionSelectionComponentHost, let componentView = reactionSelectionComponentHost.view { let _ = reactionSelectionComponentHost.update( @@ -649,73 +701,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } if let emojiContent = emojiContent { - emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { [weak self] groupId, item, sourceView, sourceRect, sourceLayer in - guard let strongSelf = self, let itemFile = item.itemFile else { - return - } - var found = false - if let groupId = groupId.base as? String, groupId == "recent" { - for reactionItem in strongSelf.items { - if case let .reaction(reactionItem) = reactionItem { - if reactionItem.stillAnimation.fileId == itemFile.fileId { - found = true - - strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) - strongSelf.reactionSelected?(reactionItem.updateMessageReaction, false) - - break - } - } - } - } - if !found { - let reactionItem = ReactionItem( - reaction: ReactionItem.Reaction(rawValue: .custom(itemFile.fileId.id)), - appearAnimation: itemFile, - stillAnimation: itemFile, - listAnimation: itemFile, - largeListAnimation: itemFile, - applicationAnimation: nil, - largeApplicationAnimation: nil, - isCustom: true - ) - strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) - strongSelf.reactionSelected?(reactionItem.updateMessageReaction, false) - } - }, - deleteBackwards: { - }, - openStickerSettings: { - }, - openFeatured: { - }, - addGroupAction: { _, _ in - }, - clearGroup: { _ in - }, - pushController: { _ in - }, - presentController: { _ in - }, - presentGlobalOverlayController: { _ in - }, - navigationController: { - return nil - }, - sendSticker: nil, - chatPeerId: nil, - peekBehavior: nil, - customLayout: EmojiPagerContentComponent.CustomLayout( - itemsPerRow: itemCount, - itemSize: itemSize, - sideInset: sideInset, - itemSpacing: itemSpacing - ), - externalBackground: EmojiPagerContentComponent.ExternalBackground( - effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView - ) - ) + self.updateEmojiContent(emojiContent) let _ = reactionSelectionComponentHost.update( transition: componentTransition, @@ -833,6 +819,75 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + private func updateEmojiContent(_ emojiContent: EmojiPagerContentComponent) { + guard let emojiContentLayout = self.emojiContentLayout else { + return + } + + emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak self] groupId, item, sourceView, sourceRect, sourceLayer in + guard let strongSelf = self, let itemFile = item.itemFile else { + return + } + var found = false + if let groupId = groupId.base as? String, groupId == "recent" { + for reactionItem in strongSelf.items { + if case let .reaction(reactionItem) = reactionItem { + if reactionItem.stillAnimation.fileId == itemFile.fileId { + found = true + + strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) + strongSelf.reactionSelected?(reactionItem.updateMessageReaction, false) + + break + } + } + } + } + if !found { + let reactionItem = ReactionItem( + reaction: ReactionItem.Reaction(rawValue: .custom(itemFile.fileId.id)), + appearAnimation: itemFile, + stillAnimation: itemFile, + listAnimation: itemFile, + largeListAnimation: itemFile, + applicationAnimation: nil, + largeApplicationAnimation: nil, + isCustom: true + ) + strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) + strongSelf.reactionSelected?(reactionItem.updateMessageReaction, false) + } + }, + deleteBackwards: { + }, + openStickerSettings: { + }, + openFeatured: { + }, + addGroupAction: { _, _ in + }, + clearGroup: { _ in + }, + pushController: { _ in + }, + presentController: { _ in + }, + presentGlobalOverlayController: { _ in + }, + navigationController: { + return nil + }, + sendSticker: nil, + chatPeerId: nil, + peekBehavior: nil, + customLayout: emojiContentLayout, + externalBackground: EmojiPagerContentComponent.ExternalBackground( + effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView + ) + ) + } + public func animateIn(from sourceAnchorRect: CGRect) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) From 5d969ed2af1646cdbc670780bb28a4c396290d98 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 23 Aug 2022 02:58:31 +0300 Subject: [PATCH 06/11] Reaction and status improvements --- submodules/ChatListUI/BUILD | 3 + .../Sources/ChatListController.swift | 85 +++- .../Sources/ChatListControllerNode.swift | 39 +- .../Sources/ChatListSearchContainerNode.swift | 6 +- .../Sources/ChatListSearchListPaneNode.swift | 16 +- .../ChatListSearchPaneContainerNode.swift | 14 +- .../Sources/ChatListTitleView.swift | 148 ++++++- .../Sources/Node/ChatListNode.swift | 40 +- .../Sources/Node/ChatListNodeEntries.swift | 4 +- .../ContextUI/Sources/ContextController.swift | 2 +- ...tControllerExtractedPresentationNode.swift | 25 +- .../ContainedViewLayoutTransition.swift | 38 ++ submodules/HashtagSearchUI/BUILD | 3 + .../Sources/HashtagSearchController.swift | 13 +- submodules/Postbox/Sources/ChatListView.swift | 58 ++- .../Postbox/Sources/ChatListViewState.swift | 8 +- submodules/Postbox/Sources/Media.swift | 28 ++ submodules/Postbox/Sources/Peer.swift | 1 + submodules/Postbox/Sources/PeerView.swift | 13 +- submodules/Postbox/Sources/Postbox.swift | 4 +- submodules/Postbox/Sources/RenderedPeer.swift | 8 +- .../Sources/ReactionContextNode.swift | 220 ++++++++- .../TextSizeSelectionController.swift | 12 +- .../ThemeAccentColorControllerNode.swift | 12 +- .../Themes/ThemePreviewControllerNode.swift | 12 +- .../Sources/ShareSearchContainerNode.swift | 2 +- .../State/AccountStateManagementUtils.swift | 8 + .../SyncCore_ReactionsMessageAttribute.swift | 36 ++ .../SyncCore/SyncCore_TelegramChannel.swift | 2 + .../SyncCore/SyncCore_TelegramGroup.swift | 2 + .../SyncCore_TelegramSecretChat.swift | 2 + .../SyncCore/SyncCore_TelegramUser.swift | 8 + .../TelegramEngine/Data/PeersData.swift | 2 +- .../Sources/TelegramEngine/Peers/Peer.swift | 10 +- .../Peers/RecentlySearchedPeerIds.swift | 2 +- .../Sources/Utils/PeerUtils.swift | 2 +- .../Sources/EmojiStatusComponent.swift | 115 ++--- .../EmojiStatusSelectionComponent/BUILD | 2 + .../EmojiStatusSelectionComponent.swift | 32 ++ .../Sources/EmojiPagerContentComponent.swift | 418 +++++++++++++++++- .../Animations/generic_reaction_effect.json | 1 + .../TelegramUI/Sources/ChatController.swift | 105 +++-- .../Sources/ChatEntityKeyboardInputNode.swift | 403 +---------------- ...hatMessageReactionsFooterContentNode.swift | 10 +- .../ChatSearchResultsContollerNode.swift | 11 +- .../TelegramUI/Sources/ChatTitleView.swift | 44 +- .../ContactMultiselectionControllerNode.swift | 12 +- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 101 +---- .../Sources/PeerInfo/PeerInfoScreen.swift | 26 +- .../Sources/PeerSelectionControllerNode.swift | 14 +- .../CoreAnimation/CoreAnimationLayer.swift | 5 + .../MainThreadAnimationLayer.swift | 8 + .../Sources/Private/RootAnimationLayer.swift | 1 + .../AnimationKeypathExtension.swift | 23 + .../Public/Animation/AnimationView.swift | 4 + 55 files changed, 1475 insertions(+), 748 deletions(-) create mode 100644 submodules/TelegramUI/Resources/Animations/generic_reaction_effect.json diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index fb2e3f1932..1a338b0ca4 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -71,10 +71,13 @@ swift_library( "//submodules/PremiumUI:PremiumUI", "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent", + "//submodules/TelegramUI/Components/EntityKeyboard", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 07b889783b..a5ec3537d5 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -30,6 +30,10 @@ import ComponentFlow import LottieAnimationComponent import ProgressIndicatorComponent import PremiumUI +import AnimationCache +import MultiAnimationRenderer +import EmojiStatusSelectionComponent +import EntityKeyboard private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -109,6 +113,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private let controlsHistoryPreload: Bool private let hideNetworkActivityStatus: Bool + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + public let groupId: PeerGroupId public let filter: ChatListFilter? public let previewing: Bool @@ -182,7 +189,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.presentationData = (context.sharedContext.currentPresentationData.with { $0 }) self.presentationDataValue.set(.single(self.presentationData)) - self.titleView = ChatListTitleView(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + + self.titleView = ChatListTitleView( + context: context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer + ) self.tabContainerNode = ChatListFilterTabContainerNode() @@ -201,9 +219,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController title = self.presentationData.strings.ChatList_ArchivedChatsTitle } - self.titleView.title = NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false) + self.titleView.title = NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil) self.navigationItem.titleView = self.titleView + self.titleView.openStatusSetup = { [weak self] sourceView in + self?.openStatusSetup(sourceView: sourceView) + } + if !previewing { if self.groupId == .root && self.filter == nil { self.tabBarItem.title = self.presentationData.strings.DialogList_Title @@ -304,6 +326,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return (data.isLockable, false) } + let peerStatus: Signal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> NetworkStatusTitle.Status? in + guard case let .user(user) = peer else { + return nil + } + if let emojiStatus = user.emojiStatus { + return .emoji(emojiStatus) + } else if user.isPremium { + return .premium + } else { + return nil + } + } + |> distinctUntilChanged + let previousEditingAndNetworkStateValue = Atomic<(Bool, AccountNetworkState)?>(value: nil) if !self.hideNetworkActivityStatus { self.titleDisposable = combineLatest(queue: .mainQueue(), @@ -311,8 +348,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController hasProxy, passcode, self.chatListDisplayNode.containerNode.currentItemState, - self.isReorderingTabsValue.get() - ).start(next: { [weak self] networkState, proxy, passcode, stateAndFilterId, isReorderingTabs in + self.isReorderingTabsValue.get(), + peerStatus + ).start(next: { [weak self] networkState, proxy, passcode, stateAndFilterId, isReorderingTabs, peerStatus in if let strongSelf = self { let defaultTitle: String if strongSelf.groupId == .root { @@ -333,7 +371,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController animated = true } } - strongSelf.titleView.setTitle(NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false), animated: animated) + strongSelf.titleView.setTitle(NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus), animated: animated) } else if isReorderingTabs { if strongSelf.groupId == .root { strongSelf.navigationItem.setRightBarButton(nil, animated: true) @@ -344,17 +382,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let (_, connectsViaProxy) = proxy switch networkState { case .waitingForNetwork: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) case let .connecting(proxy): var text = strongSelf.presentationData.strings.State_Connecting if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 { text = strongSelf.presentationData.strings.State_ConnectingToProxy } - strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false) + strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) case .updating: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) case .online: - strongSelf.titleView.title = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false) + strongSelf.titleView.title = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) } } else { var isRoot = false @@ -401,7 +439,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController var checkProxy = false switch networkState { case .waitingForNetwork: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) case let .connecting(proxy): var text = strongSelf.presentationData.strings.State_Connecting if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 { @@ -410,11 +448,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let proxy = proxy, proxy.hasConnectionIssues { checkProxy = true } - strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) + strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) case .updating: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) case .online: - strongSelf.titleView.setTitle(NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked), animated: (previousEditingAndNetworkState?.0 ?? false) != stateAndFilterId.state.editing) + strongSelf.titleView.setTitle(NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus), animated: (previousEditingAndNetworkState?.0 ?? false) != stateAndFilterId.state.editing) } if groupId == .root && filter == nil && checkProxy { if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil && strongSelf.navigationController?.topViewController === self { @@ -804,6 +842,25 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.activeDownloadsDisposable?.dispose() } + private func openStatusSetup(sourceView: UIView) { + self.present(EmojiStatusSelectionController( + context: self.context, + sourceView: sourceView, + emojiContent: EmojiPagerContentComponent.emojiInputData( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + isStandalone: false, + isStatusSelection: true, + isReactionSelection: false, + reactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: self.context.account.peerId + ) + ), in: .window(.root)) + } + private func updateThemeAndStrings() { if case .root = self.groupId { self.tabBarItem.title = self.presentationData.strings.DialogList_Title @@ -858,7 +915,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } override public func loadDisplayNode() { - self.displayNode = ChatListControllerNode(context: self.context, groupId: EngineChatList.Group(self.groupId), filter: self.filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, controller: self) + self.displayNode = ChatListControllerNode(context: self.context, groupId: EngineChatList.Group(self.groupId), filter: self.filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, controller: self) self.chatListDisplayNode.navigationBar = self.navigationBar diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index fe6b03ac48..82602e6a98 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -11,6 +11,8 @@ import AccountContext import SearchBarNode import SearchUI import ContextUI +import AnimationCache +import MultiAnimationRenderer enum ChatListContainerNodeFilter: Equatable { case all @@ -171,7 +173,7 @@ private final class ChatListShimmerNode: ASDisplayNode { self.addSubnode(self.maskNode) } - func update(context: AccountContext, size: CGSize, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { self.currentParams = (size, presentationData) @@ -180,7 +182,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil)) let timestamp1: Int32 = 100000 let peers: [EnginePeer.Id: EnginePeer] = [:] - let interaction = ChatListNodeInteraction(context: context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in }) @@ -279,6 +281,8 @@ private final class ChatListShimmerNode: ASDisplayNode { private final class ChatListContainerItemNode: ASDisplayNode { private let context: AccountContext + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private var presentationData: PresentationData private let becameEmpty: (ChatListFilter?) -> Void private let emptyAction: (ChatListFilter?) -> Void @@ -292,13 +296,15 @@ private final class ChatListContainerItemNode: ASDisplayNode { private var validLayout: (CGSize, UIEdgeInsets, CGFloat)? - init(context: AccountContext, groupId: EngineChatList.Group, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void) { + init(context: AccountContext, groupId: EngineChatList.Group, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void) { self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.presentationData = presentationData self.becameEmpty = becameEmpty self.emptyAction = emptyAction - self.listNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + self.listNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true) super.init() @@ -384,7 +390,7 @@ private final class ChatListContainerItemNode: ASDisplayNode { } private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) { - node.update(context: self.context, size: size, presentationData: self.presentationData, transition: .immediate) + node.update(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, size: size, presentationData: self.presentationData, transition: .immediate) transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size)) } @@ -430,6 +436,9 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:] private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)? private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] @@ -582,7 +591,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { var didBeginSelectingChats: (() -> Void)? var displayFilterLimit: (() -> Void)? - init(context: AccountContext, groupId: EngineChatList.Group, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void) { + init(context: AccountContext, groupId: EngineChatList.Group, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void) { self.context = context self.groupId = groupId self.previewing = previewing @@ -591,12 +600,14 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.controlsHistoryPreload = controlsHistoryPreload self.presentationData = presentationData + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.selectedId = .all super.init() - let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: nil, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: presentationData, becameEmpty: { [weak self] filter in + let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: nil, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) @@ -872,7 +883,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { itemNode.emptyNode?.restartAnimation() completion?() } else if self.pendingItemNode == nil { - let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[index].filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, becameEmpty: { [weak self] filter in + let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[index].filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) @@ -1000,7 +1011,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { validNodeIds.append(id) if self.itemNodes[id] == nil && self.enableAdjacentFilterLoading && !self.disableItemNodeOperationsWhileAnimating { - let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[i].filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, becameEmpty: { [weak self] filter in + let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[i].filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) @@ -1064,6 +1075,8 @@ final class ChatListControllerNode: ASDisplayNode { private let context: AccountContext private let groupId: EngineChatList.Group private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer let containerNode: ChatListContainerNode let inlineTabContainerNode: ChatListFilterTabInlineContainerNode @@ -1096,14 +1109,16 @@ final class ChatListControllerNode: ASDisplayNode { let debugListView = ListView() - init(context: AccountContext, groupId: EngineChatList.Group, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, controller: ChatListControllerImpl) { + init(context: AccountContext, groupId: EngineChatList.Group, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, controller: ChatListControllerImpl) { self.context = context self.groupId = groupId self.presentationData = presentationData + self.animationCache = animationCache + self.animationRenderer = animationRenderer var filterBecameEmpty: ((ChatListFilter?) -> Void)? var filterEmptyAction: ((ChatListFilter?) -> Void)? - self.containerNode = ChatListContainerNode(context: context, groupId: groupId, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, presentationData: presentationData, filterBecameEmpty: { filter in + self.containerNode = ChatListContainerNode(context: context, groupId: groupId, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, presentationData: presentationData, animationCache: animationCache, animationRenderer: animationRenderer, filterBecameEmpty: { filter in filterBecameEmpty?(filter) }, filterEmptyAction: { filter in filterEmptyAction?(filter) @@ -1246,7 +1261,7 @@ final class ChatListControllerNode: ASDisplayNode { let filter: ChatListNodePeersFilter = [] - let contentNode = ChatListSearchContainerNode(context: self.context, filter: filter, groupId: self.groupId, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, dismissSearch in + let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, groupId: self.groupId, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, dismissSearch in self?.requestOpenPeerFromSearch?(peer, dismissSearch) }, openDisabledPeer: { _ in }, openRecentPeerOptions: { [weak self] peer in diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 420d380b1f..77b40bd16a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -31,6 +31,8 @@ import UndoUI import TextFormat import Postbox import TelegramAnimatedStickerNode +import AnimationCache +import MultiAnimationRenderer private enum ChatListTokenId: Int32 { case archive @@ -129,7 +131,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var validLayout: (ContainerViewLayout, CGFloat)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, groupId: EngineChatList.Group, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { + public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, groupId: EngineChatList.Group, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { self.context = context self.peersFilter = filter self.groupId = groupId @@ -146,7 +148,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.presentInGlobalOverlay = presentInGlobalOverlay self.filterContainerNode = ChatListSearchFiltersContainerNode() - self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, groupId: groupId, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) + self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, groupId: groupId, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) self.paneContainerNode.clipsToBounds = true super.init() diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 4cf88d94d2..91f5a322f9 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -28,6 +28,8 @@ import ChatListSearchRecentPeersNode import UndoUI import Postbox import FetchManagerImpl +import AnimationCache +import MultiAnimationRenderer private enum ChatListRecentEntryStableId: Hashable { case topPeers @@ -822,6 +824,8 @@ private struct DownloadItem: Equatable { final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let context: AccountContext + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let interaction: ChatListSearchInteraction private let peersFilter: ChatListNodePeersFilter private var presentationData: PresentationData @@ -893,8 +897,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var hiddenMediaDisposable: Disposable? - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group?, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group?, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.interaction = interaction self.key = key self.peersFilter = peersFilter @@ -1691,7 +1697,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - let chatListInteraction = ChatListNodeInteraction(context: context, activateSearch: { + let chatListInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { }, peerSelected: { [weak self] peer, chatPeer, _ in interaction.dismissInput() interaction.openPeer(peer, chatPeer, false) @@ -2504,7 +2510,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let insets = UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset) self.shimmerNode.frame = CGRect(origin: CGPoint(x: overflowInset, y: topInset), size: CGSize(width: size.width - overflowInset * 2.0, height: size.height)) - self.shimmerNode.update(context: self.context, size: CGSize(width: size.width - overflowInset * 2.0, height: size.height), presentationData: self.presentationData, key: !(self.searchQueryValue?.isEmpty ?? true) && self.key == .media ? .chats : self.key, hasSelection: self.selectedMessages != nil, transition: transition) + self.shimmerNode.update(context: self.context, size: CGSize(width: size.width - overflowInset * 2.0, height: size.height), presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, key: !(self.searchQueryValue?.isEmpty ?? true) && self.key == .media ? .chats : self.key, hasSelection: self.selectedMessages != nil, transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.recentListNode.frame = CGRect(origin: CGPoint(), size: size) @@ -2914,7 +2920,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { self.addSubnode(self.maskNode) } - func update(context: AccountContext, size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey, hasSelection: Bool, transition: ContainedViewLayoutTransition) { + func update(context: AccountContext, size: CGSize, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, key: ChatListSearchPaneKey, hasSelection: Bool, transition: ContainedViewLayoutTransition) { if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData || self.currentParams?.key != key { self.currentParams = (size, presentationData, key) @@ -2924,7 +2930,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { let timestamp1: Int32 = 100000 var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer1.id] = peer1 - let interaction = ChatListNodeInteraction(context: context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in }) diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index a201c7d6e2..555176ab9f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -7,6 +7,8 @@ import TelegramPresentationData import TelegramCore import AccountContext import ContextUI +import AnimationCache +import MultiAnimationRenderer protocol ChatListSearchPaneNode: ASDisplayNode { var isReady: Signal { get } @@ -102,6 +104,8 @@ private final class ChatListSearchPendingPane { init( context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)?, interaction: ChatListSearchInteraction, navigationController: NavigationController?, @@ -112,7 +116,7 @@ private final class ChatListSearchPendingPane { key: ChatListSearchPaneKey, hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void ) { - let paneNode = ChatListSearchListPaneNode(context: context, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], groupId: groupId, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], groupId: groupId, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady @@ -130,6 +134,8 @@ private final class ChatListSearchPendingPane { final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let context: AccountContext + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let peersFilter: ChatListNodePeersFilter private let groupId: EngineChatList.Group @@ -166,8 +172,10 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD private var currentAvailablePanes: [ChatListSearchPaneKey]? - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.updatedPresentationData = updatedPresentationData self.peersFilter = peersFilter self.groupId = groupId @@ -394,6 +402,8 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD var leftScope = false let pane = ChatListSearchPendingPane( context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, updatedPresentationData: self.updatedPresentationData, interaction: self.interaction!, navigationController: self.navigationController, diff --git a/submodules/ChatListUI/Sources/ChatListTitleView.swift b/submodules/ChatListUI/Sources/ChatListTitleView.swift index 6ab72bd444..c6f9eb02f3 100644 --- a/submodules/ChatListUI/Sources/ChatListTitleView.swift +++ b/submodules/ChatListUI/Sources/ChatListTitleView.swift @@ -4,19 +4,33 @@ import AsyncDisplayKit import Display import TelegramPresentationData import ActivityIndicator +import ComponentFlow +import EmojiStatusComponent +import AnimationCache +import MultiAnimationRenderer +import TelegramCore +import ComponentDisplayAdapters +import AccountContext private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) struct NetworkStatusTitle: Equatable { + enum Status: Equatable { + case premium + case emoji(PeerEmojiStatus) + } + let text: String let activity: Bool let hasProxy: Bool let connectsViaProxy: Bool let isPasscodeSet: Bool let isManuallyLocked: Bool + let peerStatus: Status? } final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitleTransitionNode { + private let context: AccountContext private let titleNode: ImmediateTextNode private let lockView: ChatListTitleLockView private weak var lockSnapshotView: UIView? @@ -24,10 +38,15 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl private let buttonView: HighlightTrackingButton private let proxyNode: ChatTitleProxyNode private let proxyButton: HighlightTrackingButton + private var titleCredibilityIconView: ComponentHostView? + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + + var openStatusSetup: ((UIView) -> Void)? private var validLayout: (CGSize, CGRect)? - private var _title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false) + private var _title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil) var title: NetworkStatusTitle { get { return self._title @@ -91,6 +110,66 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } self.lockView.updateTheme(self.theme) + let animateStatusTransition = !oldValue.text.isEmpty && oldValue.peerStatus != title.peerStatus + + if let peerStatus = title.peerStatus { + let statusContent: EmojiStatusComponent.Content + switch peerStatus { + case .premium: + statusContent = .premium(color: self.theme.list.itemAccentColor) + case let .emoji(emoji): + statusContent = .emojiStatus(status: emoji, size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor) + } + + var titleCredibilityIconTransition: Transition + if animateStatusTransition { + titleCredibilityIconTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } else { + titleCredibilityIconTransition = .immediate + } + let titleCredibilityIconView: ComponentHostView + if let current = self.titleCredibilityIconView { + titleCredibilityIconView = current + } else { + titleCredibilityIconTransition = .immediate + titleCredibilityIconView = ComponentHostView() + self.titleCredibilityIconView = titleCredibilityIconView + self.addSubview(titleCredibilityIconView) + } + + let _ = titleCredibilityIconView.update( + transition: titleCredibilityIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: statusContent, + action: { [weak self] in + guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { + return + } + strongSelf.openStatusSetup?(titleCredibilityIconView) + }, + longTapAction: nil + )), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + } else { + if let titleCredibilityIconView = self.titleCredibilityIconView { + self.titleCredibilityIconView = nil + + if animateStatusTransition { + titleCredibilityIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak titleCredibilityIconView] _ in + titleCredibilityIconView?.removeFromSuperview() + }) + titleCredibilityIconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + titleCredibilityIconView.removeFromSuperview() + } + } + } + self.setNeedsLayout() } } @@ -118,10 +197,14 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } } - init(theme: PresentationTheme, strings: PresentationStrings) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { + self.context = context self.theme = theme self.strings = strings + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 @@ -253,7 +336,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl let buttonX = max(0.0, titleFrame.minX - 10.0) self.buttonView.frame = CGRect(origin: CGPoint(x: buttonX, y: 0.0), size: CGSize(width: min(titleFrame.maxX + 28.0, size.width) - buttonX, height: size.height)) - let lockFrame = CGRect(x: titleFrame.maxX + 6.0, y: titleFrame.minY + 2.0, width: 2.0, height: 2.0) + let lockFrame = CGRect(x: titleFrame.minX - 6.0 - 12.0, y: titleFrame.minY + 2.0, width: 2.0, height: 2.0) transition.updateFrame(view: self.lockView, frame: lockFrame) if let lockSnapshotView = self.lockSnapshotView { transition.updateFrame(view: lockSnapshotView, frame: lockFrame) @@ -261,6 +344,60 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl let activityIndicatorFrame = CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 4.0, y: titleFrame.minY - 1.0), size: indicatorSize) transition.updateFrame(node: self.activityIndicator, frame: activityIndicatorFrame) + + if let peerStatus = self.title.peerStatus { + let statusContent: EmojiStatusComponent.Content + switch peerStatus { + case .premium: + statusContent = .premium(color: self.theme.list.itemAccentColor) + case let .emoji(emoji): + statusContent = .emojiStatus(status: emoji, size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor) + } + + var titleCredibilityIconTransition = Transition(transition) + let titleCredibilityIconView: ComponentHostView + if let current = self.titleCredibilityIconView { + titleCredibilityIconView = current + } else { + titleCredibilityIconTransition = .immediate + titleCredibilityIconView = ComponentHostView() + self.titleCredibilityIconView = titleCredibilityIconView + self.addSubview(titleCredibilityIconView) + } + + let titleIconSize = titleCredibilityIconView.update( + transition: titleCredibilityIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: statusContent, + action: { [weak self] in + guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { + return + } + strongSelf.openStatusSetup?(titleCredibilityIconView) + }, + longTapAction: nil + )), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + titleCredibilityIconTransition.setFrame(view: titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: floorToScreenPixels(titleFrame.midY - titleIconSize.height / 2.0)), size: titleIconSize)) + } else { + if let titleCredibilityIconView = self.titleCredibilityIconView { + self.titleCredibilityIconView = nil + + if transition.isAnimated { + titleCredibilityIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak titleCredibilityIconView] _ in + titleCredibilityIconView?.removeFromSuperview() + }) + titleCredibilityIconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + titleCredibilityIconView.removeFromSuperview() + } + } + } } @objc private func buttonPressed() { @@ -272,11 +409,10 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } func makeTransitionMirrorNode() -> ASDisplayNode { - let view = ChatListTitleView(theme: self.theme, strings: self.strings) - view.title = self.title + let snapshotView = self.snapshotView(afterScreenUpdates: false) return ASDisplayNode(viewBlock: { - return view + return snapshotView ?? UIView() }, didLoad: nil) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 6630b2a315..cb0635854b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -81,7 +81,30 @@ public final class ChatListNodeInteraction { let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer - public init(context: AccountContext, activateSearch: @escaping () -> Void, peerSelected: @escaping (EnginePeer, EnginePeer?, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (EnginePeer) -> Void, togglePeerSelected: @escaping (EnginePeer) -> Void, togglePeersSelection: @escaping ([PeerEntry], Bool) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (EnginePeer, EngineMessage, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (EngineChatList.Group) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, setItemPinned: @escaping (EngineChatList.PinnedItem.Id, Bool) -> Void, setPeerMuted: @escaping (EnginePeer.Id, Bool) -> Void, deletePeer: @escaping (EnginePeer.Id, Bool) -> Void, updatePeerGrouping: @escaping (EnginePeer.Id, Bool) -> Void, togglePeerMarkedUnread: @escaping (EnginePeer.Id, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (EnginePeer.Id) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, present: @escaping (ViewController) -> Void) { + public init( + context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + activateSearch: @escaping () -> Void, + peerSelected: @escaping (EnginePeer, EnginePeer?, ChatListNodeEntryPromoInfo?) -> Void, + disabledPeerSelected: @escaping (EnginePeer) -> Void, + togglePeerSelected: @escaping (EnginePeer) -> Void, + togglePeersSelection: @escaping ([PeerEntry], Bool) -> Void, + additionalCategorySelected: @escaping (Int) -> Void, + messageSelected: @escaping (EnginePeer, EngineMessage, ChatListNodeEntryPromoInfo?) -> Void, + groupSelected: @escaping (EngineChatList.Group) -> Void, + addContact: @escaping (String) -> Void, + setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, + setItemPinned: @escaping (EngineChatList.PinnedItem.Id, Bool) -> Void, + setPeerMuted: @escaping (EnginePeer.Id, Bool) -> Void, + deletePeer: @escaping (EnginePeer.Id, Bool) -> Void, + updatePeerGrouping: @escaping (EnginePeer.Id, Bool) -> Void, + togglePeerMarkedUnread: @escaping (EnginePeer.Id, Bool) -> Void, + toggleArchivedFolderHiddenByDefault: @escaping () -> Void, + hidePsa: @escaping (EnginePeer.Id) -> Void, + activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, + present: @escaping (ViewController) -> Void + ) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.disabledPeerSelected = disabledPeerSelected @@ -101,11 +124,8 @@ public final class ChatListNodeInteraction { self.hidePsa = hidePsa self.activateChatPreview = activateChatPreview self.present = present - - self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { - return TempBox.shared.tempFile(fileName: "file").path - }) - self.animationRenderer = MultiAnimationRendererImpl() + self.animationCache = animationCache + self.animationRenderer = animationRenderer } } @@ -605,6 +625,8 @@ public final class ChatListNode: ListView { private let context: AccountContext private let groupId: EngineChatList.Group private let mode: ChatListNodeMode + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let _ready = ValuePromise() private var didSetReady = false @@ -719,13 +741,15 @@ public final class ChatListNode: ListView { public var selectionLimit: Int32 = 100 public var reachedSelectionLimit: ((Int32) -> Void)? - public init(context: AccountContext, groupId: EngineChatList.Group, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { + public init(context: AccountContext, groupId: EngineChatList.Group, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool) { self.context = context self.groupId = groupId self.chatListFilter = chatListFilter self.chatListFilterValue.set(.single(chatListFilter)) self.fillPreloadItems = fillPreloadItems self.mode = mode + self.animationCache = animationCache + self.animationRenderer = animationRenderer var isSelecting = false if case .peers(_, true, _, _) = mode { @@ -744,7 +768,7 @@ public final class ChatListNode: ListView { self.keepMinimalScrollHeightWithTopInset = navigationBarSearchContentHeight - let nodeInteraction = ChatListNodeInteraction(context: context, activateSearch: { [weak self] in + let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in if let strongSelf = self, let activateSearch = strongSelf.activateSearch { activateSearch() } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index c4095fbfba..dceb3994b3 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -351,7 +351,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState readState: nil, isRemovedFromTotalUnreadCount: false, draftState: nil, - peer: EngineRenderedPeer(peerId: peer.0.id, peers: peers), + peer: EngineRenderedPeer(peerId: peer.0.id, peers: peers, associatedMedia: [:]), presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, @@ -369,7 +369,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState } } - result.append(.PeerEntry(index: EngineChatList.Item.Index.absoluteUpperBound.predecessor, presentationData: state.presentationData, messages: [], readState: nil, isRemovedFromTotalUnreadCount: false, draftState: nil, peer: EngineRenderedPeer(peerId: savedMessagesPeer.id, peers: [savedMessagesPeer.id: savedMessagesPeer]), presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, editing: state.editing, hasActiveRevealControls: false, selected: state.selectedPeerIds.contains(savedMessagesPeer.id), inputActivities: nil, promoInfo: nil, hasFailedMessages: false, isContact: false)) + result.append(.PeerEntry(index: EngineChatList.Item.Index.absoluteUpperBound.predecessor, presentationData: state.presentationData, messages: [], readState: nil, isRemovedFromTotalUnreadCount: false, draftState: nil, peer: EngineRenderedPeer(peerId: savedMessagesPeer.id, peers: [savedMessagesPeer.id: savedMessagesPeer], associatedMedia: [:]), presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, editing: state.editing, hasActiveRevealControls: false, selected: state.selectedPeerIds.contains(savedMessagesPeer.id), inputActivities: nil, promoInfo: nil, hasFailedMessages: false, isContact: false)) } else { if !filteredAdditionalItemEntries.isEmpty { for item in filteredAdditionalItemEntries.reversed() { diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index cd85bff8bc..efcacf3237 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1521,7 +1521,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } if !items.reactionItems.isEmpty, let context = items.context, let animationCache = items.animationCache { - let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }) + let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }, requestLayout: { _ in }) self.reactionContextNode = reactionContextNode self.addSubnode(reactionContextNode) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 4e56f650c0..650e58c4a3 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -324,6 +324,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo func scrollViewDidScroll(_ scrollView: UIScrollView) { var adjustedBounds = scrollView.bounds var topOverscroll: CGFloat = 0.0 + switch self.overscrollMode { case .unrestricted: if adjustedBounds.origin.y < 0.0 { @@ -336,10 +337,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if adjustedBounds.origin.y > 0.0 { adjustedBounds.origin.y = 0.0 } else { + adjustedBounds.origin.y = floorToScreenPixels(adjustedBounds.origin.y * 0.35) topOverscroll = -adjustedBounds.origin.y } } else { if adjustedBounds.origin.y < 0.0 { + adjustedBounds.origin.y = floorToScreenPixels(adjustedBounds.origin.y * 0.35) topOverscroll = -adjustedBounds.origin.y } else if adjustedBounds.origin.y + adjustedBounds.height > scrollView.contentSize.height { adjustedBounds.origin.y = scrollView.contentSize.height - adjustedBounds.height @@ -353,7 +356,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo reactionContextNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: .animated(duration: 0.25, curve: .easeInOut)) if !reactionContextNode.isExpanded && reactionContextNode.canBeExpanded { - if topOverscroll > 60.0 && self.scroller.isDragging { + if topOverscroll > 30.0 && self.scroller.isDragging { self.scroller.panGestureRecognizer.state = .cancelled reactionContextNode.expand() } else { @@ -516,6 +519,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } strongSelf.setCurrentReactionsPositionLock() strongSelf.requestUpdate(transition) + }, + requestLayout: { [weak self] transition in + guard let strongSelf = self else { + return + } + strongSelf.requestUpdate(transition) } ) self.reactionContextNode = reactionContextNode @@ -538,7 +547,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo controller.premiumReactionsSelected?() } } - contentTopInset += reactionContextNode.currentContentHeight + 18.0 + contentTopInset += reactionContextNode.contentHeight + 18.0 } else if let reactionContextNode = self.reactionContextNode { self.reactionContextNode = nil removedReactionContextNode = reactionContextNode @@ -636,7 +645,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo isAnimatingOut = true } else { if let currentReactionsPositionLock = self.currentReactionsPositionLock, let reactionContextNode = self.reactionContextNode { - contentRect.origin.y = currentReactionsPositionLock + reactionContextNode.currentContentHeight + 18.0 + contentRect.origin.y = currentReactionsPositionLock + reactionContextNode.contentHeight + 18.0 + reactionContextNode.visibleExtensionDistance } else if let topPositionLock = self.actionsStackNode.topPositionLock { contentRect.origin.y = topPositionLock - contentActionsSpacing - contentRect.height } else if keepInPlace { @@ -664,7 +673,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0), isAnimatingOut: isAnimatingOut, transition: reactionContextNodeTransition) - self.proposedReactionsPositionLock = contentRect.minY - 18.0 - reactionContextNode.currentContentHeight - 46.0 + self.proposedReactionsPositionLock = contentRect.minY - 18.0 - reactionContextNode.contentHeight - 46.0 } else { self.proposedReactionsPositionLock = nil } @@ -697,6 +706,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentVerticalOffset = delta } } + var additionalVisibleOffsetY: CGFloat = 0.0 + if let reactionContextNode = self.reactionContextNode { + additionalVisibleOffsetY += reactionContextNode.visibleExtensionDistance + } if centerActionsHorizontally { actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) if actionsFrame.maxX > layout.size.width - actionsEdgeInset { @@ -731,10 +744,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo actionsFrame.origin.x = actionsEdgeInset } } - transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame, beginWithCurrentState: true) + transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame.offsetBy(dx: 0.0, dy: additionalVisibleOffsetY), beginWithCurrentState: true) if let contentNode = contentNode { - contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset), size: contentNode.containingItem.view.bounds.size), beginWithCurrentState: true) + contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentNode.containingItem.view.bounds.size), beginWithCurrentState: true) } let contentHeight: CGFloat diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 3f4e52b5d7..b378d6b817 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -263,6 +263,24 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAdditive(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if view.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + view.frame = frame + if let completion = completion { + completion(true) + } + case .animated: + let previousFrame = view.frame + view.frame = frame + self.animatePositionAdditive(layer: view.layer, offset: CGPoint(x: previousFrame.minX - frame.minX, y: previousFrame.minY - frame.minY)) + } + } + } + func updateFrameAdditiveToCenter(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) @@ -283,6 +301,26 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAdditiveToCenter(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if view.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + view.center = frame.center + view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) + if let completion = completion { + completion(true) + } + case .animated: + let previousCenter = view.frame.center + view.center = frame.center + view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) + self.animatePositionAdditive(layer: view.layer, offset: CGPoint(x: previousCenter.x - frame.midX, y: previousCenter.y - frame.midY)) + } + } + } + func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.bounds.equalTo(bounds) && !force { completion?(true) diff --git a/submodules/HashtagSearchUI/BUILD b/submodules/HashtagSearchUI/BUILD index 3f24ed0255..91c74fef79 100644 --- a/submodules/HashtagSearchUI/BUILD +++ b/submodules/HashtagSearchUI/BUILD @@ -21,6 +21,9 @@ swift_library( "//submodules/SegmentedControlNode:SegmentedControlNode", "//submodules/ListMessageItem:ListMessageItem", "//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader", + "//submodules/Postbox:Postbox", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", ], visibility = [ "//visibility:public", diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 79582574cd..f1a52b3e0b 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -2,12 +2,15 @@ import Foundation import UIKit import Display import TelegramCore +import Postbox import SwiftSignalKit import TelegramPresentationData import TelegramBaseController import AccountContext import ChatListUI import ListMessageItem +import AnimationCache +import MultiAnimationRenderer public final class HashtagSearchController: TelegramBaseController { private let queue = Queue() @@ -21,6 +24,9 @@ public final class HashtagSearchController: TelegramBaseController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private var controllerNode: HashtagSearchControllerNode { return self.displayNode as! HashtagSearchControllerNode } @@ -30,6 +36,11 @@ public final class HashtagSearchController: TelegramBaseController { self.peer = peer self.query = query + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none) @@ -47,7 +58,7 @@ public final class HashtagSearchController: TelegramBaseController { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false) }) } - let interaction = ChatListNodeInteraction(context: context, activateSearch: { + let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { }, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in diff --git a/submodules/Postbox/Sources/ChatListView.swift b/submodules/Postbox/Sources/ChatListView.swift index 31bfbf3416..e8c11793fe 100644 --- a/submodules/Postbox/Sources/ChatListView.swift +++ b/submodules/Postbox/Sources/ChatListView.swift @@ -335,6 +335,60 @@ public struct ChatListAdditionalItemEntry: Equatable { } } +func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: SimpleDictionary) -> [MediaId: Media] { + var result: [MediaId: Media] = [:] + + for (_, peer) in peers { + if let associatedMediaIds = peer.associatedMediaIds { + for id in associatedMediaIds { + if result[id] == nil { + if let media = postbox.messageHistoryTable.getMedia(id) { + result[id] = media + } + } + } + } + } + + return result +} + +func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: [Peer]) -> [MediaId: Media] { + var result: [MediaId: Media] = [:] + + for peer in peers { + if let associatedMediaIds = peer.associatedMediaIds { + for id in associatedMediaIds { + if result[id] == nil { + if let media = postbox.messageHistoryTable.getMedia(id) { + result[id] = media + } + } + } + } + } + + return result +} + +func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: [PeerId: Peer]) -> [MediaId: Media] { + var result: [MediaId: Media] = [:] + + for (_, peer) in peers { + if let associatedMediaIds = peer.associatedMediaIds { + for id in associatedMediaIds { + if result[id] == nil { + if let media = postbox.messageHistoryTable.getMedia(id) { + result[id] = media + } + } + } + } + } + + return result +} + final class MutableChatListView { let groupId: PeerGroupId let filterPredicate: ChatListFilterPredicate? @@ -448,7 +502,7 @@ final class MutableChatListView { } } - let renderedPeer = RenderedPeer(peerId: peer.id, peers: peers) + let renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)) let isUnread = postbox.readStateTable.getCombinedState(peer.id)?.isUnread ?? false renderedPeers.append(ChatListGroupReferencePeer(peer: renderedPeer, isUnread: isUnread)) @@ -601,7 +655,7 @@ final class MutableChatListView { } } - return .MessageEntry(index: index, messages: renderedMessages, readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence: presence, tagSummaryInfo: [:], hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact) + return .MessageEntry(index: index, messages: renderedMessages, readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)), presence: presence, tagSummaryInfo: [:], hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact) default: return nil } diff --git a/submodules/Postbox/Sources/ChatListViewState.swift b/submodules/Postbox/Sources/ChatListViewState.swift index baf350c840..c1aeaeb688 100644 --- a/submodules/Postbox/Sources/ChatListViewState.swift +++ b/submodules/Postbox/Sources/ChatListViewState.swift @@ -101,7 +101,7 @@ private func updateMessagePeers(_ message: Message, updatedPeers: [PeerId: Peer] return nil } -private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? { +private func updatedRenderedPeer(postbox: PostboxImpl, renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? { var updated = false for (peerId, currentPeer) in renderedPeer.peers { if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) { @@ -118,7 +118,7 @@ private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [Pe peers[peerId] = currentPeer } } - return RenderedPeer(peerId: renderedPeer.peerId, peers: peers) + return RenderedPeer(peerId: renderedPeer.peerId, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)) } return nil } @@ -666,7 +666,7 @@ private final class ChatListViewSpaceState { hasUpdatedMessages = true } } - let renderedPeer = updatedRenderedPeer(entryRenderedPeer, updatedPeers: transaction.currentUpdatedPeers) + let renderedPeer = updatedRenderedPeer(postbox: postbox, renderedPeer: entryRenderedPeer, updatedPeers: transaction.currentUpdatedPeers) if hasUpdatedMessages || renderedPeer != nil { return .MessageEntry( @@ -1416,7 +1416,7 @@ struct ChatListViewState { presence = postbox.peerPresenceTable.get(index.messageIndex.id.peerId) } } - let renderedPeer = RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers) + let renderedPeer = RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)) var tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo] = [:] for (key, component) in self.summaryComponents.components { diff --git a/submodules/Postbox/Sources/Media.swift b/submodules/Postbox/Sources/Media.swift index 68e7a23038..70d6f75241 100644 --- a/submodules/Postbox/Sources/Media.swift +++ b/submodules/Postbox/Sources/Media.swift @@ -82,6 +82,34 @@ public protocol Media: AnyObject, PostboxCoding { func isSemanticallyEqual(to other: Media) -> Bool } +public func areMediaArraysEqual(_ lhs: [Media], _ rhs: [Media]) -> Bool { + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if !lhs[i].isEqual(to: rhs[i]) { + return false + } + } + return true +} + +public func areMediaDictionariesEqual(_ lhs: [MediaId: Media], _ rhs: [MediaId: Media]) -> Bool { + if lhs.count != rhs.count { + return false + } + for (key, value) in lhs { + if let rhsValue = rhs[key] { + if !value.isEqual(to: rhsValue) { + return false + } + } else { + return false + } + } + return true +} + public extension Media { func isLikelyToBeUpdated() -> Bool { return false diff --git a/submodules/Postbox/Sources/Peer.swift b/submodules/Postbox/Sources/Peer.swift index d760dece99..a3675a7fcd 100644 --- a/submodules/Postbox/Sources/Peer.swift +++ b/submodules/Postbox/Sources/Peer.swift @@ -299,6 +299,7 @@ public protocol Peer: AnyObject, PostboxCoding { var indexName: PeerIndexNameRepresentation { get } var associatedPeerId: PeerId? { get } var notificationSettingsPeerId: PeerId? { get } + var associatedMediaIds: [MediaId]? { get } func isEqual(_ other: Peer) -> Bool } diff --git a/submodules/Postbox/Sources/PeerView.swift b/submodules/Postbox/Sources/PeerView.swift index 6db9f56c81..b9a3639216 100644 --- a/submodules/Postbox/Sources/PeerView.swift +++ b/submodules/Postbox/Sources/PeerView.swift @@ -24,6 +24,7 @@ final class MutablePeerView: MutablePostboxView { var peers: [PeerId: Peer] = [:] var peerPresences: [PeerId: PeerPresence] = [:] var messages: [MessageId: Message] = [:] + var media: [MediaId: Media] = [:] var peerIsContact: Bool var groupId: PeerGroupId? @@ -81,6 +82,7 @@ final class MutablePeerView: MutablePostboxView { self.messages[id] = message } } + self.media = renderAssociatedMediaForPeers(postbox: postbox, peers: self.peers) } func reset(postbox: PostboxImpl) -> Bool { @@ -103,7 +105,7 @@ final class MutablePeerView: MutablePostboxView { } var updated = false - + var peersUpdated = false var updateMessages = false if let cachedData = updatedCachedPeerData[self.contactPeerId], self.cachedData == nil || !self.cachedData!.isEqual(to: cachedData) { @@ -124,8 +126,10 @@ final class MutablePeerView: MutablePostboxView { for id in peerIds { if let peer = updatedPeers[id] { self.peers[id] = peer + peersUpdated = true } else if let peer = getPeer(id) { self.peers[id] = peer + peersUpdated = true } if let presence = updatedPeerPresences[id] { @@ -170,6 +174,7 @@ final class MutablePeerView: MutablePostboxView { if let peer = updatedPeers[id] { self.peers[id] = peer updated = true + peersUpdated = true } if let presence = updatedPeerPresences[id] { self.peerPresences[id] = presence @@ -178,6 +183,10 @@ final class MutablePeerView: MutablePostboxView { } } + if peersUpdated { + self.media = renderAssociatedMediaForPeers(postbox: postbox, peers: self.peers) + } + if let cachedData = self.cachedData, !cachedData.messageIds.isEmpty, let operations = transaction.currentOperationsByPeerId[self.peerId] { outer: for operation in operations { switch operation { @@ -270,6 +279,7 @@ public final class PeerView: PostboxView { public let peers: [PeerId: Peer] public let peerPresences: [PeerId: PeerPresence] public let messages: [MessageId: Message] + public let media: [MediaId: Media] public let peerIsContact: Bool public let groupId: PeerGroupId? @@ -280,6 +290,7 @@ public final class PeerView: PostboxView { self.peers = mutableView.peers self.peerPresences = mutableView.peerPresences self.messages = mutableView.messages + self.media = mutableView.media self.peerIsContact = mutableView.peerIsContact self.groupId = mutableView.groupId } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 1af2ada131..759dac9868 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -3040,7 +3040,7 @@ final class PostboxImpl { peers[associatedPeer.id] = associatedPeer } } - chatPeers.append(RenderedPeer(peerId: peer.id, peers: peers)) + chatPeers.append(RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: self, peers: peers))) peerIds.insert(peerId) } } @@ -3051,7 +3051,7 @@ final class PostboxImpl { if let peer = self.peerTable.get(peerId) { var peers = SimpleDictionary() peers[peer.id] = peer - contactPeers.append(RenderedPeer(peerId: peer.id, peers: peers)) + contactPeers.append(RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: self, peers: peers))) } } } diff --git a/submodules/Postbox/Sources/RenderedPeer.swift b/submodules/Postbox/Sources/RenderedPeer.swift index c80a30ee1e..b9ab8c253d 100644 --- a/submodules/Postbox/Sources/RenderedPeer.swift +++ b/submodules/Postbox/Sources/RenderedPeer.swift @@ -3,15 +3,18 @@ import Foundation public final class RenderedPeer: Equatable { public let peerId: PeerId public let peers: SimpleDictionary + public let associatedMedia: [MediaId: Media] - public init(peerId: PeerId, peers: SimpleDictionary) { + public init(peerId: PeerId, peers: SimpleDictionary, associatedMedia: [MediaId: Media]) { self.peerId = peerId self.peers = peers + self.associatedMedia = associatedMedia } public init(peer: Peer) { self.peerId = peer.id self.peers = SimpleDictionary([peer.id: peer]) + self.associatedMedia = [:] } public static func ==(lhs: RenderedPeer, rhs: RenderedPeer) -> Bool { @@ -26,6 +29,9 @@ public final class RenderedPeer: Equatable { }) { return false } + if !areMediaDictionariesEqual(lhs.associatedMedia, rhs.associatedMedia) { + return false + } return true } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1b0d0aa5ad..900e2cd679 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -18,6 +18,8 @@ import EntityKeyboard import ComponentDisplayAdapters import AnimationCache import MultiAnimationRenderer +import EmojiTextAttachmentView +import TextFormat public final class ReactionItem { public struct Reaction: Equatable { @@ -108,8 +110,8 @@ private final class ExpandItemView: UIView { } func update(size: CGSize, transition: ContainedViewLayoutTransition) { - self.layer.cornerRadius = size.width / 2.0 - self.tintView.layer.cornerRadius = size.width / 2.0 + transition.updateCornerRadius(layer: self.layer, cornerRadius: size.width / 2.0) + transition.updateCornerRadius(layer: self.tintView.layer, cornerRadius: size.width / 2.0) if let image = self.arrowView.image { transition.updateFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels(size.height - size.width + (size.width - image.size.height) / 2.0)), size: image.size)) @@ -125,6 +127,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let items: [ReactionContextItem] private let getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? private let isExpandedUpdated: (ContainedViewLayoutTransition) -> Void + private let requestLayout: (ContainedViewLayoutTransition) -> Void private let backgroundNode: ReactionContextBackgroundNode @@ -137,6 +140,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let scrollNode: ASScrollNode private let previewingItemContainer: ASDisplayNode private var visibleItemNodes: [Int: ReactionItemNode] = [:] + private var disappearingVisibleItemNodes: [Int: ReactionItemNode] = [:] private var visibleItemMaskNodes: [Int: ASDisplayNode] = [:] private let expandItemView: ExpandItemView? @@ -166,12 +170,17 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var didAnimateIn: Bool = false - public private(set) var currentContentHeight: CGFloat = 46.0 + public var contentHeight: CGFloat { + return self.currentContentHeight + } + + private var currentContentHeight: CGFloat = 46.0 public private(set) var isExpanded: Bool = false public private(set) var canBeExpanded: Bool = false private var animateFromExtensionDistance: CGFloat = 0.0 private var extensionDistance: CGFloat = 0.0 + public private(set) var visibleExtensionDistance: CGFloat = 0.0 private var emojiContentLayout: EmojiPagerContentComponent.CustomLayout? private var emojiContent: EmojiPagerContentComponent? @@ -181,12 +190,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var horizontalExpandStartLocation: CGPoint? private var horizontalExpandDistance: CGFloat = 0.0 - public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void) { + public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { self.context = context self.presentationData = presentationData self.items = items self.getEmojiContent = getEmojiContent self.isExpandedUpdated = isExpandedUpdated + self.requestLayout = requestLayout self.animationCache = animationCache self.animationRenderer = MultiAnimationRendererImpl() @@ -278,7 +288,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if self.canBeExpanded { let horizontalExpandRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.horizontalExpandGesture(_:))) - //self.view.addGestureRecognizer(horizontalExpandRecognizer) + self.view.addGestureRecognizer(horizontalExpandRecognizer) self.horizontalExpandRecognizer = horizontalExpandRecognizer } } @@ -306,19 +316,28 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let horizontalExpandStartLocation = self.horizontalExpandStartLocation { let currentLocation = recognizer.location(in: self.view) - let distance = min(0.0, currentLocation.x - horizontalExpandStartLocation.x) + let distance = -min(0.0, currentLocation.x - horizontalExpandStartLocation.x) self.horizontalExpandDistance = distance - if let (size, insets, anchorRect) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) - } + let maxCompressionDistance: CGFloat = 100.0 + var compressionFactor: CGFloat = max(0.0, min(1.0, self.horizontalExpandDistance / maxCompressionDistance)) + compressionFactor = compressionFactor * compressionFactor + + self.extensionDistance = 20.0 * compressionFactor + self.visibleExtensionDistance = self.extensionDistance + + self.requestLayout(.immediate) } case .cancelled, .ended: if self.horizontalExpandDistance != 0.0 { - self.horizontalExpandDistance = 0.0 - - if let (size, insets, anchorRect) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .animated(duration: 0.3, curve: .spring), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + if self.horizontalExpandDistance >= 90.0 { + self.expand() + } else { + self.horizontalExpandDistance = 0.0 + self.extensionDistance = 0.0 + self.visibleExtensionDistance = 0.0 + + self.requestLayout(.animated(duration: 0.4, curve: .spring)) } } default: @@ -430,16 +449,24 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { var currentMaskFrame: CGRect? var maskTransition: ContainedViewLayoutTransition? - let topVisibleItems: Int + let maxCompressionDistance: CGFloat = 100.0 + let compressionFactor: CGFloat = max(0.0, min(1.0, self.horizontalExpandDistance / maxCompressionDistance)) + let minItemSpacing: CGFloat = 2.0 + let effectiveItemSpacing: CGFloat = minItemSpacing + (1.0 - compressionFactor) * (itemSpacing - minItemSpacing) + + var topVisibleItems: Int if self.getEmojiContent != nil { topVisibleItems = min(self.items.count, visibleItemCount - 1) + if compressionFactor >= 0.6 { + topVisibleItems = min(self.items.count, visibleItemCount) + } } else { topVisibleItems = self.items.count } var validIndices = Set() var nextX: CGFloat = sideInset - for i in 0 ..< topVisibleItems { + for i in 0 ..< self.items.count { var currentItemSize = itemSize if let highlightedReactionIndex = highlightedReactionIndex { if highlightedReactionIndex == i { @@ -456,13 +483,40 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { baseItemFrame.origin.y = containerHeight - contentHeight + floor((contentHeight - itemSize) / 2.0) + itemSize + 4.0 - updatedSize } - nextX += currentItemSize + itemSpacing + nextX += currentItemSize + effectiveItemSpacing + + if i >= topVisibleItems { + if let itemNode = self.visibleItemNodes[i] { + self.visibleItemNodes.removeValue(forKey: i) + + if self.disappearingVisibleItemNodes[i] == nil { + self.disappearingVisibleItemNodes[i] = itemNode + itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak self, weak itemNode] _ in + guard let strongSelf = self, let itemNode = itemNode else { + return + } + itemNode.removeFromSupernode() + if strongSelf.disappearingVisibleItemNodes[i] === itemNode { + strongSelf.disappearingVisibleItemNodes.removeValue(forKey: i) + } + }) + itemNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.1, removeOnCompletion: false) + } + } + } + + if i >= topVisibleItems { + if let itemNode = self.disappearingVisibleItemNodes[i] { + transition.updatePosition(node: itemNode, position: baseItemFrame.center, beginWithCurrentState: true) + } + + break + } if appearBounds.intersects(baseItemFrame) || (self.visibleItemNodes[i] != nil && visibleBounds.intersects(baseItemFrame)) { validIndices.insert(i) - var itemFrame = baseItemFrame - itemFrame.origin.x -= self.horizontalExpandDistance + let itemFrame = baseItemFrame var isPreviewing = false if let highlightedReaction = self.highlightedReaction, highlightedReaction == self.items[i].reaction { @@ -536,10 +590,19 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } if let expandItemView = self.expandItemView { - let baseNextFrame = CGRect(origin: CGPoint(x: nextX + 3.0, y: containerHeight - contentHeight + floor((contentHeight - 30.0) / 2.0) + (self.isExpanded ? 46.0 : 0.0)), size: CGSize(width: 30.0, height: 30.0 + self.extensionDistance)) + let expandItemSize: CGFloat + let expandTintOffset: CGFloat + if self.highlightedReaction != nil { + expandItemSize = floor(30.0 * 0.9) + expandTintOffset = contentHeight - containerHeight + } else { + expandItemSize = 30.0 + expandTintOffset = 0.0 + } + let baseNextFrame = CGRect(origin: CGPoint(x: self.scrollNode.view.bounds.width - expandItemSize - 9.0, y: containerHeight - contentHeight + floor((contentHeight - expandItemSize) / 2.0) + (self.isExpanded ? 46.0 : 0.0)), size: CGSize(width: expandItemSize, height: expandItemSize + self.extensionDistance)) transition.updateFrame(view: expandItemView, frame: baseNextFrame) - transition.updateFrame(view: expandItemView.tintView, frame: baseNextFrame) + transition.updateFrame(view: expandItemView.tintView, frame: baseNextFrame.offsetBy(dx: 0.0, dy: expandTintOffset)) expandItemView.update(size: baseNextFrame.size, transition: transition) } @@ -1108,9 +1171,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: self.didTriggerExpandedReaction, isPreviewing: false, transition: transition) let additionalAnimationNode: DefaultAnimatedStickerNodeImpl? + var genericAnimationView: AnimationView? let additionalAnimation: TelegramMediaFile? - if self.didTriggerExpandedReaction, !switchToInlineImmediately { + if self.didTriggerExpandedReaction { additionalAnimation = itemNode.item.largeApplicationAnimation } else { additionalAnimation = itemNode.item.applicationAnimation @@ -1129,6 +1193,53 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { additionalAnimationNodeValue.frame = effectFrame additionalAnimationNodeValue.updateLayout(size: effectFrame.size) self.addSubnode(additionalAnimationNodeValue) + } else if itemNode.item.isCustom { + additionalAnimationNode = nil + + if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { + let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) + view.animationSpeed = 1.0 + view.backgroundColor = nil + view.isOpaque = false + + if incomingMessage { + view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + + genericAnimationView = view + + let animationCache = AnimationCacheImpl(basePath: itemNode.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + let animationRenderer = MultiAnimationRendererImpl() + + let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "BODY 1 Precomp")) + for animationLayer in allLayers { + let baseItemLayer = InlineStickerItemLayer( + context: itemNode.context, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), + file: itemNode.item.listAnimation, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.0), + pointSize: CGSize(width: 32.0, height: 32.0) + ) + + if let sublayers = animationLayer.sublayers { + for sublayer in sublayers { + sublayer.isHidden = true + } + } + + baseItemLayer.isVisibleForAnimations = true + baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0)) + animationLayer.addSublayer(baseItemLayer) + } + + view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0) + self.view.addSubview(view) + } } else { additionalAnimationNode = nil } @@ -1146,6 +1257,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { additionalAnimationCompleted = true intermediateCompletion() } + } else if let genericAnimationView = genericAnimationView { + genericAnimationView.play(completion: { _ in + additionalAnimationCompleted = true + intermediateCompletion() + }) } else { additionalAnimationCompleted = true } @@ -1172,6 +1288,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if switchToInlineImmediately { targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) + itemNode.isHidden = true } else { targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) targetView.addSubnode(itemNode) @@ -1342,6 +1459,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public func expand() { self.animateFromExtensionDistance = self.extensionDistance self.extensionDistance = 0.0 + self.visibleExtensionDistance = 0.0 self.currentContentHeight = 300.0 self.isExpanded = true self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring)) @@ -1585,6 +1703,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } let additionalAnimationNode: AnimatedStickerNode? + var genericAnimationView: AnimationView? if let additionalAnimation = additionalAnimation { let additionalAnimationNodeValue: AnimatedStickerNode @@ -1611,6 +1730,53 @@ public final class StandaloneReactionAnimation: ASDisplayNode { additionalAnimationNodeValue.frame = effectFrame additionalAnimationNodeValue.updateLayout(size: effectFrame.size) self.addSubnode(additionalAnimationNodeValue) + } else if itemNode.item.isCustom { + additionalAnimationNode = nil + + if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { + let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) + view.animationSpeed = 1.0 + view.backgroundColor = nil + view.isOpaque = false + + if incomingMessage { + view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + + genericAnimationView = view + + let animationCache = AnimationCacheImpl(basePath: itemNode.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + let animationRenderer = MultiAnimationRendererImpl() + + let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "BODY 1 Precomp")) + for animationLayer in allLayers { + let baseItemLayer = InlineStickerItemLayer( + context: itemNode.context, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), + file: itemNode.item.listAnimation, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.0), + pointSize: CGSize(width: 32.0, height: 32.0) + ) + + if let sublayers = animationLayer.sublayers { + for sublayer in sublayers { + sublayer.isHidden = true + } + } + + baseItemLayer.isVisibleForAnimations = true + baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0)) + animationLayer.addSublayer(baseItemLayer) + } + + view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0) + self.view.addSubview(view) + } } else { additionalAnimationNode = nil } @@ -1762,6 +1928,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } additionalAnimationNode.visibility = true + } else if let genericAnimationView = genericAnimationView { + genericAnimationView.play(completion: { _ in + additionalAnimationNode?.alpha = 0.0 + additionalAnimationNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + additionalAnimationCompleted = true + intermediateCompletion() + if forceSmallEffectAnimation { + maybeBeginDismissAnimation() + } else { + beginDismissAnimation() + } + }) } else { additionalAnimationCompleted = true } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 2f267b99a7..860ef82238 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -13,6 +13,8 @@ import WallpaperResources import LegacyComponents import ItemListUI import WallpaperBackgroundNode +import AnimationCache +import MultiAnimationRenderer private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -34,6 +36,9 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView private var presentationThemeSettings: PresentationThemeSettings private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private let referenceTimestamp: Int32 private let scrollNode: ASScrollNode @@ -58,6 +63,11 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationThemeSettings = presentationThemeSettings + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + let calendar = Calendar(identifier: .gregorian) var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date()) components.hour = 13 @@ -210,7 +220,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(context: self.context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 8afed86d68..b1a08141c1 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -12,6 +12,8 @@ import AccountContext import WallpaperResources import PresentationDataUtils import WallpaperBackgroundNode +import AnimationCache +import MultiAnimationRenderer private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -152,6 +154,9 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate private let mode: ThemeAccentColorControllerMode private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private let ready: Promise private let queue = Queue() @@ -228,6 +233,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate self.wallpaper = self.presentationData.chatWallpaper let bubbleCorners = self.presentationData.chatBubbleCorners + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + self.ready = ready let calendar = Calendar(identifier: .gregorian) @@ -830,7 +840,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(context: self.context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 4377e348ee..d3c2b0a811 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -12,6 +12,8 @@ import ChatListUI import WallpaperResources import LegacyComponents import WallpaperBackgroundNode +import AnimationCache +import MultiAnimationRenderer private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -33,6 +35,9 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private var previewTheme: PresentationTheme private var presentationData: PresentationData private let isPreview: Bool + + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let ready: Promise @@ -83,6 +88,11 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + let calendar = Calendar(identifier: .gregorian) var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date()) components.hour = 13 @@ -354,7 +364,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(context: self.context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index 78d8ef9720..843274ac68 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -93,7 +93,7 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { if let associatedPeer = associatedPeer { peers[associatedPeer.id] = associatedPeer } - let peer = EngineRenderedPeer(RenderedPeer(peerId: peer.id, peers: SimpleDictionary(peers))) + let peer = EngineRenderedPeer(RenderedPeer(peerId: peer.id, peers: SimpleDictionary(peers), associatedMedia: [:])) return ShareControllerPeerGridItem(context: context, theme: theme, strings: strings, peer: peer, presence: presence, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true) } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 4394e5e1ed..f8fb74f316 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1258,6 +1258,14 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo return peer } }) + case let .updateUserEmojiStatus(userId, emojiStatus): + updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), { peer in + if let user = peer as? TelegramUser { + return user.withUpdatedEmojiStatus(PeerEmojiStatus(apiStatus: emojiStatus)) + } else { + return peer + } + }) case let .updatePeerSettings(peer, settings): let peerStatusSettings = PeerStatusSettings(apiSettings: settings) updatedState.updateCachedPeerData(peer.peerId, { current in diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index 3e9a07c928..031c3b2915 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -159,6 +159,24 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { return self.recentPeers.map(\.peerId) } + public var associatedMediaIds: [MediaId] { + var result: [MediaId] = [] + + for reaction in self.reactions { + switch reaction.value { + case .builtin: + break + case let .custom(fileId): + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if !result.contains(mediaId) { + result.append(mediaId) + } + } + } + + return result + } + public init(canViewList: Bool, reactions: [MessageReaction], recentPeers: [RecentPeer]) { self.canViewList = canViewList self.reactions = reactions @@ -241,6 +259,24 @@ public final class PendingReactionsMessageAttribute: MessageAttribute { } } + public var associatedMediaIds: [MediaId] { + var result: [MediaId] = [] + + for reaction in self.reactions { + switch reaction.value { + case .builtin: + break + case let .custom(fileId): + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if !result.contains(mediaId) { + result.append(mediaId) + } + } + } + + return result + } + public init(accountPeerId: PeerId?, reactions: [PendingReaction], isLarge: Bool) { self.accountPeerId = accountPeerId self.reactions = reactions diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 5d76014559..9ed693c616 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -169,6 +169,8 @@ public final class TelegramChannel: Peer, Equatable { return .title(title: self.title, addressName: self.username) } + public var associatedMediaIds: [MediaId]? { return nil } + public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift index 66a075bf27..0231ebae13 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift @@ -92,6 +92,8 @@ public final class TelegramGroup: Peer, Equatable { return .title(title: self.title, addressName: nil) } + public var associatedMediaIds: [MediaId]? { return nil } + public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift index d5a5301ea8..a0df67bac2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift @@ -14,6 +14,8 @@ public final class TelegramSecretChat: Peer, Equatable { return .title(title: "", addressName: nil) } + public var associatedMediaIds: [MediaId]? { return nil } + public let associatedPeerId: PeerId? public let notificationSettingsPeerId: PeerId? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index 96bf0ff470..bee75b9f92 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -104,6 +104,14 @@ public final class TelegramUser: Peer, Equatable { return .personName(first: self.firstName ?? "", last: self.lastName ?? "", addressName: self.username, phoneNumber: self.phone) } + public var associatedMediaIds: [MediaId]? { + if let emojiStatus = self.emojiStatus { + return [MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)] + } else { + return nil + } + } + public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 047fefea4e..c639f80a65 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -188,7 +188,7 @@ public extension TelegramEngine.EngineData.Item { peers[mainPeer.id] = EnginePeer(mainPeer) } - return EngineRenderedPeer(peerId: self.id, peers: peers) + return EngineRenderedPeer(peerId: self.id, peers: peers, associatedMedia: view.media) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index f7a32199e3..973605f3e4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -531,15 +531,18 @@ public extension EnginePeer { public final class EngineRenderedPeer: Equatable { public let peerId: EnginePeer.Id public let peers: [EnginePeer.Id: EnginePeer] + public let associatedMedia: [EngineMedia.Id: Media] - public init(peerId: EnginePeer.Id, peers: [EnginePeer.Id: EnginePeer]) { + public init(peerId: EnginePeer.Id, peers: [EnginePeer.Id: EnginePeer], associatedMedia: [EngineMedia.Id: Media]) { self.peerId = peerId self.peers = peers + self.associatedMedia = associatedMedia } public init(peer: EnginePeer) { self.peerId = peer.id self.peers = [peer.id: peer] + self.associatedMedia = [:] } public static func ==(lhs: EngineRenderedPeer, rhs: EngineRenderedPeer) -> Bool { @@ -549,6 +552,9 @@ public final class EngineRenderedPeer: Equatable { if lhs.peers != rhs.peers { return false } + if !areMediaDictionariesEqual(lhs.associatedMedia, rhs.associatedMedia) { + return false + } return true } @@ -575,7 +581,7 @@ public extension EngineRenderedPeer { for (id, peer) in renderedPeer.peers { mappedPeers[id] = EnginePeer(peer) } - self.init(peerId: renderedPeer.peerId, peers: mappedPeers) + self.init(peerId: renderedPeer.peerId, peers: mappedPeers, associatedMedia: renderedPeer.associatedMedia) } convenience init(message: EngineMessage) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift index a3e7e1ff4e..f611f03399 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift @@ -87,7 +87,7 @@ func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[RecentlySearch subpeerSummary = RecentlySearchedPeerSubpeerSummary(count: Int(count)) } - result.append(RecentlySearchedPeer(peer: RenderedPeer(peerId: peerId, peers: SimpleDictionary(peerView.peers)), presence: presence, notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, unreadCount: unreadCount, subpeerSummary: subpeerSummary)) + result.append(RecentlySearchedPeer(peer: RenderedPeer(peerId: peerId, peers: SimpleDictionary(peerView.peers), associatedMedia: peerView.media), presence: presence, notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, unreadCount: unreadCount, subpeerSummary: subpeerSummary)) } } diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 31207ee4b0..aac2589ae8 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -250,7 +250,7 @@ public extension RenderedPeer { } } } - self.init(peerId: message.id.peerId, peers: peers) + self.init(peerId: message.id.peerId, peers: peers, associatedMedia: [:]) } var chatMainPeer: Peer? { diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 4faec44345..bc8a85f097 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -110,70 +110,6 @@ public final class EmojiStatusComponent: Component { var emojiPlaceholderColor: UIColor? var emojiSize = CGSize() - /* - if case .fake = credibilityIcon { - image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if case .scam = credibilityIcon { - image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if case .verified = credibilityIcon { - if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { - image = generateImage(backgroundImage.size, contextGenerator: { size, context in - if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.saveGState() - context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) - - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.restoreGState() - - context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) - context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - expandedImage = generateImage(backgroundImage.size, contextGenerator: { size, context in - if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.saveGState() - context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.restoreGState() - - context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) - context.setBlendMode(.clear) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } - } else if case .premium = credibilityIcon { - if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") { - image = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } - } - */ - if self.component?.content != component.content { switch component.content { case .none: @@ -232,10 +168,15 @@ public final class EmojiStatusComponent: Component { if let animationLayer = self.animationLayer { self.animationLayer = nil - animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in - animationLayer?.removeFromSuperlayer() - }) - animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + + if !transition.animation.isImmediate { + animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in + animationLayer?.removeFromSuperlayer() + }) + animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + animationLayer.removeFromSuperlayer() + } } } } @@ -262,8 +203,10 @@ public final class EmojiStatusComponent: Component { self.iconView = iconView self.addSubview(iconView) - iconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - iconView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + if !transition.animation.isImmediate { + iconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + iconView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } } iconView.image = iconImage size = iconImage.size.aspectFilled(availableSize) @@ -272,10 +215,14 @@ public final class EmojiStatusComponent: Component { if let iconView = self.iconView { self.iconView = nil - iconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak iconView] _ in - iconView?.removeFromSuperview() - }) - iconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + if !transition.animation.isImmediate { + iconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + iconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + iconView.removeFromSuperview() + } } } @@ -304,8 +251,10 @@ public final class EmojiStatusComponent: Component { self.animationLayer = animationLayer self.layer.addSublayer(animationLayer) - animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - animationLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + if !transition.animation.isImmediate { + animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animationLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } } animationLayer.frame = CGRect(origin: CGPoint(), size: size) animationLayer.isVisibleForAnimations = true @@ -317,7 +266,7 @@ public final class EmojiStatusComponent: Component { return } strongSelf.emojiFile = result[emojiFileId] - strongSelf.state?.updated(transition: .immediate) + strongSelf.state?.updated(transition: transition) emojiFileUpdated?(result[emojiFileId]) }) @@ -335,10 +284,14 @@ public final class EmojiStatusComponent: Component { if let animationLayer = self.animationLayer { self.animationLayer = nil - animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in - animationLayer?.removeFromSuperlayer() - }) - animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + if !transition.animation.isImmediate { + animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in + animationLayer?.removeFromSuperlayer() + }) + animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + animationLayer.removeFromSuperlayer() + } } } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD index f5c23070fc..6e7bc8c658 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD @@ -12,6 +12,8 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/ComponentFlow:ComponentFlow", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 7e5a698e76..e4b0ee2797 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -11,6 +11,8 @@ import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import PagerComponent +import Postbox +import TelegramCore public final class EmojiStatusSelectionComponent: Component { public typealias EnvironmentType = Empty @@ -180,6 +182,7 @@ public final class EmojiStatusSelectionController: ViewController { private var emojiContentDisposable: Disposable? private var emojiContent: EmojiPagerContentComponent? + private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var isDismissed: Bool = false @@ -245,7 +248,28 @@ public final class EmojiStatusSelectionController: ViewController { openFeatured: { }, addGroupAction: { groupId, isPremiumLocked in + guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else { + return + } + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) + let _ = (strongSelf.context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let strongSelf = self, let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredEmojiPack.info.id == collectionId { + if let strongSelf = self { + strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) + } + let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() + + break + } + } + }) }, clearGroup: { groupId in }, @@ -295,6 +319,8 @@ public final class EmojiStatusSelectionController: ViewController { func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { self.validLayout = layout + var transition = transition + guard let emojiContent = self.emojiContent else { return } @@ -320,6 +346,12 @@ public final class EmojiStatusSelectionController: ViewController { let sideInset: CGFloat = 16.0 + if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { + self.scheduledEmojiContentAnimationHint = nil + let contentAnimation = scheduledEmojiContentAnimationHint + transition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + } + let componentSize = self.componentHost.update( transition: transition, component: AnyComponent(EmojiStatusSelectionComponent( diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 541577f16f..ffe2f7c731 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -27,6 +27,27 @@ private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bund private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white) private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) +private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = { + guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else { + return [] + } + guard let string = try? String(contentsOf: URL(fileURLWithPath: path)) else { + return [] + } + + var result: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = [] + + let orderedSegments = EmojiPagerContentComponent.StaticEmojiSegment.allCases + + let segments = string.components(separatedBy: "\n\n") + for i in 0 ..< min(segments.count, orderedSegments.count) { + let list = segments[i].components(separatedBy: " ") + result.append((orderedSegments[i], list)) + } + + return result +}() + private final class WarpView: UIView { private final class WarpPartView: UIView { let cloneView: PortalView @@ -2662,6 +2683,8 @@ public final class EmojiPagerContentComponent: Component { continue } if let sourceItem = sourceItems[file.fileId] { + itemLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + component.animationRenderer.setFrameIndex(itemId: animationData.resource.resource.id.stringRepresentation, size: itemLayer.pixelSize, frameIndex: sourceItem.frameIndex, placeholder: sourceItem.placeholder) } else { let distance = itemLayer.position.y - itemLayout.frame(groupIndex: 0, itemIndex: 0).midY @@ -3433,7 +3456,7 @@ public final class EmojiPagerContentComponent: Component { let groupBorderRadius: CGFloat = 16.0 - if itemGroup.isPremiumLocked && !itemGroup.isFeatured && !itemGroup.isEmbedded { + if itemGroup.isPremiumLocked && !itemGroup.isFeatured && !itemGroup.isEmbedded && !itemLayout.curveNearBounds { validGroupBorderIds.insert(itemGroup.groupId) let groupBorderLayer: GroupBorderLayer var groupBorderTransition = transition @@ -4414,4 +4437,397 @@ public final class EmojiPagerContentComponent: Component { public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } + + private static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { + let hasPremium: Signal + if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId { + hasPremium = .single(true) + } else { + hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + } + return hasPremium + } + + public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, reactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let isPremiumDisabled = premiumConfiguration.isPremiumDisabled + + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings + + var orderedItemListCollectionIds: [Int32] = [] + + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.LocalRecentEmoji) + + if isStatusSelection { + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) + } + + let emojiItems: Signal = combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), + context.account.viewTracker.featuredEmojiPacks() + ) + |> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in + struct ItemGroup { + var supergroupId: AnyHashable + var id: AnyHashable + var title: String? + var subtitle: String? + var isPremiumLocked: Bool + var isFeatured: Bool + var isExpandable: Bool + var headerItem: EntityKeyboardAnimationData? + var items: [EmojiPagerContentComponent.Item] + } + var itemGroups: [ItemGroup] = [] + var itemGroupIndexById: [AnyHashable: Int] = [:] + + var recentEmoji: OrderedItemListView? + var featuredStatusEmoji: OrderedItemListView? + var recentStatusEmoji: OrderedItemListView? + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { + recentEmoji = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { + featuredStatusEmoji = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji { + recentStatusEmoji = orderedView + } + } + + if isStatusSelection { + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .icon(.premiumStar), + itemFile: nil, + subgroupId: nil + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + + var existingIds = Set() + if let recentStatusEmoji = recentStatusEmoji { + for item in recentStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + + let file = item.media + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + let resultItem: EmojiPagerContentComponent.Item + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } + } + } + if let featuredStatusEmoji = featuredStatusEmoji { + for item in featuredStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + + let file = item.media + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + let resultItem: EmojiPagerContentComponent.Item + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } + } + } + } else if isReactionSelection { + for reactionItem in reactionItems { + let animationFile = reactionItem.selectAnimation + let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: animationFile, + subgroupId: nil + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + } + } + + if let recentEmoji = recentEmoji, !isReactionSelection, !isStatusSelection { + for item in recentEmoji.items { + guard let item = item.contents.get(RecentEmojiItem.self) else { + continue + } + + if case let .file(file) = item.content, isPremiumDisabled, file.isPremiumEmoji { + continue + } + + if !areCustomEmojiEnabled, case .file = item.content { + continue + } + + let resultItem: EmojiPagerContentComponent.Item + switch item.content { + case let .file(file): + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil + ) + case let .text(text): + resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(text), + itemFile: nil, + subgroupId: nil + ) + } + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + } + } + + if areUnicodeEmojiEnabled { + for (subgroupId, list) in staticEmojiMapping { + let groupId: AnyHashable = "static" + for emojiString in list { + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(emojiString), + itemFile: nil, + subgroupId: subgroupId.rawValue + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + } + } + } + + var installedCollectionIds = Set() + for (id, _, _) in view.collectionInfos { + installedCollectionIds.insert(id) + } + + if areCustomEmojiEnabled { + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil + ) + + let supergroupId = entry.index.collectionId + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + var title = "" + var headerItem: EntityKeyboardAnimationData? + inner: for (id, info, _) in view.collectionInfos { + if id == entry.index.collectionId, let info = info as? StickerPackCollectionInfo { + title = info.title + + if let thumbnail = info.thumbnail { + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + headerItem = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: info.immediateThumbnailData, + isReaction: false + ) + } + + break inner + } + } + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, isExpandable: false, headerItem: headerItem, items: [resultItem])) + } + } + + if !isStandalone { + for featuredEmojiPack in featuredEmojiPacks { + if installedCollectionIds.contains(featuredEmojiPack.info.id) { + continue + } + + for item in featuredEmojiPack.topItems { + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil + ) + + let supergroupId = featuredEmojiPack.info.id + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + var headerItem: EntityKeyboardAnimationData? + if let thumbnailFileId = featuredEmojiPack.info.thumbnailFileId, let file = featuredEmojiPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { + headerItem = EntityKeyboardAnimationData(file: file.file) + } else if let thumbnail = featuredEmojiPack.info.thumbnail { + let info = featuredEmojiPack.info + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + headerItem = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: info.immediateThumbnailData, + isReaction: false + ) + } + + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, headerItem: headerItem, items: [resultItem])) + } + } + } + } + } + + return EmojiPagerContentComponent( + id: "emoji", + context: context, + avatarPeer: nil, + animationCache: animationCache, + animationRenderer: animationRenderer, + inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), + itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var hasClear = false + if group.id == AnyHashable("recent") { + hasClear = true + } + + var headerItem = group.headerItem + + if let groupId = group.id.base as? ItemCollectionId { + outer: for (id, info, _) in view.collectionInfos { + if id == groupId, let info = info as? StickerPackCollectionInfo { + if let thumbnailFileId = info.thumbnailFileId { + for item in group.items { + if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId { + headerItem = EntityKeyboardAnimationData(file: itemFile) + break outer + } + } + } + } + } + } + + return EmojiPagerContentComponent.ItemGroup( + supergroupId: group.supergroupId, + groupId: group.id, + title: group.title, + subtitle: group.subtitle, + actionButtonTitle: nil, + isFeatured: group.isFeatured, + isPremiumLocked: group.isPremiumLocked, + isEmbedded: false, + hasClear: hasClear, + isExpandable: group.isExpandable, + displayPremiumBadges: false, + headerItem: headerItem, + items: group.items + ) + }, + itemLayoutType: .compact, + warpContentsOnEdges: isReactionSelection || isStatusSelection + ) + } + return emojiItems + } } diff --git a/submodules/TelegramUI/Resources/Animations/generic_reaction_effect.json b/submodules/TelegramUI/Resources/Animations/generic_reaction_effect.json new file mode 100644 index 0000000000..d83c453b18 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/generic_reaction_effect.json @@ -0,0 +1 @@ +{"tgs":1,"v":"5.5.2","fr":60,"ip":0,"op":180,"w":512,"h":512,"nm":"MAIN","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"BODY 1","sr":1,"ks":{"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[625.68,1067.113,0]},"s":{"a":0,"k":[103,103,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[9.565,25.371],[2.68,0],[7.788,-20.656],[-3.679,11.745],[-2.247,0],[-7.411,-23.657]],"o":[[-7.788,-20.656],[-2.523,0],[-9.566,25.371],[7.411,-23.657],[2.369,0],[3.679,11.745]],"v":[[-30.474,-49.257],[-55.518,-70.085],[-80.562,-49.257],[-77.645,-8.114],[-55.518,-31.514],[-33.391,-8.114]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.490196079016,0.184313729405,0.054901961237,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[677.956,1035.621]},"a":{"a":0,"k":[-53.701,-40.574]},"s":{"a":0,"k":[-100.903,99.097]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Eye R","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[9.849,25.371],[2.76,0],[8.018,-20.656],[-3.788,11.745],[-2.313,0],[-7.63,-23.657]],"o":[[-8.018,-20.656],[-2.598,0],[-9.849,25.371],[7.63,-23.657],[2.439,0],[3.788,11.745]],"v":[[-35.931,-49.257],[-61.717,-70.085],[-87.502,-49.257],[-84.499,-8.114],[-61.717,-31.514],[-38.935,-8.114]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.490196079016,0.184313729405,0.054901961237,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[569.584,1035.621]},"a":{"a":0,"k":[-53.701,-40.574]},"s":{"a":0,"k":[100.903,99.097]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Eye L","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[625.426,1013.075]},"a":{"a":0,"k":[623.77,1035.621]},"s":{"a":0,"k":[97.549,102.52]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Eyes 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[11.985,-45.005],[25.226,15.902],[-91.092,-1.542]],"o":[[-74.879,21.928],[-19.984,-49.554],[93.26,-1.299]],"v":[[114.577,61.938],[-107.95,61.566],[4.023,43.496]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.960784316063,0.960784316063,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-70.614,14.062],[36.502,0.124],[22.149,25.985]],"o":[[-22.963,26.242],[-39.998,-0.136],[36.996,11.903]],"v":[[102.692,78.663],[4.034,123.112],[-96.354,78.362]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.960784316063,0.960784316063,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[0.808,71.626]},"a":{"a":0,"k":[0.808,71.626]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"teeth","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.008,-0.575],[70.215,1.793],[-0.044,0.501],[-98.186,-2.118],[-1.296,-16.564]],"o":[[0.718,56.868],[-124.484,-3.2],[1.355,-16.348],[97.907,-2.066],[0.037,0.501]],"v":[[133.241,28.393],[2.85,134.725],[-126.987,28.005],[3.506,32.923],[133.286,27.996]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.490196079016,0.184313729405,0.054901961237,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 7","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[626.224,1134.199]},"a":{"a":0,"k":[0.811,78.649]},"s":{"a":0,"k":[98.43,101.594]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Mouth 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-104.313],[5.311,-17.436],[31.05,-23.708],[43.057,0],[31.47,23.457],[12.414,36.634],[0,21.217],[-104.313,0]],"o":[[0,19.173],[-11.657,38.27],[-31.769,24.257],[-42.268,0],[-30.398,-22.658],[-6.451,-19.037],[0,-104.313],[104.313,0]],"v":[[667.465,-59.252],[659.298,-4.138],[593.109,90.958],[478.59,129.623],[365.826,92.282],[299.664,1.402],[289.715,-59.252],[478.59,-248.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.945098039216,0.39109287636,0.083761521882,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.811764717102,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-476.698,50.455]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[624.59,1076.875]},"a":{"a":0,"k":[0.813,1.056]},"s":{"a":0,"k":[100.925,99.075]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"BODY 2","bm":0,"hd":false}],"ip":0,"op":184,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL SCALE ALL","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[426.595,253.107,0]},"a":{"a":0,"k":[50,50,0]},"s":{"a":0,"k":[99,99,100]}},"ao":0,"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.207,"y":0.684},"o":{"x":0.05,"y":0},"t":0,"s":[46.316,9.099,0],"to":[-8.016,-28.795,0],"ti":[22.744,3.645,0]},{"i":{"x":0.787,"y":1},"o":{"x":0.6,"y":0.157},"t":13,"s":[-3.625,-57.882,0],"to":[-42.295,2.68,0],"ti":[0,0,0]},{"t":55,"s":[-58.955,361.868,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.631,0.631,0.564],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":0,"s":[-14,14,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":43,"s":[-19,19,100]},{"t":53,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":2,"op":48,"st":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.32,"y":0.763},"o":{"x":0.05,"y":0},"t":-1,"s":[51.425,5.3,0],"to":[0,-1.667,0],"ti":[-8.263,0.945,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.416,"y":0.132},"t":7,"s":[68.425,-66.7,0],"to":[17.562,-2.009,0],"ti":[-6.898,-120.235,0]},{"t":42,"s":[102.661,289.307,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.156,0.156,0.156],"y":[0,0,0]},"t":-1,"s":[7,7,100]},{"i":{"x":[0.718,0.718,0.718],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":14,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":32,"s":[20,20,100]},{"t":45,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":2,"op":41,"st":-1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.499,"y":0.705},"o":{"x":0.05,"y":0},"t":-1,"s":[26.99,1.903,0],"to":[-15.295,-19.9,0],"ti":[19.777,-12.154,0]},{"i":{"x":0.823,"y":1},"o":{"x":0.64,"y":0.44},"t":7,"s":[-57.173,-1.605,0],"to":[-62.741,38.557,0],"ti":[11.61,-102.129,0]},{"t":42,"s":[-161.339,310.307,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":-1,"s":[10,10,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":32,"s":[22,22,100]},{"t":43,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":1,"op":38,"st":-1,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.201,"y":0.933},"o":{"x":0.1,"y":0},"t":-1,"s":[37.454,-10.666,0],"to":[1.659,-26.594,0],"ti":[-13.916,0.231,0]},{"i":{"x":0.806,"y":0.637},"o":{"x":0.64,"y":0.062},"t":10,"s":[55.218,-138.248,0],"to":[18.497,-0.306,0],"ti":[-0.07,-88.869,0]},{"t":52,"s":[75.838,288.611,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.646,0.646,0.43],"y":[1,1,1]},"o":{"x":[0.577,0.577,0.235],"y":[0.168,0.168,0]},"t":-1,"s":[10,10,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":44,"s":[23,23,100]},{"t":55,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":1,"op":51,"st":-1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.283,"y":0.718},"o":{"x":0.11,"y":0},"t":-1,"s":[56.059,29.159,0],"to":[-53.493,-80.535,0],"ti":[60.825,-1.431,0]},{"i":{"x":0.806,"y":1},"o":{"x":0.6,"y":0.407},"t":11,"s":[-141.735,-119.127,0],"to":[-113.9,2.68,0],"ti":[0,0,0]},{"t":49,"s":[-333.655,288.733,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.432,0.432,0.432],"y":[1,1,1]},"o":{"x":[0.233,0.233,0.233],"y":[0.189,0.189,0]},"t":-1,"s":[9.009,9.009,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":40,"s":[30,30,100]},{"t":51,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":1,"op":46,"st":-1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.339,"y":0.87},"o":{"x":0.11,"y":0},"t":-2,"s":[72.209,11.097,0],"to":[-28.213,-68.899,0],"ti":[51.566,-5.031,0]},{"i":{"x":0.781,"y":1},"o":{"x":0.7,"y":0.142},"t":15,"s":[-53.342,-167.527,0],"to":[-82.41,8.04,0],"ti":[0,0,0]},{"t":56,"s":[-218.85,330.743,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.61,0.61,0.61],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":-2,"s":[6.806,6.806,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":44,"s":[26,26,100]},{"t":56,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":1,"op":52,"st":-2,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.404,"y":0.702},"o":{"x":0.125,"y":0.16},"t":2,"s":[46.772,16.267,0],"to":[-33.457,-55.77,0],"ti":[103.874,-12.223,0]},{"i":{"x":0.778,"y":1},"o":{"x":0.6,"y":0.339},"t":24,"s":[-172.095,-127.807,0],"to":[-137.203,16.145,0],"ti":[0,0,0]},{"t":60,"s":[-375.485,195.643,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.597,0.597,0.407],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0,0,0]},"t":2,"s":[-8.062,8.062,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":47,"s":[-18,18,100]},{"t":58,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":2,"op":53,"st":-2,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.296,"y":0.778},"o":{"x":0.05,"y":0},"t":6,"s":[72.305,51.553,0],"to":[0,0,0],"ti":[50.18,1.814,0]},{"i":{"x":0.763,"y":1},"o":{"x":0.6,"y":0.165},"t":31,"s":[-24.575,-168.867,0],"to":[-60.781,-2.198,0],"ti":[0,0,0]},{"t":76,"s":[-136.215,321.293,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.504,0.504,0.4],"y":[1,1,1]},"o":{"x":[0.52,0.52,0.3],"y":[0,0,0]},"t":6,"s":[6.7,6.7,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.135,0.135,0.3],"y":[0,0,0]},"t":62,"s":[24,24,100]},{"t":74,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":10,"op":70,"st":-2,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.904},"o":{"x":0.1,"y":0},"t":16,"s":[73.605,3.078,0],"to":[0,0,0],"ti":[-12.982,0.005,0]},{"i":{"x":0.764,"y":1},"o":{"x":0.64,"y":0.09},"t":33,"s":[85.232,-149.87,0],"to":[13.057,-0.005,0],"ti":[0,0,0]},{"t":82,"s":[103.005,285.768,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.674,0.674,0.4],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":16,"s":[6.7,6.7,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":71,"s":[16,16,100]},{"t":80,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":16,"op":76,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.317,"y":0.665},"o":{"x":0.15,"y":0},"t":15,"s":[36.62,35.238,0],"to":[-24.12,-59.295,0],"ti":[105,-7.02,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.6,"y":0.27},"t":42,"s":[-161.73,-167.752,0],"to":[-133.741,9.247,0],"ti":[0,0,0]},{"t":84,"s":[-357.525,282.143,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":15,"s":[10,10,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":70,"s":[26,26,100]},{"t":82,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":18,"op":78,"st":-12,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.868},"o":{"x":0.15,"y":0},"t":20,"s":[39.305,8.553,0],"to":[0,0,0],"ti":[12.832,-0.076,0]},{"i":{"x":0.8,"y":1},"o":{"x":0.605,"y":0.102},"t":28,"s":[6.188,-65.857,0],"to":[-20.436,0.122,0],"ti":[0,0,0]},{"t":60,"s":[-19.395,304.073,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.601,0.601,0.601],"y":[1,1,1]},"o":{"x":[0.64,0.64,0.3],"y":[0,0,0]},"t":20,"s":[8,8,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":49,"s":[18,18,100]},{"t":58,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":22,"op":56,"st":-6,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.868,"y":1},"o":{"x":0.605,"y":0.159},"t":34,"s":[-146.812,-60.357,0],"to":[-24.783,14.75,0],"ti":[21.8,-144.18,0]},{"t":64,"s":[-270.395,312.073,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0,0,0]},"t":34,"s":[12,12,100]},{"i":{"x":[0.85,0.85,0.85],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":54,"s":[22,22,100]},{"t":64,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":34,"op":62,"st":-5,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.4,"y":0.861},"o":{"x":0.15,"y":0},"t":29,"s":[78.63,31.218,0],"to":[-7.46,-21.855,0],"ti":[53.6,0,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.6,"y":0.133},"t":47,"s":[-18.915,-177.747,0],"to":[-95.32,5.38,0],"ti":[14.01,-92.815,0]},{"t":90,"s":[-203.875,371.758,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.603,0.603,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":29,"s":[-6.7,6.7,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":78,"s":[-20,20,100]},{"t":90,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":33,"op":85,"st":-6,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.834},"o":{"x":0.15,"y":0},"t":41,"s":[28.885,17.743,0],"to":[0,0,0],"ti":[-11.557,-2.816,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.64,"y":0.111},"t":59,"s":[41.957,-150.575,0],"to":[11.557,2.816,0],"ti":[-0.07,-88.869,0]},{"t":94,"s":[37.456,300.592,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":41,"s":[6.7,6.7,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":83,"s":[25,25,100]},{"t":94,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":42,"op":91,"st":-17,"bm":0},{"ddd":0,"ind":16,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.283,"y":0.666},"o":{"x":0.11,"y":0},"t":56,"s":[36.059,7.159,0],"to":[-42.493,-99.535,0],"ti":[60.825,-1.431,0]},{"i":{"x":0.806,"y":1},"o":{"x":0.6,"y":0.274},"t":72,"s":[-101.735,-119.168,0],"to":[-113.9,2.68,0],"ti":[0,0,0]},{"t":109,"s":[-301.655,292.733,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.432,0.432,0.432],"y":[1,1,1]},"o":{"x":[0.233,0.233,0.233],"y":[0.214,0.214,0]},"t":56,"s":[9.009,9.009,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":98,"s":[28,28,100]},{"t":110,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":56,"op":104,"st":56,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.296,"y":0.794},"o":{"x":0.05,"y":0},"t":57,"s":[72.305,51.553,0],"to":[0,0,0],"ti":[50.18,1.814,0]},{"i":{"x":0.763,"y":1},"o":{"x":0.6,"y":0.151},"t":80,"s":[-16.575,-168.867,0],"to":[-60.781,-2.198,0],"ti":[0,0,0]},{"t":122,"s":[-144.215,327.293,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.504,0.504,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":57,"s":[8,8,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,2.754]},"o":{"x":[0.135,0.135,0.3],"y":[0,0,0]},"t":108,"s":[21,21,100]},{"t":120,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":61,"op":116,"st":49,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.32,"y":0.772},"o":{"x":0.05,"y":0},"t":71,"s":[51.425,5.3,0],"to":[0,-1.667,0],"ti":[-8.263,0.945,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.565,"y":0.108},"t":81,"s":[68.425,-66.7,0],"to":[17.562,-2.009,0],"ti":[-2.898,-109.235,0]},{"t":110,"s":[96.661,310.307,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":71,"s":[7,7,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":99,"s":[22,22,100]},{"t":111,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":74,"op":108,"st":71,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.404,"y":0.702},"o":{"x":0.125,"y":0.16},"t":73,"s":[46.772,16.267,0],"to":[-33.457,-55.77,0],"ti":[103.874,-12.223,0]},{"i":{"x":0.778,"y":1},"o":{"x":0.6,"y":0.342},"t":95,"s":[-172.095,-127.807,0],"to":[-137.203,16.145,0],"ti":[0,0,0]},{"t":131,"s":[-365.485,195.643,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.597,0.597,0.407],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0,0,0]},"t":73,"s":[-13,13,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":116,"s":[-26,26,100]},{"t":129,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":76,"op":123,"st":69,"bm":0},{"ddd":0,"ind":20,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.886},"o":{"x":0.15,"y":0},"t":84,"s":[28.885,17.743,0],"to":[0,0,0],"ti":[7.443,1.184,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.64,"y":0.076},"t":101,"s":[19.957,-150.575,0],"to":[-7.443,-1.184,0],"ti":[-0.07,-88.869,0]},{"t":136,"s":[11.456,323.592,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":84,"s":[6.7,6.7,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":126,"s":[24,24,100]},{"t":137,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":85,"op":134,"st":26,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.408,"y":0.879},"o":{"x":0.11,"y":0},"t":92,"s":[32.209,11.097,0],"to":[-11.213,-42.899,0],"ti":[51.566,-5.031,0]},{"i":{"x":0.781,"y":1},"o":{"x":0.7,"y":0.124},"t":111,"s":[-84.342,-168.527,0],"to":[-82.41,8.04,0],"ti":[0,0,0]},{"t":150,"s":[-230.85,330.743,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.61,0.61,0.61],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":92,"s":[6.806,6.806,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":138,"s":[20,20,100]},{"t":150,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":94,"op":144,"st":92,"bm":0},{"ddd":0,"ind":22,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.32,"y":0.911},"o":{"x":0.05,"y":0},"t":110,"s":[51.425,5.3,0],"to":[0,-1.667,0],"ti":[-8.263,0.945,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.565,"y":0.044},"t":124,"s":[68.425,-106.7,0],"to":[17.562,-2.009,0],"ti":[-2.898,-109.235,0]},{"t":155,"s":[92.661,315.307,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":110,"s":[7,7,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":144,"s":[21,21,100]},{"t":156,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":111,"op":153,"st":110,"bm":0},{"ddd":0,"ind":23,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.283,"y":0.57},"o":{"x":0.11,"y":0},"t":114,"s":[56.059,29.159,0],"to":[-53.493,-80.535,0],"ti":[60.825,-1.431,0]},{"i":{"x":0.806,"y":1},"o":{"x":0.6,"y":0.332},"t":132,"s":[-116.735,-142.127,0],"to":[-113.9,2.68,0],"ti":[0,0,0]},{"t":165,"s":[-333.655,288.733,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":114,"s":[10,10,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":152,"s":[26,26,100]},{"t":166,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":116,"op":161,"st":115,"bm":0}]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 191c4968ce..4787fd71a9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -80,6 +80,7 @@ import PremiumUI import ImageTransparency import StickerPackPreviewUI import TextNodeWithEntities +import EntityKeyboard #if DEBUG import os.signpost @@ -1137,7 +1138,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G preconditionFailure() } - return ChatEntityKeyboardInputNode.emojiInputData( + return EmojiPagerContentComponent.emojiInputData( context: strongSelf.context, animationCache: animationCache, animationRenderer: animationRenderer, @@ -1673,26 +1674,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: chosenReaction) { - for reaction in availableReactions.reactions { - guard let centerAnimation = reaction.centerAnimation else { - continue - } - guard let aroundAnimation = reaction.aroundAnimation else { - continue - } - - if reaction.value == chosenReaction { - let standaloneReactionAnimation = StandaloneReactionAnimation() - - strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - standaloneReactionAnimation.animateReactionSelection( - context: strongSelf.context, - theme: strongSelf.presentationData.theme, - animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, - reaction: ReactionItem( + var reactionItem: ReactionItem? + + switch chosenReaction { + case .builtin: + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + if reaction.value == chosenReaction { + reactionItem = ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, stillAnimation: reaction.selectAnimation, @@ -1701,26 +1695,53 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G applicationAnimation: aroundAnimation, largeApplicationAnimation: reaction.effectAnimation, isCustom: false - ), - avatarPeers: [], - playHaptic: false, - isLarge: false, - targetView: targetView, - addStandaloneReactionAnimation: { standaloneReactionAnimation in - guard let strongSelf = self else { - return - } - strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - }, - completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - } - ) - - break + ) + break + } } + case let .custom(fileId): + if let itemFile = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile { + reactionItem = ReactionItem( + reaction: ReactionItem.Reaction(rawValue: chosenReaction), + appearAnimation: itemFile, + stillAnimation: itemFile, + listAnimation: itemFile, + largeListAnimation: itemFile, + applicationAnimation: nil, + largeApplicationAnimation: nil, + isCustom: true + ) + } + } + + if let reactionItem = reactionItem { + let standaloneReactionAnimation = StandaloneReactionAnimation() + + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + standaloneReactionAnimation.animateReactionSelection( + context: strongSelf.context, + theme: strongSelf.presentationData.theme, + animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, + reaction: reactionItem, + avatarPeers: [], + playHaptic: false, + isLarge: false, + targetView: targetView, + addStandaloneReactionAnimation: { standaloneReactionAnimation in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) } } }) @@ -4444,7 +4465,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { peers[associatedPeer.id] = associatedPeer } - renderedPeer = RenderedPeer(peerId: peer.id, peers: peers) + renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) } var isNotAccessible: Bool = false @@ -4741,7 +4762,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { peers[associatedPeer.id] = associatedPeer } - renderedPeer = RenderedPeer(peerId: peer.id, peers: peers) + renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) } var isNotAccessible: Bool = false diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index f12a1e79b1..adc7c416cd 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -26,27 +26,6 @@ import TelegramPresentationData import TelegramNotices import StickerPeekUI -private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = { - guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else { - return [] - } - guard let string = try? String(contentsOf: URL(fileURLWithPath: path)) else { - return [] - } - - var result: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = [] - - let orderedSegments = EmojiPagerContentComponent.StaticEmojiSegment.allCases - - let segments = string.components(separatedBy: "\n\n") - for i in 0 ..< min(segments.count, orderedSegments.count) { - let list = segments[i].components(separatedBy: " ") - result.append((orderedSegments[i], list)) - } - - return result -}() - final class EntityKeyboardGifContent: Equatable { let hasRecentGifs: Bool let component: GifPagerContentComponent @@ -104,382 +83,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return hasPremium } - static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, reactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - let isPremiumDisabled = premiumConfiguration.isPremiumDisabled - - let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - - var orderedItemListCollectionIds: [Int32] = [] - - orderedItemListCollectionIds.append(Namespaces.OrderedItemList.LocalRecentEmoji) - - if isStatusSelection { - orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) - orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) - } - - let emojiItems: Signal = combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), - ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), - context.account.viewTracker.featuredEmojiPacks() - ) - |> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in - struct ItemGroup { - var supergroupId: AnyHashable - var id: AnyHashable - var title: String? - var subtitle: String? - var isPremiumLocked: Bool - var isFeatured: Bool - var isExpandable: Bool - var headerItem: EntityKeyboardAnimationData? - var items: [EmojiPagerContentComponent.Item] - } - var itemGroups: [ItemGroup] = [] - var itemGroupIndexById: [AnyHashable: Int] = [:] - - var recentEmoji: OrderedItemListView? - var featuredStatusEmoji: OrderedItemListView? - var recentStatusEmoji: OrderedItemListView? - for orderedView in view.orderedItemListsViews { - if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { - recentEmoji = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { - featuredStatusEmoji = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji { - recentStatusEmoji = orderedView - } - } - - if isStatusSelection { - let resultItem = EmojiPagerContentComponent.Item( - animationData: nil, - content: .icon(.premiumStar), - itemFile: nil, - subgroupId: nil - ) - - let groupId = "recent" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) - } - - var existingIds = Set() - if let recentStatusEmoji = recentStatusEmoji { - for item in recentStatusEmoji.items { - guard let item = item.contents.get(RecentMediaItem.self) else { - continue - } - - let file = item.media - if existingIds.contains(file.fileId) { - continue - } - existingIds.insert(file.fileId) - - let resultItem: EmojiPagerContentComponent.Item - - let animationData = EntityKeyboardAnimationData(file: file) - resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: file, - subgroupId: nil - ) - - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } - } - } - if let featuredStatusEmoji = featuredStatusEmoji { - for item in featuredStatusEmoji.items { - guard let item = item.contents.get(RecentMediaItem.self) else { - continue - } - - let file = item.media - if existingIds.contains(file.fileId) { - continue - } - existingIds.insert(file.fileId) - - let resultItem: EmojiPagerContentComponent.Item - - let animationData = EntityKeyboardAnimationData(file: file) - resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: file, - subgroupId: nil - ) - - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } - } - } - } else if isReactionSelection { - for reactionItem in reactionItems { - let animationFile = reactionItem.selectAnimation - let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: animationFile, - subgroupId: nil - ) - - let groupId = "recent" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) - } - } - } - - if let recentEmoji = recentEmoji, !isReactionSelection, !isStatusSelection { - for item in recentEmoji.items { - guard let item = item.contents.get(RecentEmojiItem.self) else { - continue - } - - if case let .file(file) = item.content, isPremiumDisabled, file.isPremiumEmoji { - continue - } - - if !areCustomEmojiEnabled, case .file = item.content { - continue - } - - let resultItem: EmojiPagerContentComponent.Item - switch item.content { - case let .file(file): - let animationData = EntityKeyboardAnimationData(file: file) - resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: file, - subgroupId: nil - ) - case let .text(text): - resultItem = EmojiPagerContentComponent.Item( - animationData: nil, - content: .staticEmoji(text), - itemFile: nil, - subgroupId: nil - ) - } - - let groupId = "recent" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) - } - } - } - - if areUnicodeEmojiEnabled { - for (subgroupId, list) in staticEmojiMapping { - let groupId: AnyHashable = "static" - for emojiString in list { - let resultItem = EmojiPagerContentComponent.Item( - animationData: nil, - content: .staticEmoji(emojiString), - itemFile: nil, - subgroupId: subgroupId.rawValue - ) - - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) - } - } - } - } - - var installedCollectionIds = Set() - for (id, _, _) in view.collectionInfos { - installedCollectionIds.insert(id) - } - - if areCustomEmojiEnabled { - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil - ) - - let supergroupId = entry.index.collectionId - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium - if isPremiumLocked && isPremiumDisabled { - continue - } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - var title = "" - var headerItem: EntityKeyboardAnimationData? - inner: for (id, info, _) in view.collectionInfos { - if id == entry.index.collectionId, let info = info as? StickerPackCollectionInfo { - title = info.title - - if let thumbnail = info.thumbnail { - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - headerItem = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: info.immediateThumbnailData, - isReaction: false - ) - } - - break inner - } - } - itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, isExpandable: false, headerItem: headerItem, items: [resultItem])) - } - } - - if !isStandalone { - for featuredEmojiPack in featuredEmojiPacks { - if installedCollectionIds.contains(featuredEmojiPack.info.id) { - continue - } - - for item in featuredEmojiPack.topItems { - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil - ) - - let supergroupId = featuredEmojiPack.info.id - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium - if isPremiumLocked && isPremiumDisabled { - continue - } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - var headerItem: EntityKeyboardAnimationData? - if let thumbnailFileId = featuredEmojiPack.info.thumbnailFileId, let file = featuredEmojiPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { - headerItem = EntityKeyboardAnimationData(file: file.file) - } else if let thumbnail = featuredEmojiPack.info.thumbnail { - let info = featuredEmojiPack.info - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - headerItem = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: info.immediateThumbnailData, - isReaction: false - ) - } - - itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, headerItem: headerItem, items: [resultItem])) - } - } - } - } - } - - return EmojiPagerContentComponent( - id: "emoji", - context: context, - avatarPeer: nil, - animationCache: animationCache, - animationRenderer: animationRenderer, - inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), - itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in - var hasClear = false - if group.id == AnyHashable("recent") { - hasClear = true - } - - var headerItem = group.headerItem - - if let groupId = group.id.base as? ItemCollectionId { - outer: for (id, info, _) in view.collectionInfos { - if id == groupId, let info = info as? StickerPackCollectionInfo { - if let thumbnailFileId = info.thumbnailFileId { - for item in group.items { - if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId { - headerItem = EntityKeyboardAnimationData(file: itemFile) - break outer - } - } - } - } - } - } - - return EmojiPagerContentComponent.ItemGroup( - supergroupId: group.supergroupId, - groupId: group.id, - title: group.title, - subtitle: group.subtitle, - actionButtonTitle: nil, - isFeatured: group.isFeatured, - isPremiumLocked: group.isPremiumLocked, - isEmbedded: false, - hasClear: hasClear, - isExpandable: group.isExpandable, - displayPremiumBadges: false, - headerItem: headerItem, - items: group.items - ) - }, - itemLayoutType: .compact, - warpContentsOnEdges: isReactionSelection || isStatusSelection - ) - } - return emojiItems - } - static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -494,7 +97,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationRenderer = MultiAnimationRendererImpl() //} - let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) + let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] @@ -2415,7 +2018,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in + let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in emojiComponent = value semaphore.signal() }) @@ -2430,7 +2033,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV gifs: nil, availableGifSearchEmojies: [] ), - updatedInputData: ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in + updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index 69ef326a17..63b0de8680 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -15,12 +15,12 @@ import WallpaperBackgroundNode func canViewMessageReactionList(message: Message) -> Bool { var found = false + var canViewList = false for attribute in message.attributes { if let attribute = attribute as? ReactionsMessageAttribute { - if !attribute.canViewList { - return false - } + canViewList = attribute.canViewList found = true + break } } @@ -33,9 +33,11 @@ func canViewMessageReactionList(message: Message) -> Bool { if case .broadcast = channel.info { return false } else { - return true + return canViewList } } else if let _ = peer as? TelegramGroup { + return canViewList + } else if let _ = peer as? TelegramUser { return true } else { return false diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 159cea8ccd..109d0de7ce 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -12,6 +12,8 @@ import ChatListUI import AccountContext import ContextUI import ChatListSearchItemHeader +import AnimationCache +import MultiAnimationRenderer private enum ChatListSearchEntryStableId: Hashable { case messageId(MessageId) @@ -135,6 +137,8 @@ private func chatListSearchContainerPreparedTransition(from fromEntries: [ChatLi class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let location: SearchMessagesLocation private let searchQuery: String private var searchResult: SearchMessagesResult @@ -169,6 +173,11 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe self.presentationData = presentationData self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)) + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor self.listNode.accessibilityPageScrolledString = { row, count in @@ -198,7 +207,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe return entries } - let interaction = ChatListNodeInteraction(context: context, activateSearch: { + let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { }, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index 5bf864094d..60ab1e6a8e 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -63,7 +63,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private let animationRenderer: MultiAnimationRenderer private let contentContainer: ASDisplayNode - let titleNode: ImmediateAnimatedCountLabelNode + let titleContainerView: PortalSourceView + let titleTextNode: ImmediateAnimatedCountLabelNode let titleLeftIconNode: ASImageNode let titleRightIconNode: ASImageNode let titleCredibilityIconView: ComponentHostView @@ -252,8 +253,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { var updated = false - if self.titleNode.segments != segments { - self.titleNode.segments = segments + if self.titleTextNode.segments != segments { + self.titleTextNode.segments = segments updated = true } @@ -525,7 +526,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } var accessibilityText = "" - for segment in self.titleNode.segments { + for segment in self.titleTextNode.segments { switch segment { case let .number(_, string): accessibilityText.append(string.string) @@ -563,7 +564,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.contentContainer = ASDisplayNode() - self.titleNode = ImmediateAnimatedCountLabelNode() + self.titleContainerView = PortalSourceView() + self.titleTextNode = ImmediateAnimatedCountLabelNode() self.titleLeftIconNode = ASImageNode() self.titleLeftIconNode.isLayerBacked = true @@ -587,7 +589,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.accessibilityTraits = .header self.addSubnode(self.contentContainer) - self.contentContainer.addSubnode(self.titleNode) + self.titleContainerView.addSubnode(self.titleTextNode) + self.contentContainer.view.addSubview(self.titleContainerView) self.contentContainer.addSubnode(self.activityNode) self.addSubnode(self.button) @@ -599,17 +602,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.button.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleTextNode.layer.removeAnimation(forKey: "opacity") strongSelf.activityNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleCredibilityIconView.layer.removeAnimation(forKey: "opacity") - strongSelf.titleNode.alpha = 0.4 + strongSelf.titleTextNode.alpha = 0.4 strongSelf.activityNode.alpha = 0.4 strongSelf.titleCredibilityIconView.alpha = 0.4 } else { - strongSelf.titleNode.alpha = 1.0 + strongSelf.titleTextNode.alpha = 1.0 strongSelf.activityNode.alpha = 1.0 strongSelf.titleCredibilityIconView.alpha = 1.0 - strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.titleTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.activityNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } @@ -656,7 +659,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleLeftIconNode.image { if self.titleLeftIconNode.supernode == nil { - self.titleNode.addSubnode(self.titleLeftIconNode) + self.titleTextNode.addSubnode(self.titleLeftIconNode) } leftIconWidth = image.size.width + 6.0 } else if self.titleLeftIconNode.supernode != nil { @@ -694,7 +697,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { ) if self.titleCredibilityIcon != .none { - self.titleNode.view.addSubview(self.titleCredibilityIconView) + self.titleTextNode.view.addSubview(self.titleCredibilityIconView) credibilityIconWidth = titleCredibilitySize.width + 3.0 } else { if self.titleCredibilityIconView.superview != nil { @@ -704,7 +707,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleRightIconNode.image { if self.titleRightIconNode.supernode == nil { - self.titleNode.addSubnode(self.titleRightIconNode) + self.titleTextNode.addSubnode(self.titleRightIconNode) } rightIconWidth = image.size.width + 3.0 } else if self.titleRightIconNode.supernode != nil { @@ -714,7 +717,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { let titleSideInset: CGFloat = 3.0 var titleFrame: CGRect if size.height > 40.0 { - var titleSize = self.titleNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), animated: transition.isAnimated) + var titleSize = self.titleTextNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), animated: transition.isAnimated) titleSize.width += credibilityIconWidth let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .center) let titleInfoSpacing: CGFloat = 0.0 @@ -724,7 +727,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if titleFrame.size.width < size.width { titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0) } - transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) + transition.updateFrameAdditive(view: self.titleContainerView, frame: titleFrame) + transition.updateFrameAdditive(node: self.titleTextNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) } else { let combinedHeight = titleSize.height + activitySize.height + titleInfoSpacing @@ -733,7 +737,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0) } titleFrame.origin.x = max(titleFrame.origin.x, clearBounds.minX + leftIconWidth) - transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) + transition.updateFrameAdditive(view: self.titleContainerView, frame: titleFrame) + transition.updateFrameAdditive(node: self.titleTextNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) var activityFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - activitySize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize) if activitySize.width < size.width { @@ -752,14 +757,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0 + UIScreenPixel, y: 6.0), size: image.size) } } else { - let titleSize = self.titleNode.updateLayout(size: CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height), animated: transition.isAnimated) + let titleSize = self.titleTextNode.updateLayout(size: CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height), animated: transition.isAnimated) let activitySize = self.activityNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0), height: size.height), alignment: .center) let titleInfoSpacing: CGFloat = 8.0 let combinedWidth = titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + activitySize.width + titleInfoSpacing titleFrame = CGRect(origin: CGPoint(x: leftIconWidth + floor((clearBounds.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) - transition.updateFrameAdditiveToCenter(node: self.titleNode, frame: titleFrame) + + transition.updateFrameAdditiveToCenter(view: self.titleContainerView, frame: titleFrame) + transition.updateFrameAdditiveToCenter(node: self.titleTextNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) + self.activityNode.frame = CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize) if let image = self.titleLeftIconNode.image { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 3c8bc2b6c4..21480c6728 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -9,6 +9,8 @@ import MergeLists import AccountContext import ContactListUI import ChatListUI +import AnimationCache +import MultiAnimationRenderer private struct SearchResultEntry: Identifiable { let index: Int @@ -66,12 +68,20 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter], limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?) { self.navigationBar = navigationBar self.context = context self.presentationData = presentationData + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + var placeholder: String var includeChatList = false switch mode { @@ -88,7 +98,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { if case let .chatSelection(_, selectedChats, additionalCategories, chatListFilters) = mode { placeholder = self.presentationData.strings.ChatListFilter_AddChatsTitle - let chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) + let chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true) if let limit = limit { chatListNode.selectionLimit = limit chatListNode.reachedSelectionLimit = reachedSelectionLimit diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 1fcdb849c7..f125064568 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2201,12 +2201,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { let phoneGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePhoneLongPress(_:))) self.subtitleNodeRawContainer.view.addGestureRecognizer(phoneGestureRecognizer) - - /*let premiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) - self.titleCredibilityIconNode.view.addGestureRecognizer(premiumGestureRecognizer) - - let expandedPremiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) - self.titleExpandedCredibilityIconNode.view.addGestureRecognizer(expandedPremiumGestureRecognizer)*/ } @objc private func handleUsernameLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { @@ -2341,76 +2335,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { if themeUpdated || self.currentCredibilityIcon != credibilityIcon { self.currentCredibilityIcon = credibilityIcon - let image: UIImage? - var expandedImage: UIImage? - - if case .fake = credibilityIcon { - image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if case .scam = credibilityIcon { - image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if case .verified = credibilityIcon { - if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { - image = generateImage(backgroundImage.size, contextGenerator: { size, context in - if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.saveGState() - context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) - - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.restoreGState() - - context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) - context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - expandedImage = generateImage(backgroundImage.size, contextGenerator: { size, context in - if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.saveGState() - context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.restoreGState() - - context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) - context.setBlendMode(.clear) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } - } else if case .premium = credibilityIcon { - if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") { - image = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } - } else { - image = nil - } - - let _ = image - let _ = expandedImage - var currentEmojiStatus: PeerEmojiStatus? let emojiRegularStatusContent: EmojiStatusComponent.Content let emojiExpandedStatusContent: EmojiStatusComponent.Content @@ -2436,8 +2360,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { emojiExpandedStatusContent = .emojiStatus(status: emojiStatus, size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15)) } + let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty + let iconSize = self.titleCredibilityIconView.update( - transition: Transition(transition), + transition: animateStatusIcon ? Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate, component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, @@ -2497,7 +2423,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { containerSize: CGSize(width: 34.0, height: 34.0) ) let expandedIconSize = self.titleExpandedCredibilityIconView.update( - transition: Transition(transition), + transition: animateStatusIcon ? Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate, component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, @@ -2522,17 +2448,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.credibilityIconSize = iconSize self.titleExpandedCredibilityIconSize = expandedIconSize - - /*if let image = image { - self.credibilityIconSize = image.size - self.titleExpandedCredibilityIconSize = (expandedImage ?? image).size - } else { - self.credibilityIconSize = nil - self.titleExpandedCredibilityIconSize = nil - }*/ - - //self.titleCredibilityIconNode.image = image - //self.titleExpandedCredibilityIconNode.image = expandedImage ?? image } self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0 @@ -2755,7 +2670,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { var titleHorizontalOffset: CGFloat = 0.0 if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize { titleHorizontalOffset = -(credibilityIconSize.width + 4.0) / 2.0 - transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0, y: floor((titleSize.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)) + + var collapsedTransitionOffset: CGFloat = 0.0 + if let navigationTransition = self.navigationTransition { + collapsedTransitionOffset = -10.0 * navigationTransition.fraction + } + + transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0 + collapsedTransitionOffset, y: floor((titleSize.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)) transition.updateFrame(view: self.titleExpandedCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleExpandedSize.width + 4.0, y: floor((titleExpandedSize.height - titleExpandedCredibilityIconSize.height) / 2.0) + 1.0), size: titleExpandedCredibilityIconSize)) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 0b4cf3e348..55cf19dc48 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -72,6 +72,7 @@ import InstantPageCache import EmojiStatusSelectionComponent import AnimationCache import MultiAnimationRenderer +import EntityKeyboard protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -3095,7 +3096,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.controller?.present(EmojiStatusSelectionController( context: strongSelf.context, sourceView: sourceView, - emojiContent: ChatEntityKeyboardInputNode.emojiInputData( + emojiContent: EmojiPagerContentComponent.emojiInputData( context: strongSelf.context, animationCache: animationCache, animationRenderer: animationRenderer, @@ -8487,7 +8488,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig private var previousBackButtonBadge: ASDisplayNode? private var currentBackButton: ASDisplayNode? - private var previousTitleNode: (ASDisplayNode, ASDisplayNode)? + private var previousTitleNode: (ASDisplayNode, PortalView)? private var previousStatusNode: (ASDisplayNode, ASDisplayNode)? private var didSetup: Bool = false @@ -8538,11 +8539,12 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig self.currentBackButton = currentBackButton self.addSubnode(currentBackButton) } - if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView { - let previousTitleNode = previousTitleView.titleNode.makeCopy() + if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView, let previousTitleNode = PortalView(matchPosition: false) { + previousTitleNode.view.frame = previousTitleView.titleContainerView.frame + previousTitleView.titleContainerView.addPortal(view: previousTitleNode) let previousTitleContainerNode = ASDisplayNode() - previousTitleContainerNode.addSubnode(previousTitleNode) - previousTitleNode.frame = previousTitleNode.frame.offsetBy(dx: -previousTitleNode.frame.width / 2.0, dy: -previousTitleNode.frame.height / 2.0) + previousTitleContainerNode.view.addSubview(previousTitleNode.view) + previousTitleNode.view.frame = previousTitleNode.view.frame.offsetBy(dx: -previousTitleNode.view.frame.width / 2.0, dy: -previousTitleNode.view.frame.height / 2.0) self.previousTitleNode = (previousTitleContainerNode, previousTitleNode) self.addSubnode(previousTitleContainerNode) @@ -8596,12 +8598,16 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView, let _ = (bottomNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode)?.avatarNode, let (previousTitleContainerNode, previousTitleNode) = self.previousTitleNode, let (previousStatusContainerNode, previousStatusNode) = self.previousStatusNode { - let previousTitleFrame = previousTitleView.titleNode.view.convert(previousTitleView.titleNode.bounds, to: bottomNavigationBar.view) + let previousTitleFrame = previousTitleView.titleContainerView.convert(previousTitleView.titleContainerView.bounds, to: bottomNavigationBar.view) let previousStatusFrame = previousTitleView.activityNode.view.convert(previousTitleView.activityNode.bounds, to: bottomNavigationBar.view) self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction) var topHeight = topNavigationBar.backgroundNode.bounds.height + if let iconView = previousTitleView.titleCredibilityIconView.componentView { + transition.updateFrame(view: iconView, frame: iconView.bounds.offsetBy(dx: (1.0 - fraction) * 8.0, dy: 0.0)) + } + if let (layout, _) = self.screenNode.validLayout { let sectionInset: CGFloat if layout.size.width >= 375.0 { @@ -8614,11 +8620,11 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, transition: transition, additive: false) } - let titleScale = (fraction * previousTitleNode.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.bounds.height + let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height let subtitleScale = max(0.01, min(10.0, (fraction * previousStatusNode.bounds.height + (1.0 - fraction) * self.headerNode.subtitleNodeRawContainer.bounds.height) / previousStatusNode.bounds.height)) transition.updateFrame(node: previousTitleContainerNode, frame: CGRect(origin: self.headerNode.titleNodeRawContainer.frame.center, size: CGSize())) - transition.updateFrame(node: previousTitleNode, frame: CGRect(origin: CGPoint(x: -previousTitleFrame.width / 2.0, y: -previousTitleFrame.height / 2.0), size: previousTitleFrame.size)) + transition.updateFrame(view: previousTitleNode.view, frame: CGRect(origin: CGPoint(x: -previousTitleFrame.width / 2.0, y: -previousTitleFrame.height / 2.0), size: previousTitleFrame.size)) transition.updateFrame(node: previousStatusContainerNode, frame: CGRect(origin: self.headerNode.subtitleNodeRawContainer.frame.center, size: CGSize())) transition.updateFrame(node: previousStatusNode, frame: CGRect(origin: CGPoint(x: -previousStatusFrame.size.width / 2.0, y: -previousStatusFrame.size.height / 2.0), size: previousStatusFrame.size)) @@ -8626,7 +8632,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig transition.updateSublayerTransformScale(node: previousStatusContainerNode, scale: subtitleScale) transition.updateAlpha(node: self.headerNode.titleNode, alpha: (1.0 - fraction)) - transition.updateAlpha(node: previousTitleNode, alpha: fraction) + transition.updateAlpha(layer: previousTitleNode.view.layer, alpha: fraction) transition.updateAlpha(node: self.headerNode.subtitleNode, alpha: (1.0 - fraction)) transition.updateAlpha(node: previousStatusNode, alpha: fraction) diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 798e6d568a..071a961d68 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -16,6 +16,8 @@ import AttachmentTextInputPanelNode import ChatPresentationInterfaceState import ChatSendMessageActionUI import ChatTextLinkEditUI +import AnimationCache +import MultiAnimationRenderer final class PeerSelectionControllerNode: ASDisplayNode { private let context: AccountContext @@ -72,6 +74,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { } private var presentationDataPromise = Promise() + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private var readyValue = Promise() var ready: Signal { return self.readyValue.get() @@ -93,6 +98,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.presentationData = presentationData + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } @@ -120,7 +130,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { chatListCategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), title: self.presentationData.strings.PeerSelection_ImportIntoNewGroup, appearance: .action)) } - self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true) super.init() @@ -584,6 +594,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { presentationData: self.presentationData, contentNode: ChatListSearchContainerNode( context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, updatedPresentationData: self.updatedPresentationData, filter: self.filter, groupId: EngineChatList.Group(.root), diff --git a/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift index 2507d457d8..d36f608299 100644 --- a/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift @@ -424,6 +424,11 @@ extension CoreAnimationLayer: RootAnimationLayer { LottieLogger.shared.assertionFailure("`AnimationKeypath`s are currently unsupported") return nil } + + func allLayers(for keypath: AnimationKeypath) -> [CALayer] { + LottieLogger.shared.assertionFailure("`AnimationKeypath`s are currently unsupported") + return [] + } func animatorNodes(for _: AnimationKeypath) -> [AnimatorNode]? { LottieLogger.shared.assertionFailure("`AnimatorNode`s are not used in this rendering implementation") diff --git a/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift index ffc0d1eab4..3b3518f169 100644 --- a/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift @@ -255,6 +255,14 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer { } return nil } + + func allLayers(for keypath: AnimationKeypath) -> [CALayer] { + var result: [CALayer] = [] + for layer in animationLayers { + result.append(contentsOf: layer.allLayers(for: keypath)) + } + return result + } func animatorNodes(for keypath: AnimationKeypath) -> [AnimatorNode]? { var results = [AnimatorNode]() diff --git a/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift index ea586f66c4..be86aa4446 100644 --- a/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift @@ -39,6 +39,7 @@ protocol RootAnimationLayer: CALayer { func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? func layer(for keypath: AnimationKeypath) -> CALayer? + func allLayers(for keypath: AnimationKeypath) -> [CALayer] func animatorNodes(for keypath: AnimationKeypath) -> [AnimatorNode]? } diff --git a/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift b/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift index 7e31179780..94cd15df26 100644 --- a/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +++ b/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift @@ -114,6 +114,29 @@ extension KeypathSearchable { } return nil } + + func allLayers(for keyPath: AnimationKeypath) -> [CALayer] { + if keyPath.nextKeypath == nil, let layerKey = keyPath.currentKey, layerKey.equalsKeypath(keypathName) { + /// We found our layer! + + if let keypathLayer = self.keypathLayer { + return [keypathLayer] + } else { + return [] + } + } + guard let nextKeypath = keyPath.popKey(keypathName) else { + /// Nope. Stop Search + return [] + } + + /// Now check child keypaths. + var foundSublayers: [CALayer] = [] + for child in childKeypaths { + foundSublayers.append(contentsOf: child.allLayers(for: nextKeypath)) + } + return foundSublayers + } func logKeypaths(for keyPath: AnimationKeypath?) { let newKeypath: AnimationKeypath diff --git a/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift b/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift index f2b1b96186..3408de4a2b 100644 --- a/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift +++ b/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift @@ -659,6 +659,10 @@ final public class AnimationView: AnimationViewBase { sublayer.addSublayer(subViewLayer) } } + + public func allLayers(forKeypath keypath: AnimationKeypath) -> [CALayer] { + return animationLayer?.allLayers(for: keypath) ?? [] + } /// Converts a CGRect from the AnimationView's coordinate space into the /// coordinate space of the layer found at Keypath. From 53690ab1998bc73044af74cb3458c1d9887b1549 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 23 Aug 2022 18:26:41 +0300 Subject: [PATCH 07/11] Reaction and status improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/ChatListController.swift | 2 +- .../Sources/ReactionButtonListComponent.swift | 2 +- .../Sources/ReactionContextNode.swift | 51 +++++- .../Sources/ReactionSelectionNode.swift | 8 +- submodules/TelegramApi/Sources/Api0.swift | 5 + submodules/TelegramApi/Sources/Api20.swift | 12 ++ submodules/TelegramApi/Sources/Api26.swift | 166 ++++++------------ submodules/TelegramApi/Sources/Api27.swift | 164 +++++++++++------ submodules/TelegramApi/Sources/Api28.swift | 56 ++++++ submodules/TelegramApi/Sources/Api29.swift | 69 +++++++- .../Sources/Account/Account.swift | 2 + .../Sources/State/ManagedRecentStickers.swift | 128 ++++++++++++-- .../Sources/State/MessageReactions.swift | 25 +++ .../SyncCore/SyncCore_Namespaces.swift | 2 + .../SyncCore/SyncCore_RecentMediaItem.swift | 112 ++++++++++++ .../Sources/EmojiPagerContentComponent.swift | 77 +++++++- .../TelegramUI/Sources/ChatController.swift | 86 +++------ .../Sources/ChatEntityKeyboardInputNode.swift | 6 +- .../ChatMessageDateAndStatusNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 12 +- .../Sources/TopMessageReactions.swift | 140 +++++++++++++++ 22 files changed, 866 insertions(+), 263 deletions(-) create mode 100644 submodules/TelegramUI/Sources/TopMessageReactions.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 396cae1d26..7802be54dd 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7977,3 +7977,5 @@ Sorry for the inconvenience."; "Premium.EmojiStatusText" = "Emoji status is a premium feature.\n Other features included in **Telegram Premium**:"; "Login.SelectCountry" = "Country"; + +"ReportPeer.ReportReaction" = "Report Reaction"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index a5ec3537d5..5f174de088 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -853,7 +853,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController isStandalone: false, isStatusSelection: true, isReactionSelection: false, - reactionItems: [], + topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: self.context.account.peerId diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 26b05c47c1..daa8aaf93f 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -1121,7 +1121,7 @@ public final class ReactionButtonsAsyncLayoutContainer { } return false - }).prefix(10) { + }) { validIds.insert(reaction.reaction.value) var avatarPeers = reaction.peers diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 900e2cd679..5f42662e14 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -184,6 +184,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var emojiContentLayout: EmojiPagerContentComponent.CustomLayout? private var emojiContent: EmojiPagerContentComponent? + private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var emojiContentDisposable: Disposable? private var horizontalExpandRecognizer: UIPanGestureRecognizer? @@ -746,8 +747,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.updateEmojiContent(emojiContent) if let reactionSelectionComponentHost = strongSelf.reactionSelectionComponentHost, let componentView = reactionSelectionComponentHost.view { + var emojiTransition: Transition = .immediate + if let scheduledEmojiContentAnimationHint = strongSelf.scheduledEmojiContentAnimationHint { + strongSelf.scheduledEmojiContentAnimationHint = nil + let contentAnimation = scheduledEmojiContentAnimationHint + emojiTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + } + let _ = reactionSelectionComponentHost.update( - transition: .immediate, + transition: emojiTransition, component: AnyComponent(EmojiStatusSelectionComponent( theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, @@ -766,6 +774,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let emojiContent = emojiContent { self.updateEmojiContent(emojiContent) + if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { + self.scheduledEmojiContentAnimationHint = nil + let contentAnimation = scheduledEmojiContentAnimationHint + componentTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + } + let _ = reactionSelectionComponentHost.update( transition: componentTransition, component: AnyComponent(EmojiStatusSelectionComponent( @@ -892,6 +906,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { guard let strongSelf = self, let itemFile = item.itemFile else { return } + var found = false if let groupId = groupId.base as? String, groupId == "recent" { for reactionItem in strongSelf.items { @@ -928,7 +943,34 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { }, openFeatured: { }, - addGroupAction: { _, _ in + addGroupAction: { [weak self] groupId, isPremiumLocked in + guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else { + return + } + + if isPremiumLocked { + strongSelf.premiumReactionsSelected?() + return + } + + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) + let _ = (strongSelf.context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let strongSelf = self, let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredEmojiPack.info.id == collectionId { + if let strongSelf = self { + strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) + } + let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() + + break + } + } + }) }, clearGroup: { _ in }, @@ -1457,6 +1499,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } public func expand() { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.tap() + self.animateFromExtensionDistance = self.extensionDistance self.extensionDistance = 0.0 self.visibleExtensionDistance = 0.0 diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 11addcef5a..33833c1118 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -269,7 +269,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self.stillAnimationNode = stillAnimationNode self.addSubnode(stillAnimationNode) - stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) stillAnimationNode.position = animationFrame.center stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) stillAnimationNode.updateLayout(size: animationFrame.size) @@ -325,9 +325,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self.staticAnimationNode.automaticallyLoadFirstFrame = true if !self.hasAppearAnimation { - self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id))) + self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource, isVideo: self.item.largeListAnimation.isVideoEmoji || self.item.largeListAnimation.isVideoSticker), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id))) } else { - self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) } self.staticAnimationNode.position = animationFrame.center self.staticAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) @@ -335,7 +335,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self.staticAnimationNode.visibility = true if let animateInAnimationNode = self.animateInAnimationNode { - animateInAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.appearAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.appearAnimation.resource.id))) + animateInAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.appearAnimation.resource, isVideo: self.item.appearAnimation.isVideoEmoji || self.item.appearAnimation.isVideoSticker), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.appearAnimation.resource.id))) animateInAnimationNode.position = animationFrame.center animateInAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) animateInAnimationNode.updateLayout(size: animationFrame.size) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 6807723a58..0202dce082 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -828,6 +828,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[791617983] = { return Api.Update.parse_updateReadHistoryOutbox($0) } dict[1757493555] = { return Api.Update.parse_updateReadMessagesContents($0) } dict[821314523] = { return Api.Update.parse_updateRecentEmojiStatuses($0) } + dict[1870160884] = { return Api.Update.parse_updateRecentReactions($0) } dict[-1706939360] = { return Api.Update.parse_updateRecentStickers($0) } dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) } dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($0) } @@ -1007,6 +1008,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[978610270] = { return Api.messages.Messages.parse_messagesSlice($0) } dict[863093588] = { return Api.messages.PeerDialogs.parse_peerDialogs($0) } dict[1753266509] = { return Api.messages.PeerSettings.parse_peerSettings($0) } + dict[-352454890] = { return Api.messages.Reactions.parse_reactions($0) } + dict[-1334846497] = { return Api.messages.Reactions.parse_reactionsNotModified($0) } dict[-1999405994] = { return Api.messages.RecentStickers.parse_recentStickers($0) } dict[186120336] = { return Api.messages.RecentStickers.parse_recentStickersNotModified($0) } dict[-2069878259] = { return Api.messages.SavedGifs.parse_savedGifs($0) } @@ -1780,6 +1783,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.PeerSettings: _1.serialize(buffer, boxed) + case let _1 as Api.messages.Reactions: + _1.serialize(buffer, boxed) case let _1 as Api.messages.RecentStickers: _1.serialize(buffer, boxed) case let _1 as Api.messages.SavedGifs: diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index 0a2d2faea1..5996ca4de7 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -640,6 +640,7 @@ public extension Api { case updateReadHistoryOutbox(peer: Api.Peer, maxId: Int32, pts: Int32, ptsCount: Int32) case updateReadMessagesContents(messages: [Int32], pts: Int32, ptsCount: Int32) case updateRecentEmojiStatuses + case updateRecentReactions case updateRecentStickers case updateSavedGifs case updateSavedRingtones @@ -1428,6 +1429,12 @@ public extension Api { buffer.appendInt32(821314523) } + break + case .updateRecentReactions: + if boxed { + buffer.appendInt32(1870160884) + } + break case .updateRecentStickers: if boxed { @@ -1736,6 +1743,8 @@ public extension Api { return ("updateReadMessagesContents", [("messages", String(describing: messages)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) case .updateRecentEmojiStatuses: return ("updateRecentEmojiStatuses", []) + case .updateRecentReactions: + return ("updateRecentReactions", []) case .updateRecentStickers: return ("updateRecentStickers", []) case .updateSavedGifs: @@ -3347,6 +3356,9 @@ public extension Api { public static func parse_updateRecentEmojiStatuses(_ reader: BufferReader) -> Update? { return Api.Update.updateRecentEmojiStatuses } + public static func parse_updateRecentReactions(_ reader: BufferReader) -> Update? { + return Api.Update.updateRecentReactions + } public static func parse_updateRecentStickers(_ reader: BufferReader) -> Update? { return Api.Update.updateRecentStickers } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index d41b704f2c..10ff0ec3a1 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -134,6 +134,64 @@ public extension Api.messages { } } +public extension Api.messages { + enum Reactions: TypeConstructorDescription { + case reactions(hash: Int64, reactions: [Api.Reaction]) + case reactionsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .reactions(let hash, let reactions): + if boxed { + buffer.appendInt32(-352454890) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + break + case .reactionsNotModified: + if boxed { + buffer.appendInt32(-1334846497) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .reactions(let hash, let reactions): + return ("reactions", [("hash", String(describing: hash)), ("reactions", String(describing: reactions))]) + case .reactionsNotModified: + return ("reactionsNotModified", []) + } + } + + public static func parse_reactions(_ reader: BufferReader) -> Reactions? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Reaction]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.Reactions.reactions(hash: _1!, reactions: _2!) + } + else { + return nil + } + } + public static func parse_reactionsNotModified(_ reader: BufferReader) -> Reactions? { + return Api.messages.Reactions.reactionsNotModified + } + + } +} public extension Api.messages { enum RecentStickers: TypeConstructorDescription { case recentStickers(hash: Int64, packs: [Api.StickerPack], stickers: [Api.Document], dates: [Int32]) @@ -1372,111 +1430,3 @@ public extension Api.payments { } } -public extension Api.phone { - enum ExportedGroupCallInvite: TypeConstructorDescription { - case exportedGroupCallInvite(link: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedGroupCallInvite(let link): - if boxed { - buffer.appendInt32(541839704) - } - serializeString(link, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedGroupCallInvite(let link): - return ("exportedGroupCallInvite", [("link", String(describing: link))]) - } - } - - public static func parse_exportedGroupCallInvite(_ reader: BufferReader) -> ExportedGroupCallInvite? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.phone.ExportedGroupCallInvite.exportedGroupCallInvite(link: _1!) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum GroupCall: TypeConstructorDescription { - case groupCall(call: Api.GroupCall, participants: [Api.GroupCallParticipant], participantsNextOffset: String, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): - if boxed { - buffer.appendInt32(-1636664659) - } - call.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(participants.count)) - for item in participants { - item.serialize(buffer, true) - } - serializeString(participantsNextOffset, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): - return ("groupCall", [("call", String(describing: call)), ("participants", String(describing: participants)), ("participantsNextOffset", String(describing: participantsNextOffset)), ("chats", String(describing: chats)), ("users", String(describing: users))]) - } - } - - public static func parse_groupCall(_ reader: BufferReader) -> GroupCall? { - var _1: Api.GroupCall? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.GroupCall - } - var _2: [Api.GroupCallParticipant]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) - } - var _3: String? - _3 = parseString(reader) - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.phone.GroupCall.groupCall(call: _1!, participants: _2!, participantsNextOffset: _3!, chats: _4!, users: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index c867e6f525..d89e6d1747 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,111 @@ +public extension Api.phone { + enum ExportedGroupCallInvite: TypeConstructorDescription { + case exportedGroupCallInvite(link: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedGroupCallInvite(let link): + if boxed { + buffer.appendInt32(541839704) + } + serializeString(link, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedGroupCallInvite(let link): + return ("exportedGroupCallInvite", [("link", String(describing: link))]) + } + } + + public static func parse_exportedGroupCallInvite(_ reader: BufferReader) -> ExportedGroupCallInvite? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.phone.ExportedGroupCallInvite.exportedGroupCallInvite(link: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum GroupCall: TypeConstructorDescription { + case groupCall(call: Api.GroupCall, participants: [Api.GroupCallParticipant], participantsNextOffset: String, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): + if boxed { + buffer.appendInt32(-1636664659) + } + call.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(participants.count)) + for item in participants { + item.serialize(buffer, true) + } + serializeString(participantsNextOffset, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): + return ("groupCall", [("call", String(describing: call)), ("participants", String(describing: participants)), ("participantsNextOffset", String(describing: participantsNextOffset)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + } + } + + public static func parse_groupCall(_ reader: BufferReader) -> GroupCall? { + var _1: Api.GroupCall? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.GroupCall + } + var _2: [Api.GroupCallParticipant]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) + } + var _3: String? + _3 = parseString(reader) + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.phone.GroupCall.groupCall(call: _1!, participants: _2!, participantsNextOffset: _3!, chats: _4!, users: _5!) + } + else { + return nil + } + } + + } +} public extension Api.phone { enum GroupCallStreamChannels: TypeConstructorDescription { case groupCallStreamChannels(channels: [Api.GroupCallStreamChannel]) @@ -1318,59 +1426,3 @@ public extension Api.updates { } } -public extension Api.upload { - enum CdnFile: TypeConstructorDescription { - case cdnFile(bytes: Buffer) - case cdnFileReuploadNeeded(requestToken: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .cdnFile(let bytes): - if boxed { - buffer.appendInt32(-1449145777) - } - serializeBytes(bytes, buffer: buffer, boxed: false) - break - case .cdnFileReuploadNeeded(let requestToken): - if boxed { - buffer.appendInt32(-290921362) - } - serializeBytes(requestToken, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .cdnFile(let bytes): - return ("cdnFile", [("bytes", String(describing: bytes))]) - case .cdnFileReuploadNeeded(let requestToken): - return ("cdnFileReuploadNeeded", [("requestToken", String(describing: requestToken))]) - } - } - - public static func parse_cdnFile(_ reader: BufferReader) -> CdnFile? { - var _1: Buffer? - _1 = parseBytes(reader) - let _c1 = _1 != nil - if _c1 { - return Api.upload.CdnFile.cdnFile(bytes: _1!) - } - else { - return nil - } - } - public static func parse_cdnFileReuploadNeeded(_ reader: BufferReader) -> CdnFile? { - var _1: Buffer? - _1 = parseBytes(reader) - let _c1 = _1 != nil - if _c1 { - return Api.upload.CdnFile.cdnFileReuploadNeeded(requestToken: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index fda369bfdb..e66c5ef642 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1,3 +1,59 @@ +public extension Api.upload { + enum CdnFile: TypeConstructorDescription { + case cdnFile(bytes: Buffer) + case cdnFileReuploadNeeded(requestToken: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .cdnFile(let bytes): + if boxed { + buffer.appendInt32(-1449145777) + } + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .cdnFileReuploadNeeded(let requestToken): + if boxed { + buffer.appendInt32(-290921362) + } + serializeBytes(requestToken, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .cdnFile(let bytes): + return ("cdnFile", [("bytes", String(describing: bytes))]) + case .cdnFileReuploadNeeded(let requestToken): + return ("cdnFileReuploadNeeded", [("requestToken", String(describing: requestToken))]) + } + } + + public static func parse_cdnFile(_ reader: BufferReader) -> CdnFile? { + var _1: Buffer? + _1 = parseBytes(reader) + let _c1 = _1 != nil + if _c1 { + return Api.upload.CdnFile.cdnFile(bytes: _1!) + } + else { + return nil + } + } + public static func parse_cdnFileReuploadNeeded(_ reader: BufferReader) -> CdnFile? { + var _1: Buffer? + _1 = parseBytes(reader) + let _c1 = _1 != nil + if _c1 { + return Api.upload.CdnFile.cdnFileReuploadNeeded(requestToken: _1!) + } + else { + return nil + } + } + + } +} public extension Api.upload { enum File: TypeConstructorDescription { case file(type: Api.storage.FileType, mtime: Int32, bytes: Buffer) diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 8856435340..fda72e0ed6 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -344,12 +344,11 @@ public extension Api.functions.account { } } public extension Api.functions.account { - static func getEmojiStatuses(flags: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getDefaultEmojiStatuses(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(644729392) - serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(-696962170) serializeInt64(hash, buffer: buffer, boxed: false) - return (FunctionDescription(name: "account.getEmojiStatuses", parameters: [("flags", String(describing: flags)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.EmojiStatuses? in + return (FunctionDescription(name: "account.getDefaultEmojiStatuses", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.EmojiStatuses? in let reader = BufferReader(buffer) var result: Api.account.EmojiStatuses? if let signature = reader.readInt32() { @@ -469,6 +468,21 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func getRecentEmojiStatuses(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(257392901) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "account.getRecentEmojiStatuses", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.EmojiStatuses? in + let reader = BufferReader(buffer) + var result: Api.account.EmojiStatuses? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.account.EmojiStatuses + } + return result + }) + } +} public extension Api.functions.account { static func getSavedRingtones(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3392,6 +3406,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func clearRecentReactions() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1644236876) + + return (FunctionDescription(name: "messages.clearRecentReactions", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func clearRecentStickers(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4645,6 +4674,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getRecentReactions(limit: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(960896434) + serializeInt32(limit, buffer: buffer, boxed: false) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getRecentReactions", parameters: [("limit", String(describing: limit)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Reactions? in + let reader = BufferReader(buffer) + var result: Api.messages.Reactions? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.Reactions + } + return result + }) + } +} public extension Api.functions.messages { static func getRecentStickers(flags: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4853,6 +4898,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getTopReactions(limit: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1149164102) + serializeInt32(limit, buffer: buffer, boxed: false) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getTopReactions", parameters: [("limit", String(describing: limit)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Reactions? in + let reader = BufferReader(buffer) + var result: Api.messages.Reactions? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.Reactions + } + return result + }) + } +} public extension Api.functions.messages { static func getUnreadMentions(peer: Api.InputPeer, offsetId: Int32, addOffset: Int32, limit: Int32, maxId: Int32, minId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 39b4af6fa4..f8e274a16b 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1177,6 +1177,8 @@ public class Account { self.managedOperationsDisposable.add(managedAllPremiumStickers(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedRecentStatusEmoji(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedFeaturedStatusEmoji(postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedRecentReactions(postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedTopReactions(postbox: self.postbox, network: self.network).start()) if !supplementary { let mediaBox = postbox.mediaBox diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index f620d58f30..4a6710348a 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -12,11 +12,9 @@ private func hashForIds(_ ids: [Int64]) -> Int64 { return finalizeInt64Hash(acc) } -private func managedRecentMedia(postbox: Postbox, network: Network, collectionId: Int32, reverseHashOrder: Bool, forceFetch: Bool, fetch: @escaping (Int64) -> Signal<[OrderedItemListEntry]?, NoError>) -> Signal { +private func managedRecentMedia(postbox: Postbox, network: Network, collectionId: Int32, extractItemId: @escaping (MemoryBuffer) -> Int64?, reverseHashOrder: Bool, forceFetch: Bool, fetch: @escaping (Int64) -> Signal<[OrderedItemListEntry]?, NoError>) -> Signal { return postbox.transaction { transaction -> Signal in - var itemIds = transaction.getOrderedListItemIds(collectionId: collectionId).map { - RecentMediaItemId($0).mediaId.id - } + var itemIds = transaction.getOrderedListItemIds(collectionId: collectionId).compactMap(extractItemId) if reverseHashOrder { itemIds.reverse() } @@ -44,7 +42,7 @@ private func managedRecentMedia(postbox: Postbox, network: Network, collectionId } func managedRecentStickers(postbox: Postbox, network: Network) -> Signal { - return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentStickers, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in return network.request(Api.functions.messages.getRecentStickers(flags: 0, hash: hash)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in @@ -67,7 +65,7 @@ func managedRecentStickers(postbox: Postbox, network: Network) -> Signal Signal { - return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentGifs, reverseHashOrder: false, forceFetch: forceFetch, fetch: { hash in + return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentGifs, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: forceFetch, fetch: { hash in return network.request(Api.functions.messages.getSavedGifs(hash: hash)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in @@ -90,7 +88,7 @@ func managedRecentGifs(postbox: Postbox, network: Network, forceFetch: Bool = fa } func managedSavedStickers(postbox: Postbox, network: Network) -> Signal { - return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudSavedStickers, reverseHashOrder: true, forceFetch: false, fetch: { hash in + return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudSavedStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: true, forceFetch: false, fetch: { hash in return network.request(Api.functions.messages.getFavedStickers(hash: hash)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in @@ -132,7 +130,7 @@ func managedSavedStickers(postbox: Postbox, network: Network) -> Signal Signal { - let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudGreetingStickers, reverseHashOrder: false, forceFetch: false, fetch: { hash in + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudGreetingStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in return network.request(Api.functions.messages.getStickers(emoticon: "👋⭐️", hash: 0)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in @@ -156,7 +154,7 @@ func managedGreetingStickers(postbox: Postbox, network: Network) -> Signal Signal { - let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudPremiumStickers, reverseHashOrder: false, forceFetch: false, fetch: { hash in + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudPremiumStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in return network.request(Api.functions.messages.getStickers(emoticon: "⭐️⭐️", hash: 0)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in @@ -180,7 +178,7 @@ func managedPremiumStickers(postbox: Postbox, network: Network) -> Signal Signal { - let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudAllPremiumStickers, reverseHashOrder: false, forceFetch: false, fetch: { hash in + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudAllPremiumStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in return network.request(Api.functions.messages.getStickers(emoticon: "📂⭐️", hash: 0)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in @@ -204,8 +202,8 @@ func managedAllPremiumStickers(postbox: Postbox, network: Network) -> Signal Signal { - let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, reverseHashOrder: false, forceFetch: false, fetch: { hash in - return network.request(Api.functions.account.getEmojiStatuses(flags: 1 << 1, hash: hash)) + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.account.getRecentEmojiStatuses(hash: hash)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in switch result { @@ -234,8 +232,8 @@ func managedRecentStatusEmoji(postbox: Postbox, network: Network) -> Signal Signal { - let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedStatusEmoji, reverseHashOrder: false, forceFetch: false, fetch: { hash in - return network.request(Api.functions.account.getEmojiStatuses(flags: 1 << 0, hash: hash)) + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedStatusEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.account.getDefaultEmojiStatuses(hash: hash)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in switch result { @@ -262,3 +260,105 @@ func managedFeaturedStatusEmoji(postbox: Postbox, network: Network) -> Signal then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } + +func managedRecentReactions(postbox: Postbox, network: Network) -> Signal { + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentReactions, extractItemId: { rawId in + switch RecentReactionItemId(rawId).id { + case .builtin: + return 0 + case let .custom(fileId): + return fileId.id + } + }, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.messages.getRecentReactions(limit: 24, hash: hash)) + |> retryRequest + |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in + switch result { + case .reactionsNotModified: + return .single(nil) + case let .reactions(_, reactions): + let parsedReactions = reactions.compactMap(MessageReaction.Reaction.init(apiReaction:)) + + return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedReactions.compactMap { reaction -> Int64? in + switch reaction { + case .builtin: + return nil + case let .custom(fileId): + return fileId + } + }) + |> map { files -> [OrderedItemListEntry] in + var items: [OrderedItemListEntry] = [] + for reaction in parsedReactions { + let item: RecentReactionItem + switch reaction { + case let .builtin(value): + item = RecentReactionItem(.builtin(value)) + case let .custom(fileId): + guard let file = files[fileId] else { + continue + } + item = RecentReactionItem(.custom(file)) + } + if let entry = CodableEntry(item) { + items.append(OrderedItemListEntry(id: item.id.rawValue, contents: entry)) + } + } + return items + } + } + } + }) + return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} + +func managedTopReactions(postbox: Postbox, network: Network) -> Signal { + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudTopReactions, extractItemId: { rawId in + switch RecentReactionItemId(rawId).id { + case .builtin: + return 0 + case let .custom(fileId): + return fileId.id + } + }, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.messages.getTopReactions(limit: 24, hash: hash)) + |> retryRequest + |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in + switch result { + case .reactionsNotModified: + return .single(nil) + case let .reactions(_, reactions): + let parsedReactions = reactions.compactMap(MessageReaction.Reaction.init(apiReaction:)) + + return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedReactions.compactMap { reaction -> Int64? in + switch reaction { + case .builtin: + return nil + case let .custom(fileId): + return fileId + } + }) + |> map { files -> [OrderedItemListEntry] in + var items: [OrderedItemListEntry] = [] + for reaction in parsedReactions { + let item: RecentReactionItem + switch reaction { + case let .builtin(value): + item = RecentReactionItem(.builtin(value)) + case let .custom(fileId): + guard let file = files[fileId] else { + continue + } + item = RecentReactionItem(.custom(file)) + } + if let entry = CodableEntry(item) { + items.append(OrderedItemListEntry(id: item.id.rawValue, contents: entry)) + } + } + return items + } + } + } + }) + return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 6af7bd12b4..ac189d1b2d 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -57,6 +57,31 @@ public func updateMessageReactionsInteractively(account: Account, messageId: Mes } } + for attribute in currentMessage.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + for updatedReaction in reactions { + if !attribute.reactions.contains(where: { $0.value == updatedReaction.reaction }) { + let recentReactionItem: RecentReactionItem + switch updatedReaction { + case let .builtin(value): + recentReactionItem = RecentReactionItem(.builtin(value)) + case let .custom(fileId, file): + if let file = file ?? (transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile) { + recentReactionItem = RecentReactionItem(.custom(file)) + } else { + continue + } + } + + if let entry = CodableEntry(recentReactionItem) { + let itemEntry = OrderedItemListEntry(id: recentReactionItem.id.rawValue, contents: entry) + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentReactions, item: itemEntry, removeTailIfCountExceeds: 50) + } + } + } + } + } + var mappedReactions = mappedReactions let updatedReactions = mergedMessageReactions(attributes: attributes + [PendingReactionsMessageAttribute(accountPeerId: account.peerId, reactions: mappedReactions, isLarge: isLarge)])?.reactions ?? [] diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 867ed5690d..5279db8f1f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -70,6 +70,8 @@ public struct Namespaces { public static let CloudAllPremiumStickers: Int32 = 16 public static let CloudRecentStatusEmoji: Int32 = 17 public static let CloudFeaturedStatusEmoji: Int32 = 18 + public static let CloudRecentReactions: Int32 = 19 + public static let CloudTopReactions: Int32 = 20 } public struct CachedItemCollection { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift index c62293053d..935111050f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift @@ -152,3 +152,115 @@ public final class RecentEmojiItem: Codable, Equatable { return lhs.content == rhs.content } } + +public struct RecentReactionItemId { + public enum Id { + case custom(MediaId) + case builtin(String) + } + + public let rawValue: MemoryBuffer + public let id: Id + + public init(_ rawValue: MemoryBuffer) { + self.rawValue = rawValue + + assert(rawValue.length >= 1) + var type: UInt8 = 0 + memcpy(&type, rawValue.memory.advanced(by: 0), 1) + + if type == 0 { + assert(rawValue.length == 1 + 4 + 8) + var mediaIdNamespace: Int32 = 0 + var mediaIdId: Int64 = 0 + memcpy(&mediaIdNamespace, rawValue.memory.advanced(by: 1), 4) + memcpy(&mediaIdId, rawValue.memory.advanced(by: 1 + 4), 8) + self.id = .custom(MediaId(namespace: mediaIdNamespace, id: mediaIdId)) + } else if type == 1 { + var length: UInt16 = 0 + assert(rawValue.length >= 1 + 2) + memcpy(&length, rawValue.memory.advanced(by: 1), 2) + + assert(rawValue.length >= 1 + 2 + Int(length)) + + self.id = .builtin(String(data: Data(bytes: rawValue.memory.advanced(by: 1 + 2), count: Int(length)), encoding: .utf8) ?? ".") + } else { + assert(false) + self.id = .builtin(".") + } + } + + public init(_ mediaId: MediaId) { + self.id = .custom(mediaId) + + var mediaIdNamespace: Int32 = mediaId.namespace + var mediaIdId: Int64 = mediaId.id + self.rawValue = MemoryBuffer(memory: malloc(1 + 4 + 8)!, capacity: 1 + 4 + 8, length: 1 + 4 + 8, freeWhenDone: true) + var type: UInt8 = 0 + memcpy(self.rawValue.memory.advanced(by: 0), &type, 1) + memcpy(self.rawValue.memory.advanced(by: 1), &mediaIdNamespace, 4) + memcpy(self.rawValue.memory.advanced(by: 1 + 4), &mediaIdId, 8) + } + + public init(_ text: String) { + self.id = .builtin(text) + + let data = text.data(using: .utf8) ?? Data() + var length: UInt16 = UInt16(data.count) + + self.rawValue = MemoryBuffer(memory: malloc(1 + 2 + data.count)!, capacity: 1 + 2 + data.count, length: 1 + 2 + data.count, freeWhenDone: true) + var type: UInt8 = 1 + memcpy(self.rawValue.memory.advanced(by: 0), &type, 1) + memcpy(self.rawValue.memory.advanced(by: 1), &length, 2) + data.withUnsafeBytes { bytes in + let _ = memcpy(self.rawValue.memory.advanced(by: 1 + 2), bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), bytes.count) + } + } +} + +public final class RecentReactionItem: Codable, Equatable { + public enum Content: Equatable { + case custom(TelegramMediaFile) + case builtin(String) + } + + public let content: Content + + public var id: RecentReactionItemId { + switch self.content { + case let .builtin(value): + return RecentReactionItemId(value) + case let .custom(file): + return RecentReactionItemId(file.fileId) + } + } + + public init(_ content: Content) { + self.content = content + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + if let mediaData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: "m") { + self.content = .custom(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: mediaData.data)))) + } else { + self.content = .builtin(try container.decode(String.self, forKey: "s")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + switch self.content { + case let .custom(file): + try container.encode(PostboxEncoder().encodeObjectToRawData(file), forKey: "m") + case let .builtin(string): + try container.encode(string, forKey: "s") + } + } + + public static func ==(lhs: RecentReactionItem, rhs: RecentReactionItem) -> Bool { + return lhs.content == rhs.content + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index ffe2f7c731..23bf71082f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -4455,7 +4455,7 @@ public final class EmojiPagerContentComponent: Component { return hasPremium } - public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, reactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { + public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, topReactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -4468,14 +4468,24 @@ public final class EmojiPagerContentComponent: Component { if isStatusSelection { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) + } else if isReactionSelection { + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentReactions) + } + + let availableReactions: Signal + if isReactionSelection { + availableReactions = context.engine.stickers.availableReactions() + } else { + availableReactions = .single(nil) } let emojiItems: Signal = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), - context.account.viewTracker.featuredEmojiPacks() + context.account.viewTracker.featuredEmojiPacks(), + availableReactions ) - |> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in + |> map { view, hasPremium, featuredEmojiPacks, availableReactions -> EmojiPagerContentComponent in struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable @@ -4493,6 +4503,7 @@ public final class EmojiPagerContentComponent: Component { var recentEmoji: OrderedItemListView? var featuredStatusEmoji: OrderedItemListView? var recentStatusEmoji: OrderedItemListView? + var recentReactions: OrderedItemListView? for orderedView in view.orderedItemListsViews { if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { recentEmoji = orderedView @@ -4500,6 +4511,8 @@ public final class EmojiPagerContentComponent: Component { featuredStatusEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji { recentStatusEmoji = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentReactions { + recentReactions = orderedView } } @@ -4516,7 +4529,7 @@ public final class EmojiPagerContentComponent: Component { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: true, headerItem: nil, items: [resultItem])) } var existingIds = Set() @@ -4575,7 +4588,13 @@ public final class EmojiPagerContentComponent: Component { } } } else if isReactionSelection { - for reactionItem in reactionItems { + var existingIds = Set() + for reactionItem in topReactionItems { + if existingIds.contains(reactionItem.value) { + continue + } + existingIds.insert(reactionItem.value) + let animationFile = reactionItem.selectAnimation let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( @@ -4593,6 +4612,54 @@ public final class EmojiPagerContentComponent: Component { itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) } } + + if let recentReactions = recentReactions { + for item in recentReactions.items { + guard let item = item.contents.get(RecentReactionItem.self) else { + continue + } + + let animationFile: TelegramMediaFile + switch item.content { + case let .builtin(value): + if existingIds.contains(.builtin(value)) { + continue + } + existingIds.insert(.builtin(value)) + if let availableReactions = availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == .builtin(value) }) { + if let centerAnimation = availableReaction.centerAnimation { + animationFile = centerAnimation + } else { + continue + } + } else { + continue + } + case let .custom(file): + if existingIds.contains(.custom(file.fileId.id)) { + continue + } + existingIds.insert(.custom(file.fileId.id)) + animationFile = file + } + + let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: animationFile, + subgroupId: nil + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + } + } } if let recentEmoji = recentEmoji, !isReactionSelection, !isStatusSelection { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 4787fd71a9..75ef52957f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -985,18 +985,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = combineLatest(queue: .mainQueue(), strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction, messageNode: node as? ChatMessageItemView), - strongSelf.context.engine.stickers.availableReactions(), - peerAllowedReactions(context: strongSelf.context, peerId: topMessage.id.peerId), + peerMessageAllowedReactions(context: strongSelf.context, message: topMessage), + topMessageReactions(context: strongSelf.context, message: topMessage), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) - ).start(next: { peer, actions, availableReactions, allowedReactions, chatTextSelectionTips in + ).start(next: { peer, actions, allowedReactions, topReactions, chatTextSelectionTips in guard let strongSelf = self else { return } - var hasPremium = false + /*var hasPremium = false if case let .user(user) = peer, user.isPremium { hasPremium = true - } + }*/ var actions = actions switch actions.content { @@ -1045,50 +1045,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actions.animationCache = strongSelf.controllerInteraction?.presentationContext.animationCache - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) + //let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) - if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions { - var hasPremiumPlaceholder = false - filterReactions: for reaction in availableReactions.reactions { - guard let centerAnimation = reaction.centerAnimation else { - continue - } - guard let aroundAnimation = reaction.aroundAnimation else { - continue - } - if !reaction.isEnabled { - continue - } - - switch allowedReactions { - case let .set(set): - if !set.contains(reaction.value) { - continue filterReactions - } - case .all: - break - } - - if reaction.isPremium && !hasPremium { - hasPremiumPlaceholder = true - continue - } - - actions.reactionItems.append(.reaction(ReactionItem( - reaction: ReactionItem.Reaction(rawValue: reaction.value), - appearAnimation: reaction.appearAnimation, - stillAnimation: reaction.selectAnimation, - listAnimation: centerAnimation, - largeListAnimation: reaction.activateAnimation, - applicationAnimation: aroundAnimation, - largeApplicationAnimation: reaction.effectAnimation, - isCustom: false - ))) - } - - if hasPremiumPlaceholder && !premiumConfiguration.isPremiumDisabled { + if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty { + actions.reactionItems = topReactions.map(ReactionContextItem.reaction) + /*if hasPremiumPlaceholder && !premiumConfiguration.isPremiumDisabled { actions.reactionItems.append(.premium) - } + }*/ if !actions.reactionItems.isEmpty { let reactionItems: [AvailableReactions.Reaction] = actions.reactionItems.compactMap { item -> AvailableReactions.Reaction? in @@ -1118,12 +1081,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var allReactionsAreAvailable = false switch allowedReactions { - case let .set(set): - if set == Set(availableReactions.reactions.filter(\.isEnabled).map(\.value)) { - allReactionsAreAvailable = true - } else { - allReactionsAreAvailable = false - } + case .set: + allReactionsAreAvailable = false case .all: allReactionsAreAvailable = true } @@ -1145,7 +1104,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isStandalone: false, isStatusSelection: false, isReactionSelection: true, - reactionItems: reactionItems, + topReactionItems: reactionItems, areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: strongSelf.chatLocation.peerId @@ -1312,6 +1271,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break }*/ + if removedReaction == nil, case .custom = chosenReaction { + if !strongSelf.presentationInterfaceState.isPremium { + controller?.premiumReactionsSelected?() + return + } + } + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { if item.message.id == message.id { @@ -1588,7 +1554,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let _ = (peerAllowedReactions(context: strongSelf.context, peerId: message.id.peerId) + let _ = (peerMessageAllowedReactions(context: strongSelf.context, message: message) |> deliverOnMainQueue).start(next: { allowedReactions in guard let strongSelf = self else { return @@ -16981,12 +16947,16 @@ enum AllowedReactions { case all } -func peerAllowedReactions(context: AccountContext, peerId: PeerId) -> Signal { +func peerMessageAllowedReactions(context: AccountContext, message: Message) -> Signal { return context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId) + TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId), + TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: message.id.peerId) ) |> map { peer, allowedReactions -> AllowedReactions? in + if let effectiveReactions = message.effectiveReactions, effectiveReactions.count >= 11 { + return .set(Set(effectiveReactions.map(\.value))) + } + switch allowedReactions { case .unknown: return .all diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index adc7c416cd..ee04939bee 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -97,7 +97,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationRenderer = MultiAnimationRendererImpl() //} - let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) + let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] @@ -2018,7 +2018,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in + let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in emojiComponent = value semaphore.signal() }) @@ -2033,7 +2033,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV gifs: nil, availableGifSearchEmojies: [] ), - updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in + updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index b0101f7cb2..d9fd059380 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -1106,7 +1106,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return lhs.count > rhs.count } } - }).prefix(10) { + }) { let node: StatusReactionNode var animateNode = true if let current = strongSelf.reactionNodes[reaction.value] { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 55cf19dc48..ebff935be5 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -977,7 +977,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.openChat() })) - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_Report, color: .destructive, action: { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_ReportReaction, color: .destructive, action: { interaction.openReport(.reaction(reactionSourceMessageId)) })) } else if let _ = nearbyPeerDistance { @@ -1296,7 +1296,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr switch allowedReactions { case .all: //TODO:localize - label = "Enabled" + label = "All Reactions" case .empty: label = presentationData.strings.PeerInfo_ReactionsDisabled case let .limited(reactions): @@ -1462,7 +1462,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr switch allowedReactions { case .all: //TODO:localize - label = "Enabled" + label = "All Reactions" case .empty: label = presentationData.strings.PeerInfo_ReactionsDisabled case let .limited(reactions): @@ -1488,7 +1488,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr switch allowedReactions { case .all: //TODO:localize - label = "Enabled" + label = "All Reactions" case .empty: label = presentationData.strings.PeerInfo_ReactionsDisabled case let .limited(reactions): @@ -1602,7 +1602,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr switch allowedReactions { case .all: //TODO:localize - label = "Enabled" + label = "All Reactions" case .empty: label = presentationData.strings.PeerInfo_ReactionsDisabled case let .limited(reactions): @@ -3103,7 +3103,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate isStandalone: false, isStatusSelection: true, isReactionSelection: false, - reactionItems: [], + topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: strongSelf.context.account.peerId diff --git a/submodules/TelegramUI/Sources/TopMessageReactions.swift b/submodules/TelegramUI/Sources/TopMessageReactions.swift new file mode 100644 index 0000000000..37e5e14008 --- /dev/null +++ b/submodules/TelegramUI/Sources/TopMessageReactions.swift @@ -0,0 +1,140 @@ +import Foundation +import SwiftSignalKit +import TelegramCore +import Postbox +import AccountContext +import ReactionSelectionNode + +func topMessageReactions(context: AccountContext, message: Message) -> Signal<[ReactionItem], NoError> { + let viewKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudTopReactions) + let topReactions = context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> [RecentReactionItem] in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return [] + } + return view.items.compactMap { item -> RecentReactionItem? in + return item.contents.get(RecentReactionItem.self) + } + } + + return combineLatest( + context.engine.stickers.availableReactions(), + peerMessageAllowedReactions(context: context, message: message), + topReactions + ) + |> take(1) + |> map { availableReactions, allowedReactions, topReactions -> [ReactionItem] in + guard let availableReactions = availableReactions, let allowedReactions = allowedReactions else { + return [] + } + + var result: [ReactionItem] = [] + + var existingIds = Set() + + for topReaction in topReactions { + switch topReaction.content { + case let .builtin(value): + if let reaction = availableReactions.reactions.first(where: { $0.value == .builtin(value) }) { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + + if existingIds.contains(reaction.value) { + continue + } + existingIds.insert(reaction.value) + + switch allowedReactions { + case let .set(set): + if !set.contains(reaction.value) { + continue + } + case .all: + break + } + + result.append(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + )) + } else { + continue + } + case let .custom(file): + switch allowedReactions { + case let .set(set): + if !set.contains(.custom(file.fileId.id)) { + continue + } + case .all: + break + } + + if existingIds.contains(.custom(file.fileId.id)) { + continue + } + existingIds.insert(.custom(file.fileId.id)) + + result.append(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: .custom(file.fileId.id)), + appearAnimation: file, + stillAnimation: file, + listAnimation: file, + largeListAnimation: file, + applicationAnimation: nil, + largeApplicationAnimation: nil, + isCustom: true + )) + } + } + + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + if !reaction.isEnabled { + continue + } + + switch allowedReactions { + case let .set(set): + if !set.contains(reaction.value) { + continue + } + case .all: + break + } + + if existingIds.contains(reaction.value) { + continue + } + existingIds.insert(reaction.value) + + result.append(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + )) + } + + return result + } +} From 3b76a3cbfadd87de36ecc08fa8c7dd5c8cab7ae6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 23 Aug 2022 22:13:15 +0300 Subject: [PATCH 08/11] Reaction improvements --- .../Sources/ChatListSearchListPaneNode.swift | 70 +++++++---- .../Sources/ChatListTitleView.swift | 8 ++ .../Sources/Node/ChatListNode.swift | 8 +- .../Source/Base/Transition.swift | 12 +- .../Sources/ReactionButtonListComponent.swift | 2 +- submodules/ContactsPeerItem/BUILD | 4 + .../Sources/ContactsPeerItem.swift | 110 ++++++++++++---- .../ReactionContextBackgroundNode.swift | 2 +- .../Sources/ReactionContextNode.swift | 30 +++-- .../EmojiStatusSelectionComponent.swift | 13 +- .../Sources/EmojiPagerContentComponent.swift | 118 ++++++++++++++++-- .../Sources/ChatEntityKeyboardInputNode.swift | 9 +- .../ChatMessageDateAndStatusNode.swift | 18 ++- 13 files changed, 309 insertions(+), 95 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 91f5a322f9..51de4609a3 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -89,7 +89,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } - func item(context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (EnginePeer) -> Void, disabledPeerSelected: @escaping (EnginePeer) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (EnginePeer) -> Void, disabledPeerSelected: @escaping (EnginePeer) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> ListViewItem { switch self { case let .topPeers(peers, theme, strings): return ChatListRecentPeersListItem(theme: theme, strings: strings, context: context, peers: peers, peerSelected: { peer in @@ -195,25 +195,45 @@ private enum ChatListRecentEntry: Comparable, Identifiable { badge = ContactsPeerItemBadge(count: peer.unreadCount, type: isMuted ? .inactive : .active) } - return ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { - clearRecentlySearchedPeers() - }), action: { _ in - if let chatPeer = peer.peer.peers[peer.peer.peerId] { - peerSelected(EnginePeer(chatPeer)) - } - }, disabledAction: { _ in - if let chatPeer = peer.peer.peers[peer.peer.peerId] { - disabledPeerSelected(EnginePeer(chatPeer)) - } - }, deletePeer: deletePeer, contextAction: peerContextAction.flatMap { peerContextAction in - return { node, gesture, location in + return ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), + sortOrder: nameSortOrder, + displayOrder: nameDisplayOrder, + context: context, + peerMode: .generalSearch, + peer: .peer(peer: primaryPeer, chatPeer: chatPeer), + status: status, + badge: badge, + enabled: enabled, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { + clearRecentlySearchedPeers() + }), + action: { _ in if let chatPeer = peer.peer.peers[peer.peer.peerId] { - peerContextAction(EnginePeer(chatPeer), .recentSearch, node, gesture, location) - } else { - gesture?.cancel() + peerSelected(EnginePeer(chatPeer)) } - } - }) + }, + disabledAction: { _ in + if let chatPeer = peer.peer.peers[peer.peer.peerId] { + disabledPeerSelected(EnginePeer(chatPeer)) + } + }, + deletePeer: deletePeer, + contextAction: peerContextAction.flatMap { peerContextAction in + return { node, gesture, location in + if let chatPeer = peer.peer.peers[peer.peer.peerId] { + peerContextAction(EnginePeer(chatPeer), .recentSearch, node, gesture, location) + } else { + gesture?.cancel() + } + } + }, + animationCache: animationCache, + animationRenderer: animationRenderer + ) } } } @@ -497,7 +517,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { gesture?.cancel() } } - }, arrowAction: nil) + }, arrowAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer) case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): let primaryPeer: EnginePeer var chatPeer: EnginePeer? @@ -584,7 +604,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { gesture?.cancel() } } - }, arrowAction: nil) + }, arrowAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer) case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): var enabled = true if filter.contains(.onlyWriteable) { @@ -643,7 +663,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return { node, gesture, location in peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture, location) } - }) + }, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer) case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused): let header: ChatListSearchItemHeader switch orderingKey { @@ -712,12 +732,12 @@ public struct ChatListSearchContainerTransition { } } -private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (EnginePeer) -> Void, disabledPeerSelected: @escaping (EnginePeer) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void) -> ChatListSearchContainerRecentTransition { +private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (EnginePeer) -> Void, disabledPeerSelected: @escaping (EnginePeer) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> ChatListSearchContainerRecentTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer), directionHint: nil) } return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -2058,7 +2078,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { interaction.clearRecentSearch() }, deletePeer: { peerId in let _ = context.engine.peers.removeRecentlySearchedPeer(peerId: peerId).start() - }) + }, animationCache: strongSelf.animationCache, animationRenderer: strongSelf.animationRenderer) strongSelf.enqueueRecentTransition(transition, firstTime: firstTime) } })) diff --git a/submodules/ChatListUI/Sources/ChatListTitleView.swift b/submodules/ChatListUI/Sources/ChatListTitleView.swift index c6f9eb02f3..d75df8b91a 100644 --- a/submodules/ChatListUI/Sources/ChatListTitleView.swift +++ b/submodules/ChatListUI/Sources/ChatListTitleView.swift @@ -435,6 +435,14 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let titleCredibilityIconView = self.titleCredibilityIconView, !titleCredibilityIconView.isHidden, titleCredibilityIconView.alpha != 0.0 { + if titleCredibilityIconView.bounds.insetBy(dx: -8.0, dy: -8.0).contains(self.convert(point, to: titleCredibilityIconView)) { + if let result = titleCredibilityIconView.hitTest(titleCredibilityIconView.bounds.center, with: event) { + return result + } + } + } + if !self.proxyButton.isHidden { if let result = self.proxyButton.hitTest(point.offsetBy(dx: -self.proxyButton.frame.minX, dy: -self.proxyButton.frame.minY), with: event) { return result; diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index cb0635854b..bb61c8a6e7 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -393,7 +393,9 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL if let chatPeer = chatPeer { nodeInteraction.disabledPeerSelected(chatPeer) } - } + }, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): @@ -533,7 +535,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL if let chatPeer = chatPeer { nodeInteraction.disabledPeerSelected(chatPeer) } - } + }, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index c6a8334e42..cb988e2aea 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -390,7 +390,11 @@ public struct Transition { } public func setScale(view: UIView, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { - let t = view.layer.presentation()?.transform ?? view.layer.transform + self.setScale(layer: view.layer, scale: scale, completion: completion) + } + + public func setScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + let t = layer.presentation()?.transform ?? layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale == scale { completion?(true) @@ -398,12 +402,12 @@ public struct Transition { } switch self.animation { case .none: - view.layer.transform = CATransform3DMakeScale(scale, scale, 1.0) + layer.transform = CATransform3DMakeScale(scale, scale, 1.0) completion?(true) case let .curve(duration, curve): let previousScale = currentScale - view.layer.transform = CATransform3DMakeScale(scale, scale, 1.0) - view.layer.animate( + layer.transform = CATransform3DMakeScale(scale, scale, 1.0) + layer.animate( from: previousScale as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index daa8aaf93f..a6b076a22d 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -746,7 +746,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { fileId: fileId, animationCache: arguments.animationCache, animationRenderer: arguments.animationRenderer, - placeholderColor: .gray, + placeholderColor: layout.spec.component.chosenOrder != nil ? UIColor(argb: layout.spec.component.colors.selectedForeground).withMultipliedAlpha(0.1) : UIColor(argb: layout.spec.component.colors.deselectedForeground).withMultipliedAlpha(0.1), animateIdle: animateIdle, reaction: layout.spec.component.reaction.value, transition: animation.transition diff --git a/submodules/ContactsPeerItem/BUILD b/submodules/ContactsPeerItem/BUILD index af0de6ee9a..a406bb700c 100644 --- a/submodules/ContactsPeerItem/BUILD +++ b/submodules/ContactsPeerItem/BUILD @@ -26,6 +26,10 @@ swift_library( "//submodules/ListSectionHeaderNode:ListSectionHeaderNode", "//submodules/ContextUI:ContextUI", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 050eb5789e..426a909085 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -16,6 +16,10 @@ import PeerPresenceStatusManager import ItemListPeerItem import ContextUI import AccountContext +import ComponentFlow +import AnimationCache +import MultiAnimationRenderer +import EmojiStatusComponent public final class ContactItemHighlighting { public var chatLocation: ChatLocation? @@ -150,6 +154,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let itemHighlighting: ContactItemHighlighting? let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? let arrowAction: (() -> Void)? + let animationCache: AnimationCache? + let animationRenderer: MultiAnimationRenderer? public let selectable: Bool @@ -157,7 +163,35 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { public let header: ListViewItemHeader? - public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, selectionPosition: ContactsPeerItemSelectionPosition = .right, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: SortIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil, deletePeer: ((EnginePeer.Id) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil) { + public init( + presentationData: ItemListPresentationData, + style: ItemListStyle = .plain, + sectionId: ItemListSectionId = 0, + sortOrder: PresentationPersonNameOrder, + displayOrder: PresentationPersonNameOrder, + context: AccountContext, + peerMode: ContactsPeerItemPeerMode, + peer: ContactsPeerItemPeer, + status: ContactsPeerItemStatus, + badge: ContactsPeerItemBadge? = nil, + enabled: Bool, + selection: ContactsPeerItemSelection, + selectionPosition: ContactsPeerItemSelectionPosition = .right, + editing: ContactsPeerItemEditing, + options: [ItemListPeerItemRevealOption] = [], + additionalActions: [ContactsPeerItemAction] = [], + actionIcon: ContactsPeerItemActionIcon = .none, + index: SortIndex?, + header: ListViewItemHeader?, + action: @escaping (ContactsPeerItemPeer) -> Void, + disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, + setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil, + deletePeer: ((EnginePeer.Id) -> Void)? = nil, + itemHighlighting: ContactItemHighlighting? = nil, + contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil, + animationCache: AnimationCache? = nil, + animationRenderer: MultiAnimationRenderer? = nil + ) { self.presentationData = presentationData self.style = style self.sectionId = sectionId @@ -184,6 +218,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.selectable = enabled || disabledAction != nil self.contextAction = contextAction self.arrowAction = arrowAction + self.animationCache = animationCache + self.animationRenderer = animationRenderer if let index = index { var letter: String = "#" @@ -333,7 +369,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let avatarNode: AvatarNode private let titleNode: TextNode - private var credibilityIconNode: ASImageNode? + private var credibilityIconView: ComponentHostView? private let statusNode: TextNode private var badgeBackgroundNode: ASImageNode? private var badgeTextNode: TextNode? @@ -571,18 +607,20 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) - var currentCredibilityIconImage: UIImage? + var credibilityIcon: EmojiStatusComponent.Content? switch item.peer { case let .peer(peer, _): if let peer = peer, peer.id != item.context.account.peerId { if peer.isScam { - currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + credibilityIcon = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor) } else if peer.isFake { - currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor) + } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus { + credibilityIcon = .emojiStatus(status: emojiStatus, size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) } else if peer.isVerified { - currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { - currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) + credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor) } } case .deviceContact: @@ -743,8 +781,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } var additionalTitleInset: CGFloat = 0.0 - if let currentCredibilityIconImage = currentCredibilityIconImage { - additionalTitleInset += 3.0 + currentCredibilityIconImage.size.width + if let credibilityIcon = credibilityIcon { + additionalTitleInset += 3.0 + switch credibilityIcon { + case .scam, .fake: + additionalTitleInset += 30.0 + default: + additionalTitleInset += 16.0 + } } if let actionButtons = actionButtons { additionalTitleInset += 3.0 @@ -930,23 +974,35 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.frame = statusFrame transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) - if let currentCredibilityIconImage = currentCredibilityIconImage { - let iconNode: ASImageNode - if let current = strongSelf.credibilityIconNode { - iconNode = current + if let credibilityIcon = credibilityIcon, let animationCache = item.animationCache, let animationRenderer = item.animationRenderer { + let credibilityIconView: ComponentHostView + if let current = strongSelf.credibilityIconView { + credibilityIconView = current } else { - iconNode = ASImageNode() - iconNode.isLayerBacked = true - iconNode.displaysAsynchronously = false - iconNode.displayWithoutProcessing = true - strongSelf.offsetContainerNode.addSubnode(iconNode) - strongSelf.credibilityIconNode = iconNode + credibilityIconView = ComponentHostView() + strongSelf.offsetContainerNode.view.addSubview(credibilityIconView) + strongSelf.credibilityIconView = credibilityIconView } - iconNode.image = currentCredibilityIconImage - transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - currentCredibilityIconImage.size.height / 2.0) - UIScreenPixel), size: currentCredibilityIconImage.size)) - } else if let credibilityIconNode = strongSelf.credibilityIconNode { - strongSelf.credibilityIconNode = nil - credibilityIconNode.removeFromSupernode() + + let iconSize = credibilityIconView.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: item.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + content: credibilityIcon, + action: nil, + longTapAction: nil, + emojiFileUpdated: nil + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + + transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + } else if let credibilityIconView = strongSelf.credibilityIconView { + strongSelf.credibilityIconView = nil + credibilityIconView.removeFromSuperview() } if let actionButtons = actionButtons { @@ -1146,10 +1202,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.statusNode.frame = statusFrame transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) - if let credibilityIconNode = self.credibilityIconNode { - var iconFrame = credibilityIconNode.frame + if let credibilityIconView = self.credibilityIconView { + var iconFrame = credibilityIconView.frame iconFrame.origin.x = titleFrame.maxX + 4.0 - transition.updateFrame(node: credibilityIconNode, frame: iconFrame) + transition.updateFrame(view: credibilityIconView, frame: iconFrame) } if let badgeBackgroundNode = self.badgeBackgroundNode, let badgeTextNode = self.badgeTextNode { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index b998f72ac3..ebbd3ca2c6 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -215,7 +215,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { } func animateInFromAnchorRect(size: CGSize, sourceBackgroundFrame: CGRect) { - let springDuration: Double = 0.3 + let springDuration: Double = 0.5 let springDamping: CGFloat = 104.0 let springDelay: Double = 0.05 let shadowInset: CGFloat = 15.0 diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 5f42662e14..6c2d858c07 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -114,7 +114,7 @@ private final class ExpandItemView: UIView { transition.updateCornerRadius(layer: self.tintView.layer, cornerRadius: size.width / 2.0) if let image = self.arrowView.image { - transition.updateFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels(size.height - size.width + (size.width - image.size.height) / 2.0)), size: image.size)) + transition.updateFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels(size.height - size.width + (size.width - image.size.height) / 2.0 + 1.0)), size: image.size)) } } } @@ -874,7 +874,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } if let animateInFromAnchorRect = animateInFromAnchorRect { - let springDuration: Double = 0.3 + let springDuration: Double = 0.5 let springDamping: CGFloat = 104.0 let springDelay: Double = 0.05 @@ -902,11 +902,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { [weak self] groupId, item, sourceView, sourceRect, sourceLayer in + performItemAction: { [weak self] groupId, item, sourceView, sourceRect, sourceLayer, isLongPress in guard let strongSelf = self, let itemFile = item.itemFile else { return } + strongSelf.didTriggerExpandedReaction = isLongPress + var found = false if let groupId = groupId.base as? String, groupId == "recent" { for reactionItem in strongSelf.items { @@ -915,7 +917,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { found = true strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) - strongSelf.reactionSelected?(reactionItem.updateMessageReaction, false) + strongSelf.reactionSelected?(reactionItem.updateMessageReaction, isLongPress) break } @@ -934,7 +936,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isCustom: true ) strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) - strongSelf.reactionSelected?(reactionItem.updateMessageReaction, false) + strongSelf.reactionSelected?(reactionItem.updateMessageReaction, isLongPress) } }, deleteBackwards: { @@ -1157,10 +1159,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { case .builtin: switchToInlineImmediately = false case .custom: - switchToInlineImmediately = true + switchToInlineImmediately = !self.didTriggerExpandedReaction } } else { - switchToInlineImmediately = true + switchToInlineImmediately = !self.didTriggerExpandedReaction } self.animationTargetView = targetView @@ -1192,7 +1194,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { var expandedSize: CGSize = selfTargetRect.size if self.didTriggerExpandedReaction { - expandedSize = CGSize(width: 120.0, height: 120.0) + if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker { + expandedSize = CGSize(width: 80.0, height: 80.0) + } else { + expandedSize = CGSize(width: 120.0, height: 120.0) + } } let expandedFrame = CGRect(origin: CGPoint(x: selfTargetRect.midX - expandedSize.width / 2.0, y: selfTargetRect.midY - expandedSize.height / 2.0), size: expandedSize) @@ -1200,7 +1206,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let effectFrame: CGRect let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 if self.didTriggerExpandedReaction { - effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5).offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) + let expandFactor: CGFloat = 0.5 + effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * expandFactor, dy: -expandedFrame.height * expandFactor).offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) } else { effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height) } @@ -1265,7 +1272,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { cache: animationCache, renderer: animationRenderer, placeholderColor: UIColor(white: 0.0, alpha: 0.0), - pointSize: CGSize(width: 32.0, height: 32.0) + pointSize: CGSize(width: self.didTriggerExpandedReaction ? 64.0 : 32.0, height: self.didTriggerExpandedReaction ? 64.0 : 32.0) ) if let sublayers = animationLayer.sublayers { @@ -1484,6 +1491,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let expandItemView = self.expandItemView, expandItemView.bounds.contains(self.view.convert(point, to: self.expandItemView)) { self.currentContentHeight = 300.0 self.isExpanded = true + self.longPressRecognizer?.isEnabled = false self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring)) } else if let reaction = self.reaction(at: point) { switch reaction { @@ -1504,6 +1512,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } self.hapticFeedback?.tap() + self.longPressRecognizer?.isEnabled = false + self.animateFromExtensionDistance = self.extensionDistance self.extensionDistance = 0.0 self.visibleExtensionDistance = 0.0 diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index e4b0ee2797..7bf18914b6 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -235,7 +235,7 @@ public final class EmojiStatusSelectionController: ViewController { strongSelf.emojiContent = emojiContent emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { _, item, _, _, _ in + performItemAction: { _, item, _, _, _, _ in guard let strongSelf = self else { return } @@ -416,16 +416,14 @@ public final class EmojiStatusSelectionController: ViewController { transition.setFrame(layer: self.cloudShadowLayer1, frame: cloudFrame1) transition.setCornerRadius(layer: self.cloudLayer1, cornerRadius: cloudFrame1.width / 2.0) - //transition.setFrame(view: componentView, frame: componentFrame) transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) if animateIn { - //self.allowsGroupOpacity = true self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self] _ in self?.allowsGroupOpacity = false }) - let contentDuration: Double = 0.5 + let contentDuration: Double = 0.3 let contentDelay: Double = 0.14 let initialContentFrame = CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: componentFrame.minY), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)) @@ -439,16 +437,9 @@ public final class EmojiStatusSelectionController: ViewController { componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay) self.componentShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay) - //componentView.layer.animateScale(from: 0.5, to: 1.0, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) - //self.componentShadowLayer.animateScale(from: 0.5, to: 1.0, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) - let initialComponentShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: initialContentFrame.size), cornerRadius: 24.0).cgPath self.componentShadowLayer.animate(from: initialComponentShadowPath, to: self.componentShadowLayer.shadowPath!, keyPath: "shadowPath", timingFunction: kCAMediaTimingFunctionSpring, duration: contentDuration, delay: contentDelay) - //componentView.layer.animateScale(from: (componentView.bounds.width - 10.0) / componentView.bounds.width, to: 1.0, duration: 0.4, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring) - //componentView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: contentDelay) - //self.componentShadowLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: contentDelay) - self.cloudLayer0.animateScale(from: 0.01, to: 1.0, duration: 0.4, delay: 0.05, timingFunction: kCAMediaTimingFunctionSpring) self.cloudShadowLayer0.animateScale(from: 0.01, to: 1.0, duration: 0.4, delay: 0.05, timingFunction: kCAMediaTimingFunctionSpring) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 23bf71082f..f07a5d5ebb 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1543,7 +1543,7 @@ public final class EmojiPagerContentComponent: Component { } public final class InputInteraction { - public let performItemAction: (AnyHashable, Item, UIView, CGRect, CALayer) -> Void + public let performItemAction: (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void public let deleteBackwards: () -> Void public let openStickerSettings: () -> Void public let openFeatured: () -> Void @@ -1560,7 +1560,7 @@ public final class EmojiPagerContentComponent: Component { public let externalBackground: ExternalBackground? public init( - performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer) -> Void, + performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void, deleteBackwards: @escaping () -> Void, openStickerSettings: @escaping () -> Void, openFeatured: @escaping () -> Void, @@ -1777,6 +1777,7 @@ public final class EmojiPagerContentComponent: Component { public let itemGroups: [ItemGroup] public let itemLayoutType: ItemLayoutType public let warpContentsOnEdges: Bool + public let enableLongPress: Bool public init( id: AnyHashable, @@ -1787,7 +1788,8 @@ public final class EmojiPagerContentComponent: Component { inputInteractionHolder: InputInteractionHolder, itemGroups: [ItemGroup], itemLayoutType: ItemLayoutType, - warpContentsOnEdges: Bool + warpContentsOnEdges: Bool, + enableLongPress: Bool ) { self.id = id self.context = context @@ -1798,6 +1800,7 @@ public final class EmojiPagerContentComponent: Component { self.itemGroups = itemGroups self.itemLayoutType = itemLayoutType self.warpContentsOnEdges = warpContentsOnEdges + self.enableLongPress = enableLongPress } public static func ==(lhs: EmojiPagerContentComponent, rhs: EmojiPagerContentComponent) -> Bool { @@ -1831,6 +1834,9 @@ public final class EmojiPagerContentComponent: Component { if lhs.warpContentsOnEdges != rhs.warpContentsOnEdges { return false } + if lhs.enableLongPress != rhs.enableLongPress { + return false + } return true } @@ -2556,6 +2562,8 @@ public final class EmojiPagerContentComponent: Component { private var activeItemUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>? private var itemLayout: ItemLayout? + private var longTapRecognizer: UILongPressGestureRecognizer? + override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: nil) @@ -2607,6 +2615,12 @@ public final class EmojiPagerContentComponent: Component { self.scrollView.addSubview(self.placeholdersContainerView) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:))) + longTapRecognizer.minimumPressDuration = 0.2 + self.longTapRecognizer = longTapRecognizer + self.addGestureRecognizer(longTapRecognizer) + longTapRecognizer.isEnabled = false } required init?(coder: NSCoder) { @@ -2665,7 +2679,7 @@ public final class EmojiPagerContentComponent: Component { let distance = sqrt(distanceVector.x * distanceVector.x + distanceVector.y * distanceVector.y) let distanceNorm = min(1.0, max(0.0, distance / self.bounds.width)) - let delay = 0.05 + (distanceNorm) * 0.4 + let delay = 0.05 + (distanceNorm) * 0.3 itemLayer.animateScale(from: 0.01, to: 1.0, duration: 0.18, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) } } @@ -2692,7 +2706,7 @@ public final class EmojiPagerContentComponent: Component { let clippedDistance = max(0.0, min(distance, maxDistance)) let distanceNorm = clippedDistance / maxDistance - let delay = listViewAnimationCurveSystem(distanceNorm) * 0.16 + let delay = listViewAnimationCurveSystem(distanceNorm) * 0.1 itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) itemLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, delay: delay) @@ -3196,7 +3210,7 @@ public final class EmojiPagerContentComponent: Component { foundExactItem = true foundItem = true if !itemLayer.displayPlaceholder { - component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer) + component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false) } } @@ -3204,7 +3218,7 @@ public final class EmojiPagerContentComponent: Component { if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self), extendedHitRange: true), let itemLayer = self.visibleItemLayers[itemKey] { foundItem = true if !itemLayer.displayPlaceholder { - component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer) + component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false) } } } @@ -3213,6 +3227,87 @@ public final class EmojiPagerContentComponent: Component { } } + private let longPressDuration: Double = 0.5 + private var longPressItem: EmojiPagerContentComponent.View.ItemLayer.Key? + private var hapticFeedback: HapticFeedback? + private var continuousHaptic: AnyObject? + private var longPressTimer: SwiftSignalKit.Timer? + + @objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) { + switch recognizer.state { + case .began: + let point = recognizer.location(in: self) + + guard let item = self.item(atPoint: point), let itemLayer = self.visibleItemLayers[item.1] else { + return + } + switch item.0.content { + case .animation: + break + default: + return + } + self.longPressItem = item.1 + + if #available(iOS 13.0, *) { + self.continuousHaptic = try? ContinuousHaptic(duration: longPressDuration) + } + + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + + let transition = Transition(animation: .curve(duration: longPressDuration, curve: .easeInOut)) + transition.setScale(layer: itemLayer, scale: 1.3) + + self.longPressTimer?.invalidate() + self.longPressTimer = SwiftSignalKit.Timer(timeout: longPressDuration, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.longTapRecognizer?.state = .ended + }, queue: .mainQueue()) + self.longPressTimer?.start() + case .changed: + let point = recognizer.location(in: self) + + if let longPressItem = self.longPressItem, let item = self.item(atPoint: point), longPressItem == item.1 { + } else { + self.longTapRecognizer?.state = .cancelled + } + case .cancelled: + self.longPressTimer?.invalidate() + self.continuousHaptic = nil + + if let itemKey = self.longPressItem { + self.longPressItem = nil + + if let itemLayer = self.visibleItemLayers[itemKey] { + let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + transition.setScale(layer: itemLayer, scale: 1.0) + } + } + case .ended: + self.longPressTimer?.invalidate() + self.continuousHaptic = nil + + if let itemKey = self.longPressItem { + self.longPressItem = nil + + if let component = self.component, let itemLayer = self.visibleItemLayers[itemKey] { + component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, itemLayer.item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, true) + } else { + if let itemLayer = self.visibleItemLayers[itemKey] { + let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + transition.setScale(layer: itemLayer, scale: 1.0) + } + } + } + default: + break + } + } + private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (Item, ItemLayer.Key)? { let localPoint = self.convert(point, to: self.scrollView) @@ -3411,7 +3506,7 @@ public final class EmojiPagerContentComponent: Component { guard let strongSelf = self, let component = strongSelf.component else { return } - component.inputInteractionHolder.inputInteraction?.performItemAction(groupId, item, view, rect, layer) + component.inputInteractionHolder.inputInteraction?.performItemAction(groupId, item, view, rect, layer, false) } ) self.visibleGroupHeaders[itemGroup.groupId] = groupHeaderView @@ -4133,6 +4228,10 @@ public final class EmojiPagerContentComponent: Component { self.updateIsWarpEnabled(isEnabled: component.warpContentsOnEdges) + if let longTapRecognizer = self.longTapRecognizer { + longTapRecognizer.isEnabled = component.enableLongPress + } + if let shimmerHostView = self.shimmerHostView { transition.setFrame(view: shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize)) } @@ -4892,7 +4991,8 @@ public final class EmojiPagerContentComponent: Component { ) }, itemLayoutType: .compact, - warpContentsOnEdges: isReactionSelection || isStatusSelection + warpContentsOnEdges: isReactionSelection || isStatusSelection, + enableLongPress: isReactionSelection ) } return emojiItems diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index ee04939bee..988d9d0867 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -505,7 +505,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { ) }, itemLayoutType: .detailed, - warpContentsOnEdges: false + warpContentsOnEdges: false, + enableLongPress: false ) } @@ -902,7 +903,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var premiumToastCounter = 0 self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] _, item, _, _, _ in + performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] _, item, _, _, _, _ in let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { return @@ -1099,7 +1100,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { ) } self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { [weak controllerInteraction, weak interfaceInteraction] groupId, item, view, rect, layer in + performItemAction: { [weak controllerInteraction, weak interfaceInteraction] groupId, item, view, rect, layer, _ in let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { return @@ -1918,7 +1919,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV self.clipsToBounds = true let inputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { [weak self] _, item, _, _, _ in + performItemAction: { [weak self] _, item, _, _, _, _ in let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index d9fd059380..a9323b462a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -143,6 +143,22 @@ private final class StatusReactionNode: ASDisplayNode { animateIdle = false } + let placeholderColor: UIColor + switch type { + case .BubbleIncoming: + placeholderColor = theme.chat.message.incoming.mediaPlaceholderColor + case .BubbleOutgoing: + placeholderColor = theme.chat.message.incoming.mediaPlaceholderColor + case .ImageIncoming: + placeholderColor = UIColor(white: 1.0, alpha: 0.1) + case .ImageOutgoing: + placeholderColor = UIColor(white: 1.0, alpha: 0.1) + case .FreeIncoming: + placeholderColor = UIColor(white: 0.0, alpha: 0.1) + case .FreeOutgoing: + placeholderColor = UIColor(white: 0.0, alpha: 0.1) + } + self.iconView.update( size: boundingImageSize, context: context, @@ -150,7 +166,7 @@ private final class StatusReactionNode: ASDisplayNode { fileId: fileId, animationCache: animationCache, animationRenderer: animationRenderer, - placeholderColor: .gray, + placeholderColor: placeholderColor, animateIdle: animateIdle, reaction: value, transition: .immediate From 5ae5d30f4eb836073c1a841902334632b20f554e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 23 Aug 2022 23:17:26 +0300 Subject: [PATCH 09/11] Adjust reaction menu animation --- .../ReactionContextBackgroundNode.swift | 13 +++++--- .../Sources/ReactionContextNode.swift | 32 +++++++++++++++---- .../Sources/ReactionSelectionNode.swift | 6 +++- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index ebbd3ca2c6..605b7c937f 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -217,7 +217,8 @@ final class ReactionContextBackgroundNode: ASDisplayNode { func animateInFromAnchorRect(size: CGSize, sourceBackgroundFrame: CGRect) { let springDuration: Double = 0.5 let springDamping: CGFloat = 104.0 - let springDelay: Double = 0.05 + let springScaleDelay: Double = 0.1 + let springDelay: Double = springScaleDelay + 0.01 let shadowInset: CGFloat = 15.0 let contentBounds = self.backgroundView.frame @@ -225,6 +226,9 @@ final class ReactionContextBackgroundNode: ASDisplayNode { let visualSourceBackgroundFrame = sourceBackgroundFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY) let sourceShadowFrame = visualSourceBackgroundFrame.insetBy(dx: -shadowInset, dy: -shadowInset) + self.backgroundClippingLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2, delay: springScaleDelay, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.backgroundShadowLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2, delay: springScaleDelay, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.backgroundClippingLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: visualSourceBackgroundFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.backgroundClippingLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualSourceBackgroundFrame.size)), to: NSValue(cgRect: self.backgroundClippingLayer.bounds), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) self.backgroundShadowLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceShadowFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) @@ -232,11 +236,12 @@ final class ReactionContextBackgroundNode: ASDisplayNode { } func animateOut() { - self.backgroundClippingLayer.animateAlpha(from: CGFloat(self.backgroundClippingLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + self.maskLayer.allowsGroupOpacity = true + self.maskLayer.animateAlpha(from: CGFloat(self.backgroundClippingLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) self.backgroundShadowLayer.animateAlpha(from: CGFloat(self.backgroundShadowLayer.opacity), to: 0.0, duration: 0.1, removeOnCompletion: false) - self.largeCircleLayer.animateAlpha(from: CGFloat(self.largeCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + //self.largeCircleLayer.animateAlpha(from: CGFloat(self.largeCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) self.largeCircleShadowLayer.animateAlpha(from: CGFloat(self.largeCircleShadowLayer.opacity), to: 0.0, duration: 0.1, removeOnCompletion: false) - self.smallCircleLayer.animateAlpha(from: CGFloat(self.smallCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + //self.smallCircleLayer.animateAlpha(from: CGFloat(self.smallCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) self.smallCircleShadowLayer.animateAlpha(from: CGFloat(self.smallCircleShadowLayer.opacity), to: 0.0, duration: 0.1, removeOnCompletion: false) } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 6c2d858c07..f5c1982f3a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -191,6 +191,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var horizontalExpandStartLocation: CGPoint? private var horizontalExpandDistance: CGFloat = 0.0 + private var animateInInfo: (centerX: CGFloat, width: CGFloat)? + public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { self.context = context self.presentationData = presentationData @@ -876,14 +878,16 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let animateInFromAnchorRect = animateInFromAnchorRect { let springDuration: Double = 0.5 let springDamping: CGFloat = 104.0 - let springDelay: Double = 0.05 + let springScaleDelay: Double = 0.1 + let springDelay: Double = springScaleDelay + 0.01 let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: visualBackgroundFrame.height, height: contentHeight)).0 self.backgroundNode.animateInFromAnchorRect(size: visualBackgroundFrame.size, sourceBackgroundFrame: sourceBackgroundFrame.offsetBy(dx: -visualBackgroundFrame.minX, dy: -visualBackgroundFrame.minY)) + self.animateInInfo = (visualBackgroundFrame.midX - sourceBackgroundFrame.midX, visualBackgroundFrame.width) self.contentContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - visualBackgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) - self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(x: -(sourceBackgroundFrame.midX - visualBackgroundFrame.midX), y: 0.0), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) self.contentTintContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - visualBackgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.contentTintContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) @@ -1002,7 +1006,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil) } - //let mainCircleDuration: Double = 0.5 let mainCircleDelay: Double = 0.01 self.backgroundNode.animateIn() @@ -1014,14 +1017,31 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { guard let itemNode = self.visibleItemNodes[i] else { continue } - let itemDelay = mainCircleDelay + Double(i) * 0.06 - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay, execute: { [weak itemNode] in + + let itemDelay: Double + if let animateInInfo = self.animateInInfo { + let distance = abs(itemNode.frame.center.x - animateInInfo.centerX) + let distanceNorm = distance / animateInInfo.width + itemDelay = mainCircleDelay + distanceNorm * 0.4 + } else { + itemDelay = mainCircleDelay + Double(i) * 0.06 + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay * UIView.animationDurationFactor(), execute: { [weak itemNode] in itemNode?.appear(animated: true) }) } - let itemDelay = mainCircleDelay + Double(self.visibleItemNodes.count) * 0.06 if let expandItemView = self.expandItemView { + let itemDelay: Double + if let animateInInfo = self.animateInInfo { + let distance = abs(expandItemView.frame.center.x - animateInInfo.centerX) + let distanceNorm = distance / animateInInfo.width + itemDelay = mainCircleDelay + distanceNorm * 0.4 + } else { + itemDelay = mainCircleDelay + Double(8) * 0.06 + } + expandItemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: itemDelay) expandItemView.tintView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: itemDelay) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 33833c1118..a1105de7b5 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -131,7 +131,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { func appear(animated: Bool) { if animated { - self.animateInAnimationNode?.visibility = true + if self.item.isCustom { + self.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } else { + self.animateInAnimationNode?.visibility = true + } } else { self.animateInAnimationNode?.completed(true) } From 6c1fef69bf9f22c85a348c0d0ac86f6265de79e8 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 24 Aug 2022 00:43:33 +0300 Subject: [PATCH 10/11] Fix animation --- .../ReactionSelectionNode/Sources/ReactionContextNode.swift | 6 +++--- .../TelegramCore/Sources/State/PendingMessageManager.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index f5c1982f3a..28eac1b594 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -885,12 +885,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.backgroundNode.animateInFromAnchorRect(size: visualBackgroundFrame.size, sourceBackgroundFrame: sourceBackgroundFrame.offsetBy(dx: -visualBackgroundFrame.minX, dy: -visualBackgroundFrame.minY)) - self.animateInInfo = (visualBackgroundFrame.midX - sourceBackgroundFrame.midX, visualBackgroundFrame.width) + self.animateInInfo = (sourceBackgroundFrame.minX - visualBackgroundFrame.minX, visualBackgroundFrame.width) self.contentContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - visualBackgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) - self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(x: -(sourceBackgroundFrame.midX - visualBackgroundFrame.midX), y: 0.0), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(x: (sourceBackgroundFrame.minX - visualBackgroundFrame.minX), y: 0.0), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) self.contentTintContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - visualBackgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) - self.contentTintContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + self.contentTintContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(x: (sourceBackgroundFrame.minX - visualBackgroundFrame.minX), y: 0.0), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) } else if let animateOutToAnchorRect = animateOutToAnchorRect { let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0 diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index e51aaeb91c..94fa5ce7ed 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -96,9 +96,9 @@ private func uploadActivityTypeForMessage(_ message: Message) -> PeerInputActivi } else if let file = media as? TelegramMediaFile { if file.isInstantVideo { return .uploadingInstantVideo(progress: 0) - } else if file.isVideo && !file.isAnimated { + } else if file.isVideo && !file.isAnimated && !file.isVideoEmoji && !file.isVideoSticker { return .uploadingVideo(progress: 0) - } else if !file.isSticker && !file.isVoice && !file.isAnimated { + } else if !file.isSticker && !file.isCustomEmoji && !file.isVoice && !file.isAnimated { return .uploadingFile(progress: 0) } } From baad5e1eb8e6014d0f80d84db5559d38bb9b5c46 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 24 Aug 2022 00:48:48 +0300 Subject: [PATCH 11/11] Adjust animation --- .../ReactionSelectionNode/Sources/ReactionContextNode.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 28eac1b594..e8b7ea1d37 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1022,7 +1022,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let animateInInfo = self.animateInInfo { let distance = abs(itemNode.frame.center.x - animateInInfo.centerX) let distanceNorm = distance / animateInInfo.width - itemDelay = mainCircleDelay + distanceNorm * 0.4 + let adjustedDistanceNorm = distanceNorm//listViewAnimationCurveSystem(distanceNorm) + itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.3 } else { itemDelay = mainCircleDelay + Double(i) * 0.06 } @@ -1037,7 +1038,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let animateInInfo = self.animateInInfo { let distance = abs(expandItemView.frame.center.x - animateInInfo.centerX) let distanceNorm = distance / animateInInfo.width - itemDelay = mainCircleDelay + distanceNorm * 0.4 + let adjustedDistanceNorm = distanceNorm//listViewAnimationCurveSystem(distanceNorm) + itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.3 } else { itemDelay = mainCircleDelay + Double(8) * 0.06 }