diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index 8243efd2bb..8dcdf78175 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -120,6 +120,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { isMinimized: Bool, isCoveredByInput: Bool, displayTail: Bool, + forceTailToRight: Bool, transition: ContainedViewLayoutTransition ) { let shadowInset: CGFloat = 15.0 @@ -171,7 +172,10 @@ final class ReactionContextBackgroundNode: ASDisplayNode { let largeCircleFrame: CGRect let smallCircleFrame: CGRect - if isLeftAligned { + if forceTailToRight { + largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: size.height - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) + smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) + } else if isLeftAligned { largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: size.height - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) } else { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index ef680b6bfd..9b1b05b968 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -233,6 +233,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var animationHideNode: Bool = false public var displayTail: Bool = true + public var forceTailToRight: Bool = true private var didAnimateIn: Bool = false public private(set) var isAnimatingOut: Bool = false @@ -636,12 +637,20 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { contentSize.width = max(46.0, contentSize.width) contentSize.height = self.currentContentHeight - let sideInset: CGFloat = 11.0 + insets.left + let sideInset: CGFloat + if self.forceTailToRight { + sideInset = insets.left + } else { + sideInset = 11.0 + insets.left + } let backgroundOffset: CGPoint = CGPoint(x: 22.0, y: -7.0) var rect: CGRect let isLeftAligned: Bool - if anchorRect.minX < containerSize.width - anchorRect.maxX { + if self.forceTailToRight { + rect = CGRect(origin: CGPoint(x: anchorRect.minX - backgroundOffset.x - 4.0, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize) + isLeftAligned = false + } else if anchorRect.minX < containerSize.width - anchorRect.maxX { rect = CGRect(origin: CGPoint(x: anchorRect.maxX - contentSize.width + backgroundOffset.x, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize) isLeftAligned = true } else { @@ -665,7 +674,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } let cloudSourcePoint: CGFloat - if isLeftAligned { + if self.forceTailToRight { + cloudSourcePoint = min(rect.maxX - 46.0 / 2.0, anchorRect.maxX - 4.0) + } else if isLeftAligned { cloudSourcePoint = min(rect.maxX - 46.0 / 2.0, anchorRect.maxX - 4.0) } else { cloudSourcePoint = max(rect.minX + 46.0 / 2.0, anchorRect.minX) @@ -1190,6 +1201,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isMinimized: self.highlightedReaction != nil && !self.highlightedByHover, isCoveredByInput: isCoveredByInput, displayTail: self.displayTail, + forceTailToRight: self.forceTailToRight, transition: transition ) @@ -1776,7 +1788,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { + public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { self.isAnimatingOutToReaction = true var foundItemNode: ReactionNode? @@ -1808,7 +1820,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji { switch itemNode.item.reaction.rawValue { case .builtin: - switchToInlineImmediately = false + switchToInlineImmediately = forceSwitchToInlineImmediately case .custom: switchToInlineImmediately = !self.didTriggerExpandedReaction } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index cf0078f6b8..460faa2d2e 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -798,11 +798,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } dict[2008112412] = { return Api.StickerSetCovered.parse_stickerSetNoCovered($0) } dict[1898850301] = { return Api.StoriesStealthMode.parse_storiesStealthMode($0) } - dict[-1806085190] = { return Api.StoryItem.parse_storyItem($0) } + dict[1153718222] = { return Api.StoryItem.parse_storyItem($0) } dict[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) } dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) } - dict[-793729058] = { return Api.StoryView.parse_storyView($0) } - dict[-748199729] = { return Api.StoryViews.parse_storyViews($0) } + dict[-1329730875] = { return Api.StoryView.parse_storyView($0) } + dict[-968094825] = { return Api.StoryViews.parse_storyViews($0) } dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } @@ -915,6 +915,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1706939360] = { return Api.Update.parse_updateRecentStickers($0) } dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) } dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($0) } + dict[-475579104] = { return Api.Update.parse_updateSentStoryReaction($0) } dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) } dict[834816008] = { return Api.Update.parse_updateStickerSets($0) } dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) } @@ -1174,7 +1175,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } dict[1340440049] = { return Api.stories.Stories.parse_stories($0) } dict[-560009955] = { return Api.stories.StoryViews.parse_storyViews($0) } - dict[-79726676] = { return Api.stories.StoryViewsList.parse_storyViewsList($0) } + dict[1189722604] = { return Api.stories.StoryViewsList.parse_storyViewsList($0) } dict[933691231] = { return Api.stories.UserStories.parse_userStories($0) } dict[543450958] = { return Api.updates.ChannelDifference.parse_channelDifference($0) } dict[1041346555] = { return Api.updates.ChannelDifference.parse_channelDifferenceEmpty($0) } diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index cf242d53e2..ba1fa16627 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -408,15 +408,15 @@ public extension Api { } public extension Api { indirect enum StoryItem: TypeConstructorDescription { - case storyItem(flags: Int32, id: Int32, date: Int32, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?) + case storyItem(flags: Int32, id: Int32, date: Int32, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?, sentReaction: Api.Reaction?) case storyItemDeleted(id: Int32) case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyItem(let flags, let id, let date, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views): + case .storyItem(let flags, let id, let date, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction): if boxed { - buffer.appendInt32(-1806085190) + buffer.appendInt32(1153718222) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) @@ -440,6 +440,7 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 3) != 0 {views!.serialize(buffer, true)} + if Int(flags) & Int(1 << 15) != 0 {sentReaction!.serialize(buffer, true)} break case .storyItemDeleted(let id): if boxed { @@ -461,8 +462,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyItem(let flags, let id, let date, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views): - return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any)]) + case .storyItem(let flags, let id, let date, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction): + return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any), ("sentReaction", sentReaction as Any)]) case .storyItemDeleted(let id): return ("storyItemDeleted", [("id", id as Any)]) case .storyItemSkipped(let flags, let id, let date, let expireDate): @@ -501,6 +502,10 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.StoryViews } } + var _11: Api.Reaction? + if Int(_1!) & Int(1 << 15) != 0 {if let signature = reader.readInt32() { + _11 = Api.parse(reader, signature: signature) as? Api.Reaction + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -511,8 +516,9 @@ public extension Api { let _c8 = (Int(_1!) & Int(1 << 14) == 0) || _8 != nil let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { - return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, expireDate: _4!, caption: _5, entities: _6, media: _7!, mediaAreas: _8, privacy: _9, views: _10) + let _c11 = (Int(_1!) & Int(1 << 15) == 0) || _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, expireDate: _4!, caption: _5, entities: _6, media: _7!, mediaAreas: _8, privacy: _9, views: _10, sentReaction: _11) } else { return nil @@ -554,25 +560,26 @@ public extension Api { } public extension Api { enum StoryView: TypeConstructorDescription { - case storyView(flags: Int32, userId: Int64, date: Int32) + case storyView(flags: Int32, userId: Int64, date: Int32, reaction: Api.Reaction?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyView(let flags, let userId, let date): + case .storyView(let flags, let userId, let date, let reaction): if boxed { - buffer.appendInt32(-793729058) + buffer.appendInt32(-1329730875) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {reaction!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyView(let flags, let userId, let date): - return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any)]) + case .storyView(let flags, let userId, let date, let reaction): + return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any), ("reaction", reaction as Any)]) } } @@ -583,11 +590,16 @@ public extension Api { _2 = reader.readInt64() var _3: Int32? _3 = reader.readInt32() + var _4: Api.Reaction? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Reaction + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!) + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!, reaction: _4) } else { return nil @@ -598,16 +610,17 @@ public extension Api { } public extension Api { enum StoryViews: TypeConstructorDescription { - case storyViews(flags: Int32, viewsCount: Int32, recentViewers: [Int64]?) + case storyViews(flags: Int32, viewsCount: Int32, reactionsCount: Int32, recentViewers: [Int64]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyViews(let flags, let viewsCount, let recentViewers): + case .storyViews(let flags, let viewsCount, let reactionsCount, let recentViewers): if boxed { - buffer.appendInt32(-748199729) + buffer.appendInt32(-968094825) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(viewsCount, buffer: buffer, boxed: false) + serializeInt32(reactionsCount, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) buffer.appendInt32(Int32(recentViewers!.count)) for item in recentViewers! { @@ -619,8 +632,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyViews(let flags, let viewsCount, let recentViewers): - return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("recentViewers", recentViewers as Any)]) + case .storyViews(let flags, let viewsCount, let reactionsCount, let recentViewers): + return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("reactionsCount", reactionsCount as Any), ("recentViewers", recentViewers as Any)]) } } @@ -629,15 +642,18 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: [Int64]? + var _3: Int32? + _3 = reader.readInt32() + var _4: [Int64]? if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + _4 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, recentViewers: _3) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, reactionsCount: _3!, recentViewers: _4) } else { return nil @@ -1141,6 +1157,7 @@ public extension Api { case updateRecentStickers case updateSavedGifs case updateSavedRingtones + case updateSentStoryReaction(userId: Int64, storyId: Int32, reaction: Api.Reaction) case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity]) case updateStickerSets(flags: Int32) case updateStickerSetsOrder(flags: Int32, order: [Int64]) @@ -2013,6 +2030,14 @@ public extension Api { buffer.appendInt32(1960361625) } + break + case .updateSentStoryReaction(let userId, let storyId, let reaction): + if boxed { + buffer.appendInt32(-475579104) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(storyId, buffer: buffer, boxed: false) + reaction.serialize(buffer, true) break case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): if boxed { @@ -2346,6 +2371,8 @@ public extension Api { return ("updateSavedGifs", []) case .updateSavedRingtones: return ("updateSavedRingtones", []) + case .updateSentStoryReaction(let userId, let storyId, let reaction): + return ("updateSentStoryReaction", [("userId", userId as Any), ("storyId", storyId as Any), ("reaction", reaction as Any)]) case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): return ("updateServiceNotification", [("flags", flags as Any), ("inboxDate", inboxDate as Any), ("type", type as Any), ("message", message as Any), ("media", media as Any), ("entities", entities as Any)]) case .updateStickerSets(let flags): @@ -4086,6 +4113,25 @@ public extension Api { public static func parse_updateSavedRingtones(_ reader: BufferReader) -> Update? { return Api.Update.updateSavedRingtones } + public static func parse_updateSentStoryReaction(_ reader: BufferReader) -> Update? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.Reaction? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Reaction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateSentStoryReaction(userId: _1!, storyId: _2!, reaction: _3!) + } + else { + return nil + } + } public static func parse_updateServiceNotification(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index f7dd597db4..a8c45964ab 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -568,15 +568,17 @@ public extension Api.stories { } public extension Api.stories { enum StoryViewsList: TypeConstructorDescription { - case storyViewsList(count: Int32, views: [Api.StoryView], users: [Api.User]) + case storyViewsList(flags: Int32, count: Int32, reactionsCount: Int32, views: [Api.StoryView], users: [Api.User], nextOffset: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyViewsList(let count, let views, let users): + case .storyViewsList(let flags, let count, let reactionsCount, let views, let users, let nextOffset): if boxed { - buffer.appendInt32(-79726676) + buffer.appendInt32(1189722604) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) + serializeInt32(reactionsCount, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(views.count)) for item in views { @@ -587,33 +589,43 @@ public extension Api.stories { for item in users { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyViewsList(let count, let views, let users): - return ("storyViewsList", [("count", count as Any), ("views", views as Any), ("users", users as Any)]) + case .storyViewsList(let flags, let count, let reactionsCount, let views, let users, let nextOffset): + return ("storyViewsList", [("flags", flags as Any), ("count", count as Any), ("reactionsCount", reactionsCount as Any), ("views", views as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) } } public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? { var _1: Int32? _1 = reader.readInt32() - var _2: [Api.StoryView]? + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.StoryView]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) } - var _3: [Api.User]? + var _5: [Api.User]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.StoryViewsList.storyViewsList(count: _1!, views: _2!, users: _3!) + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.StoryViewsList.storyViewsList(flags: _1!, count: _2!, reactionsCount: _3!, views: _4!, users: _5!, nextOffset: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 5f57835ae2..69fba326d9 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -8472,15 +8472,15 @@ public extension Api.functions.stickers { } } public extension Api.functions.stories { - static func activateStealthMode(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func activateStealthMode(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(299359662) + buffer.appendInt32(1471926630) serializeInt32(flags, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.activateStealthMode", parameters: [("flags", String(describing: flags))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "stories.activateStealthMode", parameters: [("flags", String(describing: flags))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.Updates? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.Updates } return result }) @@ -8658,14 +8658,15 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func getStoryViewsList(id: Int32, offsetDate: Int32, offsetId: Int64, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getStoryViewsList(flags: Int32, q: String?, id: Int32, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1262182039) + buffer.appendInt32(-111189596) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(q!, buffer: buffer, boxed: false)} serializeInt32(id, buffer: buffer, boxed: false) - serializeInt32(offsetDate, buffer: buffer, boxed: false) - serializeInt64(offsetId, buffer: buffer, boxed: false) + serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.getStoryViewsList", parameters: [("id", String(describing: id)), ("offsetDate", String(describing: offsetDate)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.StoryViewsList? in + return (FunctionDescription(name: "stories.getStoryViewsList", parameters: [("flags", String(describing: flags)), ("q", String(describing: q)), ("id", String(describing: id)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.StoryViewsList? in let reader = BufferReader(buffer) var result: Api.stories.StoryViewsList? if let signature = reader.readInt32() { @@ -8748,6 +8749,24 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func sendReaction(flags: Int32, userId: Api.InputUser, storyId: Int32, reaction: Api.Reaction) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1235921331) + serializeInt32(flags, buffer: buffer, boxed: false) + userId.serialize(buffer, true) + serializeInt32(storyId, buffer: buffer, boxed: false) + reaction.serialize(buffer, true) + return (FunctionDescription(name: "stories.sendReaction", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("storyId", String(describing: storyId)), ("reaction", String(describing: reaction))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.stories { static func sendStory(flags: Int32, media: Api.InputMedia, mediaAreas: [Api.MediaArea]?, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule], randomId: Int64, period: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index b7de343f4b..79b4f16fca 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -123,7 +123,7 @@ enum AccountStateMutationOperation { case UpdateStory(peerId: PeerId, story: Api.StoryItem) case UpdateReadStories(peerId: PeerId, maxId: Int32) case UpdateStoryStealthMode(data: Api.StoriesStealthMode) - case UpdateStoryStealth(expireDate: Int32) + case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) } struct HoleFromPreviousState { @@ -649,13 +649,13 @@ struct AccountMutableState { self.addOperation(.UpdateStoryStealthMode(data: data)) } - mutating func updateStoryStealth(expireDate: Int32) { - self.addOperation(.UpdateStoryStealth(expireDate: expireDate)) + mutating func updateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) { + self.addOperation(.UpdateStorySentReaction(peerId: peerId, id: id, reaction: reaction)) } mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStoryStealth: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction: break case let .AddMessages(messages, location): for message in messages { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 46fc877c64..da1f555d16 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1679,6 +1679,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.readStories(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), maxId: id) case let .updateStoriesStealthMode(stealthMode): updatedState.updateStoryStealthMode(stealthMode) + case let .updateSentStoryReaction(userId, storyId, reaction): + updatedState.updateStorySentReaction(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId, reaction: reaction) default: break } @@ -3167,7 +3169,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStoryStealth: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -4557,10 +4559,65 @@ func replayFinalState( var configuration = _internal_getStoryConfigurationState(transaction: transaction) configuration.stealthModeState = Stories.StealthModeState(apiMode: data) _internal_setStoryConfigurationState(transaction: transaction, state: configuration) - case let .UpdateStoryStealth(expireDate): - var configuration = _internal_getStoryConfigurationState(transaction: transaction) - configuration.stealthModeState.activeUntilTimestamp = expireDate - _internal_setStoryConfigurationState(transaction: transaction, state: configuration) + case let .UpdateStorySentReaction(peerId, id, reaction): + var updatedPeerEntries: [StoryItemsTableEntry] = transaction.getStoryItems(peerId: peerId) + + if let index = updatedPeerEntries.firstIndex(where: { item in + return item.id == id + }) { + if let value = updatedPeerEntries[index].value.get(Stories.StoredItem.self), case let .item(item) = value { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views, + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + myReaction: MessageReaction.Reaction(apiReaction: reaction) + )) + if let entry = CodableEntry(updatedItem) { + updatedPeerEntries[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: item.expirationTimestamp, isCloseFriends: item.isCloseFriends) + } + } + } + transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries) + + if let value = transaction.getStory(id: StoryId(peerId: peerId, id: id))?.get(Stories.StoredItem.self), case let .item(item) = value { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views, + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + myReaction: MessageReaction.Reaction(apiReaction: reaction) + )) + if let entry = CodableEntry(updatedItem) { + transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry) + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 0e88881799..dcfe4b95a6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -41,14 +41,17 @@ public enum Stories { public struct Views: Codable, Equatable { private enum CodingKeys: String, CodingKey { case seenCount = "seenCount" + case reactedCount = "reactedCount" case seenPeerIds = "seenPeerIds" } public var seenCount: Int + public var reactedCount: Int public var seenPeerIds: [PeerId] - public init(seenCount: Int, seenPeerIds: [PeerId]) { + public init(seenCount: Int, reactedCount: Int, seenPeerIds: [PeerId]) { self.seenCount = seenCount + self.reactedCount = reactedCount self.seenPeerIds = seenPeerIds } @@ -56,6 +59,7 @@ public enum Stories { let container = try decoder.container(keyedBy: CodingKeys.self) self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount)) + self.reactedCount = Int(try container.decodeIfPresent(Int32.self, forKey: .reactedCount) ?? 0) self.seenPeerIds = try container.decode([Int64].self, forKey: .seenPeerIds).map(PeerId.init) } @@ -63,6 +67,7 @@ public enum Stories { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(Int32(clamping: self.seenCount), forKey: .seenCount) + try container.encode(Int32(clamping: self.reactedCount), forKey: .reactedCount) try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds) } } @@ -137,7 +142,7 @@ public enum Stories { case isSelectedContacts case isForwardingDisabled case isEdited - case hasLike + case myReaction } public let id: Int32 @@ -157,7 +162,7 @@ public enum Stories { public let isSelectedContacts: Bool public let isForwardingDisabled: Bool public let isEdited: Bool - public let hasLike: Bool + public let myReaction: MessageReaction.Reaction? public init( id: Int32, @@ -177,7 +182,7 @@ public enum Stories { isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, - hasLike: Bool + myReaction: MessageReaction.Reaction? ) { self.id = id self.timestamp = timestamp @@ -196,7 +201,7 @@ public enum Stories { self.isSelectedContacts = isSelectedContacts self.isForwardingDisabled = isForwardingDisabled self.isEdited = isEdited - self.hasLike = hasLike + self.myReaction = myReaction } public init(from decoder: Decoder) throws { @@ -225,7 +230,7 @@ public enum Stories { self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false - self.hasLike = try container.decodeIfPresent(Bool.self, forKey: .hasLike) ?? false + self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction) } public func encode(to encoder: Encoder) throws { @@ -255,7 +260,7 @@ public enum Stories { try container.encode(self.isSelectedContacts, forKey: .isSelectedContacts) try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) try container.encode(self.isEdited, forKey: .isEdited) - try container.encode(self.hasLike, forKey: .hasLike) + try container.encodeIfPresent(self.myReaction, forKey: .myReaction) } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -317,7 +322,7 @@ public enum Stories { if lhs.isEdited != rhs.isEdited { return false } - if lhs.hasLike != rhs.hasLike { + if lhs.myReaction != rhs.myReaction { return false } @@ -949,7 +954,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId for update in updates.allUpdates { if case let .updateStory(_, story) = update { switch story { - case let .storyItem(_, idValue, _, _, _, _, media, _, _, _): + case let .storyItem(_, idValue, _, _, _, _, media, _, _, _, _): if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: accountPeerId, transaction: transaction) { var items = transaction.getStoryItems(peerId: accountPeerId) var updatedItems: [Stories.Item] = [] @@ -972,7 +977,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)) @@ -1094,7 +1099,7 @@ func _internal_editStory(account: Account, id: Int32, media: EngineStoryInputMed for update in updates.allUpdates { if case let .updateStory(_, story) = update { switch story { - case let .storyItem(_, _, _, _, _, _, media, _, _, _): + case let .storyItem(_, _, _, _, _, _, media, _, _, _, _): let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) if let parsedMedia = parsedMedia, let originalMedia = originalMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false) @@ -1136,7 +1141,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { transaction.setStory(id: storyId, value: entry) @@ -1164,7 +1169,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1292,7 +1297,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1319,7 +1324,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) updatedItems.append(updatedItem) } @@ -1345,7 +1350,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory extension Api.StoryItem { var id: Int32 { switch self { - case let .storyItem(_, id, _, _, _, _, _, _, _, _): + case let .storyItem(_, id, _, _, _, _, _, _, _, _, _): return id case let .storyItemDeleted(id): return id @@ -1358,12 +1363,12 @@ extension Api.StoryItem { extension Stories.Item.Views { init(apiViews: Api.StoryViews) { switch apiViews { - case let .storyViews(_, viewsCount, recentViewers): + case let .storyViews(_, viewsCount, reactionsCount, recentViewers): var seenPeerIds: [PeerId] = [] if let recentViewers = recentViewers { seenPeerIds = recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } } - self.init(seenCount: Int(viewsCount), seenPeerIds: seenPeerIds) + self.init(seenCount: Int(viewsCount), reactedCount: Int(reactionsCount), seenPeerIds: seenPeerIds) } } } @@ -1371,7 +1376,7 @@ extension Stories.Item.Views { extension Stories.StoredItem { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { switch apiStoryItem { - case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views): + case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction): let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let parsedMedia = parsedMedia { var parsedPrivacy: Stories.Item.Privacy? @@ -1428,6 +1433,13 @@ extension Stories.StoredItem { mergedViews = views.flatMap(Stories.Item.Views.init(apiViews:)) } + var mergedMyReaction: MessageReaction.Reaction? + if isMin, let existingItem = existingItem { + mergedMyReaction = existingItem.myReaction + } else { + mergedMyReaction = sentReaction.flatMap(MessageReaction.Reaction.init(apiReaction:)) + } + let item = Stories.Item( id: id, timestamp: date, @@ -1446,7 +1458,7 @@ extension Stories.StoredItem { isSelectedContacts: isSelectedContacts, isForwardingDisabled: isForwardingDisabled, isEdited: isEdited, - hasLike: false + myReaction: mergedMyReaction ) self = .item(item) } else { @@ -1533,42 +1545,12 @@ public final class StoryViewList { public let items: [Item] public let totalCount: Int + public let totalReactedCount: Int - public init(items: [Item], totalCount: Int) { + public init(items: [Item], totalCount: Int, totalReactedCount: Int) { self.items = items self.totalCount = totalCount - } -} - -func _internal_getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal { - let accountPeerId = account.peerId - return account.network.request(Api.functions.stories.getStoryViewsList(id: id, offsetDate: offsetTimestamp ?? 0, offsetId: offsetPeerId?.id._internalGetInt64Value() ?? 0, limit: Int32(limit))) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal in - guard let result = result else { - return .single(nil) - } - return account.postbox.transaction { transaction -> StoryViewList? in - switch result { - case let .storyViewsList(count, views, users): - updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) - - var items: [StoryViewList.Item] = [] - for view in views { - switch view { - case let .storyView(_, userId, date): - if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) { - items.append(StoryViewList.Item(peer: EnginePeer(peer), timestamp: date)) - } - } - } - - return StoryViewList(items: items, totalCount: Int(count)) - } - } + self.totalReactedCount = totalReactedCount } } @@ -1603,26 +1585,28 @@ func _internal_getStoryViews(account: Account, ids: [Int32]) -> Signal<[Int32: S public final class EngineStoryViewListContext { public struct LoadMoreToken: Equatable { - var id: Int64 - var timestamp: Int32 + var value: String } public final class Item: Equatable { public let peer: EnginePeer public let timestamp: Int32 public let storyStats: PeerStoryStats? - public let isLike: Bool + public let reaction: MessageReaction.Reaction? + public let reactionFile: TelegramMediaFile? public init( peer: EnginePeer, timestamp: Int32, storyStats: PeerStoryStats?, - isLike: Bool + reaction: MessageReaction.Reaction?, + reactionFile: TelegramMediaFile? ) { self.peer = peer self.timestamp = timestamp self.storyStats = storyStats - self.isLike = isLike + self.reaction = reaction + self.reactionFile = reactionFile } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -1635,7 +1619,10 @@ public final class EngineStoryViewListContext { if lhs.storyStats != rhs.storyStats { return false } - if lhs.isLike != rhs.isLike { + if lhs.reaction != rhs.reaction { + return false + } + if lhs.reactionFile?.fileId != rhs.reactionFile?.fileId { return false } return true @@ -1644,15 +1631,18 @@ public final class EngineStoryViewListContext { public struct State: Equatable { public var totalCount: Int + public var totalReactedCount: Int public var items: [Item] public var loadMoreToken: LoadMoreToken? public init( totalCount: Int, + totalReactedCount: Int, items: [Item], loadMoreToken: LoadMoreToken? ) { self.totalCount = totalCount + self.totalReactedCount = totalReactedCount self.items = items self.loadMoreToken = loadMoreToken } @@ -1660,12 +1650,12 @@ public final class EngineStoryViewListContext { private final class Impl { struct NextOffset: Equatable { - var id: Int64 - var timestamp: Int32 + var value: String } struct InternalState: Equatable { var totalCount: Int + var totalReactedCount: Int var items: [Item] var canLoadMore: Bool var nextOffset: NextOffset? @@ -1689,8 +1679,8 @@ public final class EngineStoryViewListContext { self.account = account self.storyId = storyId - let initialState = State(totalCount: views.seenCount, items: [], loadMoreToken: LoadMoreToken(id: 0, timestamp: 0)) - self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil) + let initialState = State(totalCount: views.seenCount, totalReactedCount: views.reactedCount, items: [], loadMoreToken: LoadMoreToken(value: "")) + self.state = InternalState(totalCount: initialState.totalCount, totalReactedCount: initialState.totalReactedCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil) self.statePromise.set(.single(self.state)) if initialState.loadMoreToken != nil { @@ -1722,7 +1712,7 @@ public final class EngineStoryViewListContext { let signal: Signal = self.account.postbox.transaction { transaction -> Void in } |> mapToSignal { _ -> Signal in - return account.network.request(Api.functions.stories.getStoryViewsList(id: storyId, offsetDate: currentOffset?.timestamp ?? 0, offsetId: currentOffset?.id ?? 0, limit: Int32(limit))) + return account.network.request(Api.functions.stories.getStoryViewsList(flags: 0, q: nil, id: storyId, offset: currentOffset?.value ?? "", limit: Int32(limit))) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -1730,14 +1720,13 @@ public final class EngineStoryViewListContext { |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> InternalState in switch result { - case let .storyViewsList(count, views, users): + case let .storyViewsList(_, count, reactionsCount, views, users, nextOffset): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) var items: [Item] = [] - var nextOffset: NextOffset? for view in views { switch view { - case let .storyView(flags, userId, date): + case let .storyView(flags, userId, date, reaction): let isBlocked = (flags & (1 << 0)) != 0 let isBlockedFromStories = (flags & (1 << 1)) != 0 @@ -1758,9 +1747,21 @@ public final class EngineStoryViewListContext { return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags) }) if let peer = transaction.getPeer(peerId) { - items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId), isLike: false)) - - nextOffset = NextOffset(id: userId, timestamp: date) + let parsedReaction = reaction.flatMap(MessageReaction.Reaction.init(apiReaction:)) + items.append(Item( + peer: EnginePeer(peer), + timestamp: date, + storyStats: transaction.getPeerStoryStats(peerId: peerId), + reaction: parsedReaction, + reactionFile: parsedReaction.flatMap { reaction -> TelegramMediaFile? in + switch reaction { + case .builtin: + return nil + case let .custom(fileId): + return transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile + } + } + )) } } } @@ -1774,7 +1775,7 @@ public final class EngineStoryViewListContext { mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds), + views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, @@ -1784,7 +1785,7 @@ public final class EngineStoryViewListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry) @@ -1803,7 +1804,7 @@ public final class EngineStoryViewListContext { mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds), + views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, @@ -1813,7 +1814,7 @@ public final class EngineStoryViewListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1823,9 +1824,9 @@ public final class EngineStoryViewListContext { } transaction.setStoryItems(peerId: account.peerId, items: currentItems) - return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset) + return InternalState(totalCount: Int(count), totalReactedCount: Int(reactionsCount), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) }) case .none: - return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil) + return InternalState(totalCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil) } } } @@ -1853,10 +1854,22 @@ public final class EngineStoryViewListContext { existingItems.insert(itemHash) strongSelf.state.items.append(item) } + + var allReactedCount = 0 + for item in strongSelf.state.items { + if item.reaction != nil { + allReactedCount += 1 + } else { + break + } + } + if state.canLoadMore { strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count) + strongSelf.state.totalReactedCount = max(state.totalReactedCount, allReactedCount) } else { strongSelf.state.totalCount = strongSelf.state.items.count + strongSelf.state.totalReactedCount = allReactedCount } strongSelf.state.canLoadMore = state.canLoadMore strongSelf.state.nextOffset = state.nextOffset @@ -1884,7 +1897,8 @@ public final class EngineStoryViewListContext { peer: item.peer, timestamp: item.timestamp, storyStats: value, - isLike: false + reaction: item.reaction, + reactionFile: item.reactionFile ) } } @@ -1907,10 +1921,11 @@ public final class EngineStoryViewListContext { disposable.set(impl.statePromise.get().start(next: { state in var loadMoreToken: LoadMoreToken? if let nextOffset = state.nextOffset { - loadMoreToken = LoadMoreToken(id: nextOffset.id, timestamp: nextOffset.timestamp) + loadMoreToken = LoadMoreToken(value: nextOffset.value) } subscriber.putNext(State( totalCount: state.totalCount, + totalReactedCount: state.totalReactedCount, items: state.items, loadMoreToken: loadMoreToken )) @@ -2113,10 +2128,15 @@ func _internal_enableStoryStealthMode(account: Account) -> Signal `catch` { _ -> Signal in - return .single(.boolFalse) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) } |> mapToSignal { result -> Signal in + if let result = result { + account.stateManager.addUpdates(result) + } + return account.postbox.transaction { transaction in let appConfig = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue @@ -2158,8 +2178,15 @@ public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction, transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data())) } -func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal { - return account.postbox.transaction { transaction -> Void in +func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal { + return account.postbox.transaction { transaction -> Api.InputUser? in + guard let peer = transaction.getPeer(peerId) else { + return nil + } + guard let inputUser = apiInputUser(peer) else { + return nil + } + var currentItems = transaction.getStoryItems(peerId: peerId) for i in 0 ..< currentItems.count { if currentItems[i].id == id { @@ -2182,7 +2209,7 @@ func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: hasLike + myReaction: reaction )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -2211,12 +2238,30 @@ func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: hasLike + myReaction: reaction )) if let entry = CodableEntry(updatedItem) { transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry) } } + + return inputUser + } + |> mapToSignal { inputUser -> Signal in + guard let inputUser = inputUser else { + return .complete() + } + return account.network.request(Api.functions.stories.sendReaction(flags: 0, userId: inputUser, storyId: id, reaction: reaction?.apiReaction ?? .reactionEmpty)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + + return .complete() + } } - |> ignoreValues } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index c79f60e2de..68e1322644 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -13,10 +13,12 @@ enum InternalStoryUpdate { public final class EngineStoryItem: Equatable { public final class Views: Equatable { public let seenCount: Int + public let reactedCount: Int public let seenPeers: [EnginePeer] - public init(seenCount: Int, seenPeers: [EnginePeer]) { + public init(seenCount: Int, reactedCount: Int, seenPeers: [EnginePeer]) { self.seenCount = seenCount + self.reactedCount = reactedCount self.seenPeers = seenPeers } @@ -24,6 +26,9 @@ public final class EngineStoryItem: Equatable { if lhs.seenCount != rhs.seenCount { return false } + if lhs.reactedCount != rhs.reactedCount { + return false + } if lhs.seenPeers != rhs.seenPeers { return false } @@ -49,9 +54,9 @@ public final class EngineStoryItem: Equatable { public let isSelectedContacts: Bool public let isForwardingDisabled: Bool public let isEdited: Bool - public let hasLike: Bool + public let myReaction: MessageReaction.Reaction? - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, hasLike: Bool) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, myReaction: MessageReaction.Reaction?) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp @@ -70,7 +75,7 @@ public final class EngineStoryItem: Equatable { self.isSelectedContacts = isSelectedContacts self.isForwardingDisabled = isForwardingDisabled self.isEdited = isEdited - self.hasLike = hasLike + self.myReaction = myReaction } public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { @@ -128,7 +133,7 @@ public final class EngineStoryItem: Equatable { if lhs.isEdited != rhs.isEdited { return false } - if lhs.hasLike != rhs.hasLike { + if lhs.myReaction != rhs.myReaction { return false } return true @@ -148,6 +153,7 @@ extension EngineStoryItem { views: self.views.flatMap { views in return Stories.Item.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeerIds: views.seenPeers.map(\.id) ) }, @@ -165,7 +171,7 @@ extension EngineStoryItem { isSelectedContacts: self.isSelectedContacts, isForwardingDisabled: self.isForwardingDisabled, isEdited: self.isEdited, - hasLike: self.hasLike + myReaction: self.myReaction ) } } @@ -520,6 +526,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) } @@ -535,7 +542,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) items.append(mappedItem) @@ -646,6 +653,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) } @@ -661,7 +669,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) storyItems.append(mappedItem) } @@ -796,6 +804,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -811,7 +820,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) finalUpdatedState = updatedState } @@ -837,6 +846,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -852,7 +862,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) finalUpdatedState = updatedState } else { @@ -880,6 +890,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -895,7 +906,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) updatedState.items.sort(by: { lhs, rhs in return lhs.timestamp > rhs.timestamp @@ -919,6 +930,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -934,7 +946,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) updatedState.items.sort(by: { lhs, rhs in return lhs.timestamp > rhs.timestamp @@ -1082,6 +1094,7 @@ public final class PeerExpiringStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) } @@ -1097,7 +1110,7 @@ public final class PeerExpiringStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) items.append(.item(mappedItem)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index c48b0965a3..1b521aceea 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1035,7 +1035,7 @@ public extension TelegramEngine { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1102,10 +1102,6 @@ public extension TelegramEngine { return _internal_updateStoriesArePinned(account: self.account, ids: ids, isPinned: isPinned) } - public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal { - return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit) - } - public func storyViewList(id: Int32, views: EngineStoryItem.Views) -> EngineStoryViewListContext { return EngineStoryViewListContext(account: self.account, storyId: id, views: views) } @@ -1118,8 +1114,8 @@ public extension TelegramEngine { return _internal_enableStoryStealthMode(account: self.account) } - public func setStoryLike(peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal { - return _internal_setStoryLike(account: self.account, peerId: peerId, id: id, hasLike: hasLike) + public func setStoryReaction(peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal { + return _internal_setStoryReaction(account: self.account, peerId: peerId, id: id, reaction: reaction) } } } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 4a914141bc..360ddd9f2c 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -177,6 +177,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case storiesCameraTooltip = 43 case storiesDualCameraTooltip = 44 case displayChatListArchiveTooltip = 45 + case displayStoryReactionTooltip = 46 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -414,6 +415,10 @@ private struct ApplicationSpecificNoticeKeys { static func displayChatListArchiveTooltip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayChatListArchiveTooltip.key) } + + static func displayStoryReactionTooltip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayStoryReactionTooltip.key) + } } public struct ApplicationSpecificNotice { @@ -1546,6 +1551,27 @@ public struct ApplicationSpecificNotice { |> take(1) } + public static func setDisplayStoryReactionTooltip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { + transaction.setNotice(ApplicationSpecificNoticeKeys.displayStoryReactionTooltip(), entry) + } + } + |> ignoreValues + } + + public static func displayStoryReactionTooltip(accountManager: AccountManager) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.displayStoryReactionTooltip()) + |> map { view -> Bool in + if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { + return true + } else { + return false + } + } + |> take(1) + } + public static func setDisplayChatListArchiveTooltip(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index d3e01101f6..10b7527b97 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1113,8 +1113,9 @@ final class MediaEditorScreenComponent: Component { stopAndPreviewMediaRecording: nil, discardMediaRecordingPreview: nil, attachmentAction: nil, - hasLike: false, + myReaction: nil, likeAction: nil, + likeOptionsAction: nil, inputModeAction: { [weak self] in if let self { switch self.currentInputMode { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index f590e981d2..b2580c4c84 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -266,8 +266,9 @@ final class StoryPreviewComponent: Component { stopAndPreviewMediaRecording: nil, discardMediaRecordingPreview: nil, attachmentAction: { }, - hasLike: false, + myReaction: nil, likeAction: nil, + likeOptionsAction: nil, inputModeAction: nil, timeoutAction: nil, forwardAction: {}, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index 1951f54982..fd6453217f 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/StickerPeekUI", + "//submodules/Components/ReactionButtonListComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift index 422ca44d16..36ea09ad07 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift @@ -9,6 +9,8 @@ import TelegramPresentationData import ChatPresentationInterfaceState import MoreHeaderButton import ContextUI +import ReactionButtonListComponent +import TelegramCore private extension MessageInputActionButtonComponent.Mode { var iconName: String? { @@ -19,12 +21,8 @@ private extension MessageInputActionButtonComponent.Mode { return "Chat/Input/Text/IconAttachment" case .forward: return "Chat/Input/Text/IconForwardSend" - case let .like(isActive): - if isActive { - return "Stories/InputLikeOn" - } else { - return "Stories/InputLikeOff" - } + case .like: + return "Stories/InputLikeOff" default: return nil } @@ -43,7 +41,7 @@ public final class MessageInputActionButtonComponent: Component { case attach case forward case more - case like(isActive: Bool) + case like(reaction: MessageReaction.Reaction?, file: TelegramMediaFile?, animationFileId: Int64?) } public enum Action { @@ -127,12 +125,18 @@ public final class MessageInputActionButtonComponent: Component { public let referenceNode: ContextReferenceContentNode public let containerNode: ContextControllerSourceNode private let sendIconView: UIImageView - private var moreButton: MoreHeaderButton? + private var reactionIconView: ReactionIconView? private var component: MessageInputActionButtonComponent? private weak var componentState: EmptyComponentState? + private var acceptNextButtonPress: Bool = false + + public var likeIconView: UIView? { + return self.reactionIconView + } + override init(frame: CGRect) { self.sendIconView = UIImageView() @@ -157,6 +161,7 @@ public final class MessageInputActionButtonComponent: Component { guard let self, let component = self.component, let longPressAction = component.longPressAction else { return } + self.acceptNextButtonPress = false longPressAction(self, gesture) } @@ -173,8 +178,6 @@ public final class MessageInputActionButtonComponent: Component { self.button.addTarget(self, action: #selector(self.touchDown), forControlEvents: .touchDown) self.button.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) -// but.addTarget(self, action: #selector(self.touchDown), for: .touchDown) -// self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -182,6 +185,8 @@ public final class MessageInputActionButtonComponent: Component { } @objc private func touchDown() { + self.acceptNextButtonPress = true + guard let component = self.component else { return } @@ -189,6 +194,10 @@ public final class MessageInputActionButtonComponent: Component { } @objc private func pressed() { + if !self.acceptNextButtonPress { + return + } + guard let component = self.component else { return } @@ -207,6 +216,11 @@ public final class MessageInputActionButtonComponent: Component { let themeUpdated = previousComponent?.theme !== component.theme + var transition = transition + if transition.animation.isImmediate, let previousComponent, case .like = previousComponent.mode, case .like = component.mode, previousComponent.mode != component.mode { + transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + } + self.containerNode.isUserInteractionEnabled = component.longPressAction != nil if self.micButton == nil { @@ -306,8 +320,14 @@ public final class MessageInputActionButtonComponent: Component { switch component.mode { case .none: break - case .send, .apply, .attach, .delete, .forward, .like: + case .send, .apply, .attach, .delete, .forward: sendAlpha = 1.0 + case let .like(reaction, _, _): + if reaction != nil { + sendAlpha = 0.0 + } else { + sendAlpha = 1.0 + } case .more: moreAlpha = 1.0 case .videoInput, .voiceInput: @@ -318,10 +338,7 @@ public final class MessageInputActionButtonComponent: Component { if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName { if let iconName = component.mode.iconName { - var tintColor: UIColor = .white - if case .like(true) = component.mode { - tintColor = UIColor(rgb: 0xFF3B30) - } + let tintColor: UIColor = .white self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: tintColor) } else if case .apply = component.mode { self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), contextGenerator: { size, context in @@ -379,6 +396,44 @@ public final class MessageInputActionButtonComponent: Component { } } + if case let .like(reactionValue, reactionFile, animationFileId) = component.mode, let reaction = reactionValue { + let reactionIconFrame = CGRect(origin: .zero, size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: 2.0, dy: 2.0) + + let reactionIconView: ReactionIconView + if let current = self.reactionIconView { + reactionIconView = current + } else { + reactionIconView = ReactionIconView(frame: reactionIconFrame) + reactionIconView.isUserInteractionEnabled = false + self.reactionIconView = reactionIconView + self.addSubview(reactionIconView) + + if previousComponent != nil { + reactionIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + reactionIconView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) + } + } + transition.setFrame(view: reactionIconView, frame: reactionIconFrame) + reactionIconView.update( + size: reactionIconFrame.size, + context: component.context, + file: reactionFile, + fileId: animationFileId ?? reactionFile?.fileId.id ?? 0, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.2), + animateIdle: false, + reaction: reaction, + transition: .immediate + ) + } else if let reactionIconView = self.reactionIconView { + self.reactionIconView = nil + reactionIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak reactionIconView] _ in + reactionIconView?.removeFromSuperview() + }) + reactionIconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + } + transition.setFrame(view: self.button.view, frame: CGRect(origin: .zero, size: availableSize)) transition.setFrame(view: self.containerNode.view, frame: CGRect(origin: .zero, size: availableSize)) transition.setFrame(view: self.referenceNode.view, frame: CGRect(origin: .zero, size: availableSize)) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index f15878e994..adfae1ffe7 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -44,6 +44,18 @@ public final class MessageInputPanelComponent: Component { case emoji } + public struct MyReaction: Equatable { + public let reaction: MessageReaction.Reaction + public let file: TelegramMediaFile? + public let animationFileId: Int64? + + public init(reaction: MessageReaction.Reaction, file: TelegramMediaFile?, animationFileId: Int64?) { + self.reaction = reaction + self.file = file + self.animationFileId = animationFileId + } + } + public final class ExternalState { public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var hasText: Bool = false @@ -79,8 +91,9 @@ public final class MessageInputPanelComponent: Component { public let stopAndPreviewMediaRecording: (() -> Void)? public let discardMediaRecordingPreview: (() -> Void)? public let attachmentAction: (() -> Void)? - public let hasLike: Bool + public let myReaction: MyReaction? public let likeAction: (() -> Void)? + public let likeOptionsAction: ((UIView, ContextGesture?) -> Void)? public let inputModeAction: (() -> Void)? public let timeoutAction: ((UIView) -> Void)? public let forwardAction: (() -> Void)? @@ -126,8 +139,9 @@ public final class MessageInputPanelComponent: Component { stopAndPreviewMediaRecording: (() -> Void)?, discardMediaRecordingPreview: (() -> Void)?, attachmentAction: (() -> Void)?, - hasLike: Bool, + myReaction: MyReaction?, likeAction: (() -> Void)?, + likeOptionsAction: ((UIView, ContextGesture?) -> Void)?, inputModeAction: (() -> Void)?, timeoutAction: ((UIView) -> Void)?, forwardAction: (() -> Void)?, @@ -172,8 +186,9 @@ public final class MessageInputPanelComponent: Component { self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording self.discardMediaRecordingPreview = discardMediaRecordingPreview self.attachmentAction = attachmentAction - self.hasLike = hasLike + self.myReaction = myReaction self.likeAction = likeAction + self.likeOptionsAction = likeOptionsAction self.inputModeAction = inputModeAction self.timeoutAction = timeoutAction self.forwardAction = forwardAction @@ -280,12 +295,15 @@ public final class MessageInputPanelComponent: Component { if (lhs.attachmentAction == nil) != (rhs.attachmentAction == nil) { return false } - if lhs.hasLike != rhs.hasLike { + if lhs.myReaction != rhs.myReaction { return false } if (lhs.likeAction == nil) != (rhs.likeAction == nil) { return false } + if (lhs.likeOptionsAction == nil) != (rhs.likeOptionsAction == nil) { + return false + } return true } @@ -345,6 +363,10 @@ public final class MessageInputPanelComponent: Component { return self.likeButton.view } + public var likeIconView: UIView? { + return (self.likeButton.view as? MessageInputActionButtonComponent.View)?.likeIconView + } + override init(frame: CGRect) { self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true) @@ -1064,7 +1086,7 @@ public final class MessageInputPanelComponent: Component { let likeButtonSize = self.likeButton.update( transition: transition, component: AnyComponent(MessageInputActionButtonComponent( - mode: .like(isActive: component.hasLike), + mode: .like(reaction: component.myReaction?.reaction, file: component.myReaction?.file, animationFileId: component.myReaction?.animationFileId), action: { [weak self] _, action, _ in guard let self, let component = self.component else { return @@ -1074,7 +1096,7 @@ public final class MessageInputPanelComponent: Component { } component.likeAction?() }, - longPressAction: nil, + longPressAction: component.likeOptionsAction, switchMediaInputMode: { }, updateMediaCancelFraction: { _ in diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD index 90a414e411..e6d82851b0 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD @@ -25,7 +25,9 @@ swift_library( "//submodules/AppBundle", "//submodules/PeerPresenceStatusManager", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/ContextUI", + "//submodules/TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 2b726efb00..ee3ad3d1a1 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -16,6 +16,8 @@ import AppBundle import PeerPresenceStatusManager import EmojiStatusComponent import ContextUI +import EmojiTextAttachmentView +import TextFormat private let avatarFont = avatarPlaceholderFont(size: 15.0) private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) @@ -44,6 +46,39 @@ public final class PeerListItemComponent: Component { case checks } + public final class Reaction: Equatable { + public let reaction: MessageReaction.Reaction + public let file: TelegramMediaFile? + public let animationFileId: Int64? + + public init( + reaction: MessageReaction.Reaction, + file: TelegramMediaFile?, + animationFileId: Int64? + ) { + self.reaction = reaction + self.file = file + self.animationFileId = animationFileId + } + + public static func ==(lhs: Reaction, rhs: Reaction) -> Bool { + if lhs === rhs { + return true + } + if lhs.reaction != rhs.reaction { + return false + } + if lhs.file?.fileId != rhs.file?.fileId { + return false + } + if lhs.animationFileId != rhs.animationFileId { + return false + } + + return true + } + } + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings @@ -55,7 +90,7 @@ public final class PeerListItemComponent: Component { let subtitle: String? let subtitleAccessory: SubtitleAccessory let presence: EnginePeer.Presence? - let displayLike: Bool + let reaction: Reaction? let selectionState: SelectionState let hasNext: Bool let action: (EnginePeer) -> Void @@ -74,7 +109,7 @@ public final class PeerListItemComponent: Component { subtitle: String?, subtitleAccessory: SubtitleAccessory, presence: EnginePeer.Presence?, - displayLike: Bool = false, + reaction: Reaction? = nil, selectionState: SelectionState, hasNext: Bool, action: @escaping (EnginePeer) -> Void, @@ -92,7 +127,7 @@ public final class PeerListItemComponent: Component { self.subtitle = subtitle self.subtitleAccessory = subtitleAccessory self.presence = presence - self.displayLike = displayLike + self.reaction = reaction self.selectionState = selectionState self.hasNext = hasNext self.action = action @@ -134,7 +169,7 @@ public final class PeerListItemComponent: Component { if lhs.presence != rhs.presence { return false } - if lhs.displayLike != rhs.displayLike { + if lhs.reaction != rhs.reaction { return false } if lhs.selectionState != rhs.selectionState { @@ -160,7 +195,10 @@ public final class PeerListItemComponent: Component { private var iconView: UIImageView? private var checkLayer: CheckLayer? - private var likeIconView: UIImageView? + private var reactionLayer: InlineStickerItemLayer? + private var iconFrame: CGRect? + private var file: TelegramMediaFile? + private var fileDisposable: Disposable? private var component: PeerListItemComponent? private weak var state: EmptyComponentState? @@ -251,6 +289,10 @@ public final class PeerListItemComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.fileDisposable?.dispose() + } + @objc private func pressed() { guard let component = self.component, let peer = component.peer else { return @@ -265,7 +307,49 @@ public final class PeerListItemComponent: Component { component.openStories?(peer, self.avatarNode) } + private func updateReactionLayer() { + guard let component = self.component else { + return + } + + if let reactionLayer = self.reactionLayer { + self.reactionLayer = nil + reactionLayer.removeFromSuperlayer() + } + + guard let file = self.file else { + return + } + + let reactionLayer = InlineStickerItemLayer( + context: component.context, + userLocation: .other, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file), + file: file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.1), + pointSize: CGSize(width: 64.0, height: 64.0) + ) + self.reactionLayer = reactionLayer + + if let reaction = component.reaction, case .custom = reaction.reaction { + reactionLayer.isVisibleForAnimations = true + } + self.layer.addSublayer(reactionLayer) + + if var iconFrame = self.iconFrame { + if let reaction = component.reaction, case .builtin = reaction.reaction { + iconFrame = iconFrame.insetBy(dx: -iconFrame.width * 0.5, dy: -iconFrame.height * 0.5) + } + reactionLayer.frame = iconFrame + } + } + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + var synchronousLoad = false if let hint = transition.userData(TransitionHint.self) { synchronousLoad = hint.synchronousLoad @@ -351,7 +435,7 @@ public final class PeerListItemComponent: Component { leftInset += 9.0 } var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset - if component.displayLike { + if component.reaction != nil { rightInset += 32.0 } @@ -581,25 +665,29 @@ public final class PeerListItemComponent: Component { transition.setFrame(view: labelView, frame: labelFrame) } - if component.displayLike { - let likeIconView: UIImageView - if let current = self.likeIconView { - likeIconView = current + let imageSize = CGSize(width: 22.0, height: 22.0) + self.iconFrame = CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 14.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize) + + if previousComponent?.reaction != component.reaction { + if let reaction = component.reaction { + switch reaction.reaction { + case .builtin: + self.file = reaction.file + self.updateReactionLayer() + case let .custom(fileId): + self.fileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> deliverOnMainQueue).start(next: { [weak self] files in + guard let self, let file = files[fileId] else { + return + } + self.file = file + self.updateReactionLayer() + }) + } } else { - likeIconView = UIImageView() - self.likeIconView = likeIconView - self.containerButton.addSubview(likeIconView) - - likeIconView.image = PresentationResourcesChat.storyViewListLikeIcon(component.theme) + self.file = nil + self.updateReactionLayer() } - - if let _ = likeIconView.image { - let imageSize = CGSize(width: 32.0, height: 32.0) - transition.setFrame(view: likeIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 11.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize)) - } - } else if let likeIconView = self.likeIconView { - self.likeIconView = nil - likeIconView.removeFromSuperview() } if themeUpdated { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 38276c86aa..62fb62a08c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -158,6 +158,7 @@ public final class StoryContentContextImpl: StoryContentContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -173,7 +174,7 @@ public final class StoryContentContextImpl: StoryContentContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) } var totalCount = peerStoryItemsView.items.count @@ -198,7 +199,7 @@ public final class StoryContentContextImpl: StoryContentContext { isSelectedContacts: item.privacy.base == .nobody, isForwardingDisabled: false, isEdited: false, - hasLike: false + myReaction: nil )) totalCount += 1 } @@ -1029,6 +1030,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { views: itemValue.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -1044,7 +1046,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isSelectedContacts: itemValue.isSelectedContacts, isForwardingDisabled: itemValue.isForwardingDisabled, isEdited: itemValue.isEdited, - hasLike: itemValue.hasLike + myReaction: itemValue.myReaction ) let mainItem = StoryContentItem( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 2cfffc77b7..b71196a30a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -20,6 +20,7 @@ import VolumeButtons import TooltipUI import ChatEntityKeyboardInputNode import notify +import TelegramNotices func hasFirstResponder(_ view: UIView) -> Bool { if view.isFirstResponder { @@ -383,6 +384,8 @@ private final class StoryContainerScreenComponent: Component { private var pendingNavigationToItemId: (peerId: EnginePeer.Id, id: Int32)? + private var didDisplayReactionTooltip: Bool = false + override init(frame: CGRect) { self.backgroundLayer = SimpleLayer() self.backgroundLayer.backgroundColor = UIColor.black.cgColor @@ -912,6 +915,30 @@ private final class StoryContainerScreenComponent: Component { self?.layer.allowsGroupOpacity = false }) } + + Queue.mainQueue().after(0.4, { [weak self] in + guard let self, let component = self.component else { + return + } + + let _ = (ApplicationSpecificNotice.displayStoryReactionTooltip(accountManager: component.context.sharedContext.accountManager) + |> delay(1.0, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + if !value { + if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { + currentItemView.maybeDisplayReactionTooltip() + } + } + + self.didDisplayReactionTooltip = true + #if !DEBUG + let _ = ApplicationSpecificNotice.setDisplayStoryReactionTooltip(accountManager: component.context.sharedContext.accountManager).start() + #endif + }) + }) } func animateOut(completion: @escaping () -> Void) { @@ -1095,6 +1122,7 @@ private final class StoryContainerScreenComponent: Component { } } }) + update = true } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 1e01a3b5f4..c5c6f3fee3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -421,6 +421,8 @@ public final class StoryItemSetContainerComponent: Component { var reactionContextNode: ReactionContextNode? weak var disappearingReactionContextNode: ReactionContextNode? + var displayLikeReactions: Bool = false + var waitingForReactionAnimateOutToLike: MessageReaction.Reaction? weak var contextController: ContextController? weak var privacyController: ShareWithPeersScreen? @@ -781,7 +783,11 @@ public final class StoryItemSetContainerComponent: Component { if let _ = self.sendMessageContext.menuController { return } - if self.hasActiveDeactivateableInput() { + if self.displayLikeReactions { + self.displayLikeReactions = false + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.updateIsProgressPaused() + } else if self.hasActiveDeactivateableInput() { Queue.mainQueue().justDispatch { self.deactivateInput() } @@ -947,10 +953,21 @@ public final class StoryItemSetContainerComponent: Component { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + + if self.displayLikeReactions, let reactionContextNode = self.reactionContextNode { + if let result, result.isDescendant(of: reactionContextNode.view) { + return result + } else { + return self.itemsContainerView + } + } + if let inputView = self.inputPanel.view, let inputViewHitTest = inputView.hitTest(self.convert(point, to: inputView), with: event) { return inputViewHitTest } - guard let result = super.hitTest(point, with: event) else { + + guard let result else { return nil } @@ -1065,6 +1082,9 @@ public final class StoryItemSetContainerComponent: Component { if let captionItem = self.captionItem, captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText { return .blurred } + if self.displayLikeReactions { + return .blurred + } return .play } @@ -1817,6 +1837,67 @@ public final class StoryItemSetContainerComponent: Component { }) } + func maybeDisplayReactionTooltip() { + if "".isEmpty { + return + } + guard let component = self.component else { + return + } + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView else { + return + } + if inputPanelView.isHidden || inputPanelView.alpha == 0.0 { + return + } + if !likeButtonView.isDescendant(of: self) { + return + } + + let rect = likeButtonView.convert(likeButtonView.bounds, to: nil) + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + let text = "Long tap for more reactions" + let controller = TooltipController(content: .text(text), baseFontSize: presentationData.listsFontSize.baseDisplaySize, padding: 2.0) + controller.dismissed = { [weak self] _ in + if let self { + self.voiceMessagesRestrictedTooltipController = nil + self.updateIsProgressPaused() + } + } + component.presentController(controller, TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in + if let self { + return (self, rect) + } + return nil + })) + self.voiceMessagesRestrictedTooltipController = controller + self.updateIsProgressPaused() + + //TODO:localize + /*let tooltipScreen = TooltipScreen( + account: component.context.account, + sharedContext: component.context.sharedContext, + text: .markdown(text: "Long tap for more reactions"), + balancedTextLayout: true, + style: .default, + location: TooltipScreen.Location.point(likeButtonView.convert(likeButtonView.bounds, to: nil).offsetBy(dx: 0.0, dy: 0.0), .bottom), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: true) + } + ) + tooltipScreen.willBecomeDismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.tooltipScreen = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.tooltipScreen?.dismiss() + self.sendMessageContext.tooltipScreen = tooltipScreen + self.updateIsProgressPaused() + component.controller()?.present(tooltipScreen, in: .current)*/ + } + func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let isFirstTime = self.component == nil @@ -2042,13 +2123,44 @@ public final class StoryItemSetContainerComponent: Component { } self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default) }, - hasLike: component.slice.item.storyItem.hasLike, + myReaction: component.slice.item.storyItem.myReaction.flatMap { value -> MessageInputPanelComponent.MyReaction? in + var centerAnimation: TelegramMediaFile? + var animationFileId: Int64? + + switch value { + case .builtin: + if let availableReactions = component.availableReactions { + for availableReaction in availableReactions.reactionItems { + if availableReaction.reaction.rawValue == value { + centerAnimation = availableReaction.listAnimation + break + } + } + } + case let .custom(fileId): + animationFileId = fileId + } + + if animationFileId == nil && centerAnimation == nil { + return nil + } + + return MessageInputPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId) + }, likeAction: component.slice.peer.isService ? nil : { [weak self] in guard let self else { return } self.performLikeAction() }, + likeOptionsAction: component.slice.peer.isService ? nil : { [weak self] sourceView, gesture in + gesture?.cancel() + + guard let self else { + return + } + self.performLikeOptionsAction(sourceView: sourceView) + }, inputModeAction: { [weak self] in guard let self else { return @@ -2320,6 +2432,7 @@ public final class StoryItemSetContainerComponent: Component { minimizedContentHeight: 325.0, outerExpansionFraction: outerExpansionFraction, outerExpansionDirection: outerExpansionDirection, + availableReactions: component.availableReactions, close: { [weak self] in guard let self else { return @@ -3224,10 +3337,22 @@ public final class StoryItemSetContainerComponent: Component { } } - let reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) + let reactionsAnchorRect: CGRect + if self.displayLikeReactions, let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView { + var likeRect = likeButtonView.convert(likeButtonView.bounds, to: self) + likeRect.origin.y -= 15.0 + likeRect.size.height += 15.0 + likeRect.origin.x -= 30.0 + reactionsAnchorRect = likeRect + } else { + reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) + } var effectiveDisplayReactions = false - if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText { + if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText { + effectiveDisplayReactions = true + } + if self.displayLikeReactions { effectiveDisplayReactions = true } if self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil { @@ -3262,7 +3387,7 @@ public final class StoryItemSetContainerComponent: Component { animationCache: component.context.animationCache, presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme), items: reactionItems.map(ReactionContextItem.reaction), - selectedItems: Set(), + selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(), getEmojiContent: { [weak self] animationCache, animationRenderer in guard let self, let component = self.component else { preconditionFailure() @@ -3308,34 +3433,30 @@ public final class StoryItemSetContainerComponent: Component { self.state?.updated(transition: Transition(transition)) } ) - reactionContextNode.displayTail = false + reactionContextNode.displayTail = self.displayLikeReactions + reactionContextNode.forceTailToRight = self.displayLikeReactions self.reactionContextNode = reactionContextNode - reactionContextNode.reactionSelected = { [weak self, weak reactionContextNode] updateReaction, _ in + reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in guard let self, let component = self.component else { return } - let _ = (component.context.engine.stickers.availableReactions() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] availableReactions in - guard let self, let component = self.component, let availableReactions else { - return + if self.displayLikeReactions { + if component.slice.item.storyItem.myReaction == updateReaction.reaction { + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).start() + self.displayLikeReactions = false + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + } else { + self.waitingForReactionAnimateOutToLike = updateReaction.reaction + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start() } - - var animation: TelegramMediaFile? - for reaction in availableReactions.reactions { - if reaction.value == updateReaction.reaction { - animation = reaction.centerAnimation - break - } - } - + } else { let targetView = UIView(frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - 100.0) * 0.5), y: floor((self.bounds.height - 100.0) * 0.5)), size: CGSize(width: 100.0, height: 100.0))) targetView.isUserInteractionEnabled = false self.addSubview(targetView) - if let reactionContextNode { + if let reactionContextNode = self.reactionContextNode { reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction) reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in guard let self else { @@ -3359,74 +3480,9 @@ public final class StoryItemSetContainerComponent: Component { self.endEditing(true) } self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) - - var text = "" - var messageAttributes: [MessageAttribute] = [] - var inlineStickers: [MediaId : Media] = [:] - switch updateReaction { - case let .builtin(textValue): - text = textValue - case let .custom(fileId, file): - if let file { - animation = file - loop: for attribute in file.attributes { - switch attribute { - case let .CustomEmoji(_, _, displayText, _): - text = displayText - let length = (text as NSString).length - messageAttributes = [TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< length, type: .CustomEmoji(stickerPack: nil, fileId: fileId))])] - inlineStickers = [file.fileId: file] - break loop - default: - break - } - } - } - } - - let message: EnqueueMessage = .message( - text: text, - attributes: messageAttributes, - inlineStickers: inlineStickers, - mediaReference: nil, - replyToMessageId: nil, - replyToStoryId: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id), - localGroupingKey: nil, - correlationId: nil, - bubbleUpEmojiOrStickersets: [] - ) - let context = component.context - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let presentController = component.presentController - let peer = component.slice.peer - - let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: [message]) - |> deliverOnMainQueue).start(next: { [weak self] messageIds in - if let animation, let self, let component = self.component { - let controller = UndoOverlayController( - presentationData: presentationData, - content: .sticker(context: context, file: animation, loop: false, title: nil, text: component.strings.Story_ToastReactionSent, undoText: component.strings.Story_ToastViewInChat, customAction: { [weak self] in - if let messageId = messageIds.first, let self { - self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) }) - } - }), - elevatedLayout: false, - animateInAsReplacement: false, - blurred: true, - action: { [weak self] _ in - self?.sendMessageContext.tooltipScreen = nil - self?.updateIsProgressPaused() - return false - } - ) - self.sendMessageContext.tooltipScreen?.dismiss() - self.sendMessageContext.tooltipScreen = controller - self.updateIsProgressPaused() - presentController(controller, nil) - } - }) - }) + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start() + } } reactionContextNode.premiumReactionsSelected = { [weak self] file in @@ -3487,12 +3543,41 @@ public final class StoryItemSetContainerComponent: Component { } } else { reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) - reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition) + reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: !self.displayLikeReactions, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition) if animateReactionsIn { reactionContextNode.animateIn(from: reactionsAnchorRect) } } + + if let waitingReaction = self.waitingForReactionAnimateOutToLike, component.slice.item.storyItem.myReaction == waitingReaction { + self.waitingForReactionAnimateOutToLike = nil + + if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeIconView { + reactionContextNode.willAnimateOutToReaction(value: waitingReaction) + reactionContextNode.animateOutToReaction(value: waitingReaction, targetView: likeButtonView, hideNode: true, animateTargetContainer: nil, addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + standaloneReactionAnimation.frame = self.bounds + self.addSubview(standaloneReactionAnimation.view) + }, completion: { [weak reactionContextNode] in + if let reactionContextNode { + reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in + reactionContextNode?.view.removeFromSuperview() + }) + } + }) + } + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.displayLikeReactions = false + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + } + } } else { if let reactionContextNode = self.reactionContextNode { if let disappearingReactionContextNode = self.disappearingReactionContextNode { @@ -4215,9 +4300,9 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.messages.setStoryLike(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, hasLike: !component.slice.item.storyItem.hasLike).start() + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: component.slice.item.storyItem.myReaction == nil ? .builtin("❤") : nil).start() - if component.slice.item.storyItem.hasLike { + if component.slice.item.storyItem.myReaction != nil { return } @@ -4263,6 +4348,12 @@ public final class StoryItemSetContainerComponent: Component { ) } + private func performLikeOptionsAction(sourceView: UIView) { + self.displayLikeReactions = true + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.updateIsProgressPaused() + } + func dismissAllTooltips() { guard let component = self.component, let controller = component.controller() else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index b32e58ebce..35bfa26582 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -67,6 +67,7 @@ final class StoryItemSetViewListComponent: Component { let minimizedContentHeight: CGFloat let outerExpansionFraction: CGFloat let outerExpansionDirection: Bool + let availableReactions: StoryAvailableReactions? let close: () -> Void let expandViewStats: () -> Void let deleteAction: () -> Void @@ -88,6 +89,7 @@ final class StoryItemSetViewListComponent: Component { minimizedContentHeight: CGFloat, outerExpansionFraction: CGFloat, outerExpansionDirection: Bool, + availableReactions: StoryAvailableReactions?, close: @escaping () -> Void, expandViewStats: @escaping () -> Void, deleteAction: @escaping () -> Void, @@ -108,6 +110,7 @@ final class StoryItemSetViewListComponent: Component { self.minimizedContentHeight = minimizedContentHeight self.outerExpansionFraction = outerExpansionFraction self.outerExpansionDirection = outerExpansionDirection + self.availableReactions = availableReactions self.close = close self.expandViewStats = expandViewStats self.deleteAction = deleteAction @@ -143,6 +146,9 @@ final class StoryItemSetViewListComponent: Component { if lhs.outerExpansionDirection != rhs.outerExpansionDirection { return false } + if lhs.availableReactions !== rhs.availableReactions { + return false + } return true } @@ -517,7 +523,29 @@ final class StoryItemSetViewListComponent: Component { subtitle: dateText, subtitleAccessory: .checks, presence: nil, - displayLike: item.isLike, + reaction: item.reaction.flatMap { reaction -> PeerListItemComponent.Reaction in + var animationFileId: Int64? + var animationFile: TelegramMediaFile? + switch reaction { + case .builtin: + if let availableReactions = component.availableReactions { + for availableReaction in availableReactions.reactionItems { + if availableReaction.reaction.rawValue == reaction { + animationFile = availableReaction.listAnimation + break + } + } + } + case let .custom(fileId): + animationFileId = fileId + animationFile = item.reactionFile + } + return PeerListItemComponent.Reaction( + reaction: reaction, + file: animationFile, + animationFileId: animationFileId + ) + }, selectionState: .none, hasNext: index != viewListState.totalCount - 1, action: { [weak self] peer in @@ -670,7 +698,7 @@ final class StoryItemSetViewListComponent: Component { applyState = true let _ = synchronous } else { - self.viewListState = EngineStoryViewListContext.State(totalCount: 0, items: [], loadMoreToken: nil) + self.viewListState = EngineStoryViewListContext.State(totalCount: 0, totalReactedCount: 0, items: [], loadMoreToken: nil) } } @@ -728,7 +756,7 @@ final class StoryItemSetViewListComponent: Component { var externalViews: EngineStoryItem.Views? = component.storyItem.views if let viewListState = self.viewListState, !viewListState.items.isEmpty { - externalViews = EngineStoryItem.Views(seenCount: viewListState.totalCount, seenPeers: viewListState.items.prefix(3).map(\.peer)) + externalViews = EngineStoryItem.Views(seenCount: viewListState.totalCount, reactedCount: viewListState.totalReactedCount, seenPeers: viewListState.items.prefix(3).map(\.peer)) } let navigationPanelSize = self.navigationPanel.update( diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 7cbb8342c8..8e6202de9f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -76,6 +76,9 @@ public final class StoryFooterPanelComponent: Component { private let viewStatsExpandedText: AnimatedCountLabelView private let deleteButton = ComponentView() + private var reactionStatsIcon: UIImageView? + private var reactionStatsText: AnimatedCountLabelView? + private var statusButton: HighlightableButton? private var statusNode: SemanticStatusNode? private var uploadingText: ComponentView? @@ -114,9 +117,13 @@ public final class StoryFooterPanelComponent: Component { if highlighted { self.avatarsView.alpha = 0.7 self.viewStatsText.alpha = 0.7 + self.reactionStatsIcon?.alpha = 0.7 + self.reactionStatsText?.alpha = 0.7 } else { self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.viewStatsText.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) + self.reactionStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) + self.reactionStatsText?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) } } self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside) @@ -278,8 +285,10 @@ public final class StoryFooterPanelComponent: Component { } var viewCount = 0 + var reactionCount = 0 if let views = component.externalViews ?? component.storyItem?.views, views.seenCount != 0 { viewCount = views.seenCount + reactionCount = views.reactedCount } let viewsText: String @@ -353,7 +362,65 @@ public final class StoryFooterPanelComponent: Component { transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width) } - transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0))) + var statsButtonWidth = viewStatsTextFrame.maxY + 8.0 + + if reactionCount != 0 { + var reactionsTransition = transition + let reactionStatsIcon: UIImageView + if let current = self.reactionStatsIcon { + reactionStatsIcon = current + } else { + reactionsTransition = reactionsTransition.withAnimation(.none) + reactionStatsIcon = UIImageView() + reactionStatsIcon.image = UIImage(bundleImageName: "Stories/InputLikeOn")?.withRenderingMode(.alwaysTemplate) + reactionStatsIcon.tintColor = UIColor(rgb: 0xFF3B30) + + self.reactionStatsIcon = reactionStatsIcon + self.externalContainerView.addSubview(reactionStatsIcon) + } + + let reactionStatsText: AnimatedCountLabelView + if let current = self.reactionStatsText { + reactionStatsText = current + } else { + reactionStatsText = AnimatedCountLabelView(frame: CGRect()) + reactionStatsText.isUserInteractionEnabled = false + self.reactionStatsText = reactionStatsText + self.externalContainerView.addSubview(reactionStatsText) + } + + let reactionStatsLayout = reactionStatsText.update( + size: CGSize(width: availableSize.width, height: size.height), + segments: [ + .number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.regular(15.0), textColor: .white)) + ], + transition: (isFirstTime || reactionsTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) + ) + + let imageSize = CGSize(width: 23.0, height: 23.0) + reactionsTransition.setFrame(view: reactionStatsIcon, frame: CGRect(origin: CGPoint(x: viewStatsTextFrame.maxX + 7.0, y: viewStatsTextFrame.minY - 3.0), size: imageSize)) + + let reactionStatsFrame = CGRect(origin: CGPoint(x: viewStatsTextFrame.maxX + 7.0 + imageSize.width + 3.0, y: viewStatsTextFrame.minY), size: reactionStatsLayout.size) + reactionsTransition.setFrame(view: reactionStatsText, frame: reactionStatsFrame) + + statsButtonWidth = reactionStatsFrame.maxX + 8.0 + } else { + if let reactionStatsIcon = self.reactionStatsIcon { + self.reactionStatsIcon = nil + reactionStatsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsIcon] _ in + reactionStatsIcon?.removeFromSuperview() + }) + } + + if let reactionStatsText = self.reactionStatsText { + self.reactionStatsText = nil + reactionStatsText.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsText] _ in + reactionStatsText?.removeFromSuperview() + }) + } + } + + transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: statsButtonWidth, height: baseHeight))) self.viewStatsButton.isUserInteractionEnabled = component.expandFraction == 0.0 var rightContentOffset: CGFloat = availableSize.width - 12.0