Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-08-05 15:34:20 +02:00
commit eb66f51388
24 changed files with 932 additions and 315 deletions

View File

@ -120,6 +120,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
isMinimized: Bool, isMinimized: Bool,
isCoveredByInput: Bool, isCoveredByInput: Bool,
displayTail: Bool, displayTail: Bool,
forceTailToRight: Bool,
transition: ContainedViewLayoutTransition transition: ContainedViewLayoutTransition
) { ) {
let shadowInset: CGFloat = 15.0 let shadowInset: CGFloat = 15.0
@ -171,7 +172,10 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
let largeCircleFrame: CGRect let largeCircleFrame: CGRect
let smallCircleFrame: 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)) 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)) smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize))
} else { } else {

View File

@ -233,6 +233,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private var animationHideNode: Bool = false private var animationHideNode: Bool = false
public var displayTail: Bool = true public var displayTail: Bool = true
public var forceTailToRight: Bool = true
private var didAnimateIn: Bool = false private var didAnimateIn: Bool = false
public private(set) var isAnimatingOut: 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.width = max(46.0, contentSize.width)
contentSize.height = self.currentContentHeight 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) let backgroundOffset: CGPoint = CGPoint(x: 22.0, y: -7.0)
var rect: CGRect var rect: CGRect
let isLeftAligned: Bool 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) rect = CGRect(origin: CGPoint(x: anchorRect.maxX - contentSize.width + backgroundOffset.x, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize)
isLeftAligned = true isLeftAligned = true
} else { } else {
@ -665,7 +674,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
let cloudSourcePoint: CGFloat 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) cloudSourcePoint = min(rect.maxX - 46.0 / 2.0, anchorRect.maxX - 4.0)
} else { } else {
cloudSourcePoint = max(rect.minX + 46.0 / 2.0, anchorRect.minX) 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, isMinimized: self.highlightedReaction != nil && !self.highlightedByHover,
isCoveredByInput: isCoveredByInput, isCoveredByInput: isCoveredByInput,
displayTail: self.displayTail, displayTail: self.displayTail,
forceTailToRight: self.forceTailToRight,
transition: transition 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 self.isAnimatingOutToReaction = true
var foundItemNode: ReactionNode? 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 { if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji {
switch itemNode.item.reaction.rawValue { switch itemNode.item.reaction.rawValue {
case .builtin: case .builtin:
switchToInlineImmediately = false switchToInlineImmediately = forceSwitchToInlineImmediately
case .custom: case .custom:
switchToInlineImmediately = !self.didTriggerExpandedReaction switchToInlineImmediately = !self.didTriggerExpandedReaction
} }

View File

@ -798,11 +798,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) }
dict[2008112412] = { return Api.StickerSetCovered.parse_stickerSetNoCovered($0) } dict[2008112412] = { return Api.StickerSetCovered.parse_stickerSetNoCovered($0) }
dict[1898850301] = { return Api.StoriesStealthMode.parse_storiesStealthMode($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[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) }
dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) } dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) }
dict[-793729058] = { return Api.StoryView.parse_storyView($0) } dict[-1329730875] = { return Api.StoryView.parse_storyView($0) }
dict[-748199729] = { return Api.StoryViews.parse_storyViews($0) } dict[-968094825] = { return Api.StoryViews.parse_storyViews($0) }
dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) } dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) }
dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) }
dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($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[-1706939360] = { return Api.Update.parse_updateRecentStickers($0) }
dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) } dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) }
dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($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[-337352679] = { return Api.Update.parse_updateServiceNotification($0) }
dict[834816008] = { return Api.Update.parse_updateStickerSets($0) } dict[834816008] = { return Api.Update.parse_updateStickerSets($0) }
dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($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[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) }
dict[1340440049] = { return Api.stories.Stories.parse_stories($0) } dict[1340440049] = { return Api.stories.Stories.parse_stories($0) }
dict[-560009955] = { return Api.stories.StoryViews.parse_storyViews($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[933691231] = { return Api.stories.UserStories.parse_userStories($0) }
dict[543450958] = { return Api.updates.ChannelDifference.parse_channelDifference($0) } dict[543450958] = { return Api.updates.ChannelDifference.parse_channelDifference($0) }
dict[1041346555] = { return Api.updates.ChannelDifference.parse_channelDifferenceEmpty($0) } dict[1041346555] = { return Api.updates.ChannelDifference.parse_channelDifferenceEmpty($0) }

View File

@ -408,15 +408,15 @@ public extension Api {
} }
public extension Api { public extension Api {
indirect enum StoryItem: TypeConstructorDescription { 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 storyItemDeleted(id: Int32)
case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32) case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { 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 { if boxed {
buffer.appendInt32(-1806085190) buffer.appendInt32(1153718222)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false)
@ -440,6 +440,7 @@ public extension Api {
item.serialize(buffer, true) item.serialize(buffer, true)
}} }}
if Int(flags) & Int(1 << 3) != 0 {views!.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 break
case .storyItemDeleted(let id): case .storyItemDeleted(let id):
if boxed { if boxed {
@ -461,8 +462,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { 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):
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)]) 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): case .storyItemDeleted(let id):
return ("storyItemDeleted", [("id", id as Any)]) return ("storyItemDeleted", [("id", id as Any)])
case .storyItemSkipped(let flags, let id, let date, let expireDate): 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() { if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.StoryViews _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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -511,8 +516,9 @@ public extension Api {
let _c8 = (Int(_1!) & Int(1 << 14) == 0) || _8 != nil let _c8 = (Int(_1!) & Int(1 << 14) == 0) || _8 != nil
let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil
let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { let _c11 = (Int(_1!) & Int(1 << 15) == 0) || _11 != nil
return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, expireDate: _4!, caption: _5, entities: _6, media: _7!, mediaAreas: _8, privacy: _9, views: _10) 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 { else {
return nil return nil
@ -554,25 +560,26 @@ public extension Api {
} }
public extension Api { public extension Api {
enum StoryView: TypeConstructorDescription { 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .storyView(let flags, let userId, let date): case .storyView(let flags, let userId, let date, let reaction):
if boxed { if boxed {
buffer.appendInt32(-793729058) buffer.appendInt32(-1329730875)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(userId, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false)
serializeInt32(date, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 2) != 0 {reaction!.serialize(buffer, true)}
break break
} }
} }
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .storyView(let flags, let userId, let date): case .storyView(let flags, let userId, let date, let reaction):
return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any)]) 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() _2 = reader.readInt64()
var _3: Int32? var _3: Int32?
_3 = reader.readInt32() _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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
if _c1 && _c2 && _c3 { let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!) if _c1 && _c2 && _c3 && _c4 {
return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!, reaction: _4)
} }
else { else {
return nil return nil
@ -598,16 +610,17 @@ public extension Api {
} }
public extension Api { public extension Api {
enum StoryViews: TypeConstructorDescription { 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .storyViews(let flags, let viewsCount, let recentViewers): case .storyViews(let flags, let viewsCount, let reactionsCount, let recentViewers):
if boxed { if boxed {
buffer.appendInt32(-748199729) buffer.appendInt32(-968094825)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(viewsCount, 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) if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(recentViewers!.count)) buffer.appendInt32(Int32(recentViewers!.count))
for item in recentViewers! { for item in recentViewers! {
@ -619,8 +632,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .storyViews(let flags, let viewsCount, let recentViewers): case .storyViews(let flags, let viewsCount, let reactionsCount, let recentViewers):
return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("recentViewers", recentViewers as Any)]) 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() _1 = reader.readInt32()
var _2: Int32? var _2: Int32?
_2 = reader.readInt32() _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() { 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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil let _c3 = _3 != nil
if _c1 && _c2 && _c3 { let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, recentViewers: _3) if _c1 && _c2 && _c3 && _c4 {
return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, reactionsCount: _3!, recentViewers: _4)
} }
else { else {
return nil return nil
@ -1141,6 +1157,7 @@ public extension Api {
case updateRecentStickers case updateRecentStickers
case updateSavedGifs case updateSavedGifs
case updateSavedRingtones 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 updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity])
case updateStickerSets(flags: Int32) case updateStickerSets(flags: Int32)
case updateStickerSetsOrder(flags: Int32, order: [Int64]) case updateStickerSetsOrder(flags: Int32, order: [Int64])
@ -2013,6 +2030,14 @@ public extension Api {
buffer.appendInt32(1960361625) 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 break
case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities):
if boxed { if boxed {
@ -2346,6 +2371,8 @@ public extension Api {
return ("updateSavedGifs", []) return ("updateSavedGifs", [])
case .updateSavedRingtones: case .updateSavedRingtones:
return ("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): 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)]) 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): case .updateStickerSets(let flags):
@ -4086,6 +4113,25 @@ public extension Api {
public static func parse_updateSavedRingtones(_ reader: BufferReader) -> Update? { public static func parse_updateSavedRingtones(_ reader: BufferReader) -> Update? {
return Api.Update.updateSavedRingtones 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? { public static func parse_updateServiceNotification(_ reader: BufferReader) -> Update? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()

View File

@ -568,15 +568,17 @@ public extension Api.stories {
} }
public extension Api.stories { public extension Api.stories {
enum StoryViewsList: TypeConstructorDescription { 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { 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 { if boxed {
buffer.appendInt32(-79726676) buffer.appendInt32(1189722604)
} }
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(count, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false)
serializeInt32(reactionsCount, buffer: buffer, boxed: false)
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(views.count)) buffer.appendInt32(Int32(views.count))
for item in views { for item in views {
@ -587,33 +589,43 @@ public extension Api.stories {
for item in users { for item in users {
item.serialize(buffer, true) item.serialize(buffer, true)
} }
if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)}
break break
} }
} }
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .storyViewsList(let count, let views, let users): case .storyViewsList(let flags, let count, let reactionsCount, let views, let users, let nextOffset):
return ("storyViewsList", [("count", count as Any), ("views", views as Any), ("users", users as Any)]) 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? { public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _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() { 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() { 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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
if _c1 && _c2 && _c3 { let _c4 = _4 != nil
return Api.stories.StoryViewsList.storyViewsList(count: _1!, views: _2!, users: _3!) 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 { else {
return nil return nil

View File

@ -8472,15 +8472,15 @@ public extension Api.functions.stickers {
} }
} }
public extension Api.functions.stories { public extension Api.functions.stories {
static func activateStealthMode(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) { static func activateStealthMode(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(299359662) buffer.appendInt32(1471926630)
serializeInt32(flags, buffer: buffer, boxed: false) 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) let reader = BufferReader(buffer)
var result: Api.Bool? var result: Api.Updates?
if let signature = reader.readInt32() { 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 return result
}) })
@ -8658,14 +8658,15 @@ public extension Api.functions.stories {
} }
} }
public extension Api.functions.stories { public extension Api.functions.stories {
static func getStoryViewsList(id: Int32, offsetDate: Int32, offsetId: Int64, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stories.StoryViewsList>) { static func getStoryViewsList(flags: Int32, q: String?, id: Int32, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stories.StoryViewsList>) {
let buffer = Buffer() 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(id, buffer: buffer, boxed: false)
serializeInt32(offsetDate, buffer: buffer, boxed: false) serializeString(offset, buffer: buffer, boxed: false)
serializeInt64(offsetId, buffer: buffer, boxed: false)
serializeInt32(limit, 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) let reader = BufferReader(buffer)
var result: Api.stories.StoryViewsList? var result: Api.stories.StoryViewsList?
if let signature = reader.readInt32() { 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<Api.Updates>) {
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 { 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<Api.Updates>) { 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<Api.Updates>) {
let buffer = Buffer() let buffer = Buffer()

View File

@ -123,7 +123,7 @@ enum AccountStateMutationOperation {
case UpdateStory(peerId: PeerId, story: Api.StoryItem) case UpdateStory(peerId: PeerId, story: Api.StoryItem)
case UpdateReadStories(peerId: PeerId, maxId: Int32) case UpdateReadStories(peerId: PeerId, maxId: Int32)
case UpdateStoryStealthMode(data: Api.StoriesStealthMode) case UpdateStoryStealthMode(data: Api.StoriesStealthMode)
case UpdateStoryStealth(expireDate: Int32) case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction)
} }
struct HoleFromPreviousState { struct HoleFromPreviousState {
@ -649,13 +649,13 @@ struct AccountMutableState {
self.addOperation(.UpdateStoryStealthMode(data: data)) self.addOperation(.UpdateStoryStealthMode(data: data))
} }
mutating func updateStoryStealth(expireDate: Int32) { mutating func updateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) {
self.addOperation(.UpdateStoryStealth(expireDate: expireDate)) self.addOperation(.UpdateStorySentReaction(peerId: peerId, id: id, reaction: reaction))
} }
mutating func addOperation(_ operation: AccountStateMutationOperation) { mutating func addOperation(_ operation: AccountStateMutationOperation) {
switch operation { 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 break
case let .AddMessages(messages, location): case let .AddMessages(messages, location):
for message in messages { for message in messages {

View File

@ -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) updatedState.readStories(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), maxId: id)
case let .updateStoriesStealthMode(stealthMode): case let .updateStoriesStealthMode(stealthMode):
updatedState.updateStoryStealthMode(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: default:
break break
} }
@ -3167,7 +3169,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
var currentAddScheduledMessages: OptimizeAddMessagesState? var currentAddScheduledMessages: OptimizeAddMessagesState?
for operation in operations { for operation in operations {
switch operation { 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 { if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
} }
@ -4557,10 +4559,65 @@ func replayFinalState(
var configuration = _internal_getStoryConfigurationState(transaction: transaction) var configuration = _internal_getStoryConfigurationState(transaction: transaction)
configuration.stealthModeState = Stories.StealthModeState(apiMode: data) configuration.stealthModeState = Stories.StealthModeState(apiMode: data)
_internal_setStoryConfigurationState(transaction: transaction, state: configuration) _internal_setStoryConfigurationState(transaction: transaction, state: configuration)
case let .UpdateStoryStealth(expireDate): case let .UpdateStorySentReaction(peerId, id, reaction):
var configuration = _internal_getStoryConfigurationState(transaction: transaction) var updatedPeerEntries: [StoryItemsTableEntry] = transaction.getStoryItems(peerId: peerId)
configuration.stealthModeState.activeUntilTimestamp = expireDate
_internal_setStoryConfigurationState(transaction: transaction, state: configuration) 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)
}
}
} }
} }

View File

@ -41,14 +41,17 @@ public enum Stories {
public struct Views: Codable, Equatable { public struct Views: Codable, Equatable {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case seenCount = "seenCount" case seenCount = "seenCount"
case reactedCount = "reactedCount"
case seenPeerIds = "seenPeerIds" case seenPeerIds = "seenPeerIds"
} }
public var seenCount: Int public var seenCount: Int
public var reactedCount: Int
public var seenPeerIds: [PeerId] public var seenPeerIds: [PeerId]
public init(seenCount: Int, seenPeerIds: [PeerId]) { public init(seenCount: Int, reactedCount: Int, seenPeerIds: [PeerId]) {
self.seenCount = seenCount self.seenCount = seenCount
self.reactedCount = reactedCount
self.seenPeerIds = seenPeerIds self.seenPeerIds = seenPeerIds
} }
@ -56,6 +59,7 @@ public enum Stories {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount)) 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) 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) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(Int32(clamping: self.seenCount), forKey: .seenCount) 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) try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds)
} }
} }
@ -137,7 +142,7 @@ public enum Stories {
case isSelectedContacts case isSelectedContacts
case isForwardingDisabled case isForwardingDisabled
case isEdited case isEdited
case hasLike case myReaction
} }
public let id: Int32 public let id: Int32
@ -157,7 +162,7 @@ public enum Stories {
public let isSelectedContacts: Bool public let isSelectedContacts: Bool
public let isForwardingDisabled: Bool public let isForwardingDisabled: Bool
public let isEdited: Bool public let isEdited: Bool
public let hasLike: Bool public let myReaction: MessageReaction.Reaction?
public init( public init(
id: Int32, id: Int32,
@ -177,7 +182,7 @@ public enum Stories {
isSelectedContacts: Bool, isSelectedContacts: Bool,
isForwardingDisabled: Bool, isForwardingDisabled: Bool,
isEdited: Bool, isEdited: Bool,
hasLike: Bool myReaction: MessageReaction.Reaction?
) { ) {
self.id = id self.id = id
self.timestamp = timestamp self.timestamp = timestamp
@ -196,7 +201,7 @@ public enum Stories {
self.isSelectedContacts = isSelectedContacts self.isSelectedContacts = isSelectedContacts
self.isForwardingDisabled = isForwardingDisabled self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited self.isEdited = isEdited
self.hasLike = hasLike self.myReaction = myReaction
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -225,7 +230,7 @@ public enum Stories {
self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? 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 { 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.isSelectedContacts, forKey: .isSelectedContacts)
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
try container.encode(self.isEdited, forKey: .isEdited) 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 { public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -317,7 +322,7 @@ public enum Stories {
if lhs.isEdited != rhs.isEdited { if lhs.isEdited != rhs.isEdited {
return false return false
} }
if lhs.hasLike != rhs.hasLike { if lhs.myReaction != rhs.myReaction {
return false return false
} }
@ -949,7 +954,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
for update in updates.allUpdates { for update in updates.allUpdates {
if case let .updateStory(_, story) = update { if case let .updateStory(_, story) = update {
switch story { switch story {
case let .storyItem(_, idValue, _, _, _, _, media, _, _, _): case let .storyItem(_, idValue, _, _, _, _, media, _, _, _, _):
if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: accountPeerId, transaction: transaction) { if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: accountPeerId, transaction: transaction) {
var items = transaction.getStoryItems(peerId: accountPeerId) var items = transaction.getStoryItems(peerId: accountPeerId)
var updatedItems: [Stories.Item] = [] var updatedItems: [Stories.Item] = []
@ -972,7 +977,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)) 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 { for update in updates.allUpdates {
if case let .updateStory(_, story) = update { if case let .updateStory(_, story) = update {
switch story { switch story {
case let .storyItem(_, _, _, _, _, _, media, _, _, _): case let .storyItem(_, _, _, _, _, _, media, _, _, _, _):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
if let parsedMedia = parsedMedia, let originalMedia = originalMedia { if let parsedMedia = parsedMedia, let originalMedia = originalMedia {
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false) 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, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
transaction.setStory(id: storyId, value: entry) transaction.setStory(id: storyId, value: entry)
@ -1164,7 +1169,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) 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, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) 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, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
updatedItems.append(updatedItem) updatedItems.append(updatedItem)
} }
@ -1345,7 +1350,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
extension Api.StoryItem { extension Api.StoryItem {
var id: Int32 { var id: Int32 {
switch self { switch self {
case let .storyItem(_, id, _, _, _, _, _, _, _, _): case let .storyItem(_, id, _, _, _, _, _, _, _, _, _):
return id return id
case let .storyItemDeleted(id): case let .storyItemDeleted(id):
return id return id
@ -1358,12 +1363,12 @@ extension Api.StoryItem {
extension Stories.Item.Views { extension Stories.Item.Views {
init(apiViews: Api.StoryViews) { init(apiViews: Api.StoryViews) {
switch apiViews { switch apiViews {
case let .storyViews(_, viewsCount, recentViewers): case let .storyViews(_, viewsCount, reactionsCount, recentViewers):
var seenPeerIds: [PeerId] = [] var seenPeerIds: [PeerId] = []
if let recentViewers = recentViewers { if let recentViewers = recentViewers {
seenPeerIds = recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } 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 { extension Stories.StoredItem {
init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) {
switch apiStoryItem { 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) let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let parsedMedia = parsedMedia { if let parsedMedia = parsedMedia {
var parsedPrivacy: Stories.Item.Privacy? var parsedPrivacy: Stories.Item.Privacy?
@ -1428,6 +1433,13 @@ extension Stories.StoredItem {
mergedViews = views.flatMap(Stories.Item.Views.init(apiViews:)) 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( let item = Stories.Item(
id: id, id: id,
timestamp: date, timestamp: date,
@ -1446,7 +1458,7 @@ extension Stories.StoredItem {
isSelectedContacts: isSelectedContacts, isSelectedContacts: isSelectedContacts,
isForwardingDisabled: isForwardingDisabled, isForwardingDisabled: isForwardingDisabled,
isEdited: isEdited, isEdited: isEdited,
hasLike: false myReaction: mergedMyReaction
) )
self = .item(item) self = .item(item)
} else { } else {
@ -1533,42 +1545,12 @@ public final class StoryViewList {
public let items: [Item] public let items: [Item]
public let totalCount: Int 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.items = items
self.totalCount = totalCount self.totalCount = totalCount
} self.totalReactedCount = totalReactedCount
}
func _internal_getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
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<Api.stories.StoryViewsList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<StoryViewList?, NoError> 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))
}
}
} }
} }
@ -1603,26 +1585,28 @@ func _internal_getStoryViews(account: Account, ids: [Int32]) -> Signal<[Int32: S
public final class EngineStoryViewListContext { public final class EngineStoryViewListContext {
public struct LoadMoreToken: Equatable { public struct LoadMoreToken: Equatable {
var id: Int64 var value: String
var timestamp: Int32
} }
public final class Item: Equatable { public final class Item: Equatable {
public let peer: EnginePeer public let peer: EnginePeer
public let timestamp: Int32 public let timestamp: Int32
public let storyStats: PeerStoryStats? public let storyStats: PeerStoryStats?
public let isLike: Bool public let reaction: MessageReaction.Reaction?
public let reactionFile: TelegramMediaFile?
public init( public init(
peer: EnginePeer, peer: EnginePeer,
timestamp: Int32, timestamp: Int32,
storyStats: PeerStoryStats?, storyStats: PeerStoryStats?,
isLike: Bool reaction: MessageReaction.Reaction?,
reactionFile: TelegramMediaFile?
) { ) {
self.peer = peer self.peer = peer
self.timestamp = timestamp self.timestamp = timestamp
self.storyStats = storyStats self.storyStats = storyStats
self.isLike = isLike self.reaction = reaction
self.reactionFile = reactionFile
} }
public static func ==(lhs: Item, rhs: Item) -> Bool { public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -1635,7 +1619,10 @@ public final class EngineStoryViewListContext {
if lhs.storyStats != rhs.storyStats { if lhs.storyStats != rhs.storyStats {
return false return false
} }
if lhs.isLike != rhs.isLike { if lhs.reaction != rhs.reaction {
return false
}
if lhs.reactionFile?.fileId != rhs.reactionFile?.fileId {
return false return false
} }
return true return true
@ -1644,15 +1631,18 @@ public final class EngineStoryViewListContext {
public struct State: Equatable { public struct State: Equatable {
public var totalCount: Int public var totalCount: Int
public var totalReactedCount: Int
public var items: [Item] public var items: [Item]
public var loadMoreToken: LoadMoreToken? public var loadMoreToken: LoadMoreToken?
public init( public init(
totalCount: Int, totalCount: Int,
totalReactedCount: Int,
items: [Item], items: [Item],
loadMoreToken: LoadMoreToken? loadMoreToken: LoadMoreToken?
) { ) {
self.totalCount = totalCount self.totalCount = totalCount
self.totalReactedCount = totalReactedCount
self.items = items self.items = items
self.loadMoreToken = loadMoreToken self.loadMoreToken = loadMoreToken
} }
@ -1660,12 +1650,12 @@ public final class EngineStoryViewListContext {
private final class Impl { private final class Impl {
struct NextOffset: Equatable { struct NextOffset: Equatable {
var id: Int64 var value: String
var timestamp: Int32
} }
struct InternalState: Equatable { struct InternalState: Equatable {
var totalCount: Int var totalCount: Int
var totalReactedCount: Int
var items: [Item] var items: [Item]
var canLoadMore: Bool var canLoadMore: Bool
var nextOffset: NextOffset? var nextOffset: NextOffset?
@ -1689,8 +1679,8 @@ public final class EngineStoryViewListContext {
self.account = account self.account = account
self.storyId = storyId self.storyId = storyId
let initialState = State(totalCount: views.seenCount, items: [], loadMoreToken: LoadMoreToken(id: 0, timestamp: 0)) let initialState = State(totalCount: views.seenCount, totalReactedCount: views.reactedCount, items: [], loadMoreToken: LoadMoreToken(value: ""))
self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil) self.state = InternalState(totalCount: initialState.totalCount, totalReactedCount: initialState.totalReactedCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil)
self.statePromise.set(.single(self.state)) self.statePromise.set(.single(self.state))
if initialState.loadMoreToken != nil { if initialState.loadMoreToken != nil {
@ -1722,7 +1712,7 @@ public final class EngineStoryViewListContext {
let signal: Signal<InternalState, NoError> = self.account.postbox.transaction { transaction -> Void in let signal: Signal<InternalState, NoError> = self.account.postbox.transaction { transaction -> Void in
} }
|> mapToSignal { _ -> Signal<InternalState, NoError> in |> mapToSignal { _ -> Signal<InternalState, NoError> 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) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.stories.StoryViewsList?, NoError> in |> `catch` { _ -> Signal<Api.stories.StoryViewsList?, NoError> in
return .single(nil) return .single(nil)
@ -1730,14 +1720,13 @@ public final class EngineStoryViewListContext {
|> mapToSignal { result -> Signal<InternalState, NoError> in |> mapToSignal { result -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in return account.postbox.transaction { transaction -> InternalState in
switch result { 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)) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
var items: [Item] = [] var items: [Item] = []
var nextOffset: NextOffset?
for view in views { for view in views {
switch view { switch view {
case let .storyView(flags, userId, date): case let .storyView(flags, userId, date, reaction):
let isBlocked = (flags & (1 << 0)) != 0 let isBlocked = (flags & (1 << 0)) != 0
let isBlockedFromStories = (flags & (1 << 1)) != 0 let isBlockedFromStories = (flags & (1 << 1)) != 0
@ -1758,9 +1747,21 @@ public final class EngineStoryViewListContext {
return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags) return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags)
}) })
if let peer = transaction.getPeer(peerId) { if let peer = transaction.getPeer(peerId) {
items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId), isLike: false)) let parsedReaction = reaction.flatMap(MessageReaction.Reaction.init(apiReaction:))
items.append(Item(
nextOffset = NextOffset(id: userId, timestamp: date) 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, mediaAreas: item.mediaAreas,
text: item.text, text: item.text,
entities: item.entities, 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, privacy: item.privacy,
isPinned: item.isPinned, isPinned: item.isPinned,
isExpired: item.isExpired, isExpired: item.isExpired,
@ -1784,7 +1785,7 @@ public final class EngineStoryViewListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry) transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry)
@ -1803,7 +1804,7 @@ public final class EngineStoryViewListContext {
mediaAreas: item.mediaAreas, mediaAreas: item.mediaAreas,
text: item.text, text: item.text,
entities: item.entities, 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, privacy: item.privacy,
isPinned: item.isPinned, isPinned: item.isPinned,
isExpired: item.isExpired, isExpired: item.isExpired,
@ -1813,7 +1814,7 @@ public final class EngineStoryViewListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) 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) 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: 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) existingItems.insert(itemHash)
strongSelf.state.items.append(item) 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 { if state.canLoadMore {
strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count) strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count)
strongSelf.state.totalReactedCount = max(state.totalReactedCount, allReactedCount)
} else { } else {
strongSelf.state.totalCount = strongSelf.state.items.count strongSelf.state.totalCount = strongSelf.state.items.count
strongSelf.state.totalReactedCount = allReactedCount
} }
strongSelf.state.canLoadMore = state.canLoadMore strongSelf.state.canLoadMore = state.canLoadMore
strongSelf.state.nextOffset = state.nextOffset strongSelf.state.nextOffset = state.nextOffset
@ -1884,7 +1897,8 @@ public final class EngineStoryViewListContext {
peer: item.peer, peer: item.peer,
timestamp: item.timestamp, timestamp: item.timestamp,
storyStats: value, 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 disposable.set(impl.statePromise.get().start(next: { state in
var loadMoreToken: LoadMoreToken? var loadMoreToken: LoadMoreToken?
if let nextOffset = state.nextOffset { if let nextOffset = state.nextOffset {
loadMoreToken = LoadMoreToken(id: nextOffset.id, timestamp: nextOffset.timestamp) loadMoreToken = LoadMoreToken(value: nextOffset.value)
} }
subscriber.putNext(State( subscriber.putNext(State(
totalCount: state.totalCount, totalCount: state.totalCount,
totalReactedCount: state.totalReactedCount,
items: state.items, items: state.items,
loadMoreToken: loadMoreToken loadMoreToken: loadMoreToken
)) ))
@ -2113,10 +2128,15 @@ func _internal_enableStoryStealthMode(account: Account) -> Signal<Never, NoError
flags |= 1 << 0 flags |= 1 << 0
flags |= 1 << 1 flags |= 1 << 1
return account.network.request(Api.functions.stories.activateStealthMode(flags: flags)) return account.network.request(Api.functions.stories.activateStealthMode(flags: flags))
|> `catch` { _ -> Signal<Api.Bool, NoError> in |> map(Optional.init)
return .single(.boolFalse) |> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
} }
|> mapToSignal { result -> Signal<Never, NoError> in |> mapToSignal { result -> Signal<Never, NoError> in
if let result = result {
account.stateManager.addUpdates(result)
}
return account.postbox.transaction { transaction in return account.postbox.transaction { transaction in
let appConfig = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue 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())) 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<Never, NoError> { func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in 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) var currentItems = transaction.getStoryItems(peerId: peerId)
for i in 0 ..< currentItems.count { for i in 0 ..< currentItems.count {
if currentItems[i].id == id { if currentItems[i].id == id {
@ -2182,7 +2209,7 @@ func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32,
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: hasLike myReaction: reaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) 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, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: hasLike myReaction: reaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry) transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry)
} }
} }
return inputUser
}
|> mapToSignal { inputUser -> Signal<Never, NoError> 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<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
} }
|> ignoreValues
} }

View File

@ -13,10 +13,12 @@ enum InternalStoryUpdate {
public final class EngineStoryItem: Equatable { public final class EngineStoryItem: Equatable {
public final class Views: Equatable { public final class Views: Equatable {
public let seenCount: Int public let seenCount: Int
public let reactedCount: Int
public let seenPeers: [EnginePeer] public let seenPeers: [EnginePeer]
public init(seenCount: Int, seenPeers: [EnginePeer]) { public init(seenCount: Int, reactedCount: Int, seenPeers: [EnginePeer]) {
self.seenCount = seenCount self.seenCount = seenCount
self.reactedCount = reactedCount
self.seenPeers = seenPeers self.seenPeers = seenPeers
} }
@ -24,6 +26,9 @@ public final class EngineStoryItem: Equatable {
if lhs.seenCount != rhs.seenCount { if lhs.seenCount != rhs.seenCount {
return false return false
} }
if lhs.reactedCount != rhs.reactedCount {
return false
}
if lhs.seenPeers != rhs.seenPeers { if lhs.seenPeers != rhs.seenPeers {
return false return false
} }
@ -49,9 +54,9 @@ public final class EngineStoryItem: Equatable {
public let isSelectedContacts: Bool public let isSelectedContacts: Bool
public let isForwardingDisabled: Bool public let isForwardingDisabled: Bool
public let isEdited: 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.id = id
self.timestamp = timestamp self.timestamp = timestamp
self.expirationTimestamp = expirationTimestamp self.expirationTimestamp = expirationTimestamp
@ -70,7 +75,7 @@ public final class EngineStoryItem: Equatable {
self.isSelectedContacts = isSelectedContacts self.isSelectedContacts = isSelectedContacts
self.isForwardingDisabled = isForwardingDisabled self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited self.isEdited = isEdited
self.hasLike = hasLike self.myReaction = myReaction
} }
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
@ -128,7 +133,7 @@ public final class EngineStoryItem: Equatable {
if lhs.isEdited != rhs.isEdited { if lhs.isEdited != rhs.isEdited {
return false return false
} }
if lhs.hasLike != rhs.hasLike { if lhs.myReaction != rhs.myReaction {
return false return false
} }
return true return true
@ -148,6 +153,7 @@ extension EngineStoryItem {
views: self.views.flatMap { views in views: self.views.flatMap { views in
return Stories.Item.Views( return Stories.Item.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeerIds: views.seenPeers.map(\.id) seenPeerIds: views.seenPeers.map(\.id)
) )
}, },
@ -165,7 +171,7 @@ extension EngineStoryItem {
isSelectedContacts: self.isSelectedContacts, isSelectedContacts: self.isSelectedContacts,
isForwardingDisabled: self.isForwardingDisabled, isForwardingDisabled: self.isForwardingDisabled,
isEdited: self.isEdited, isEdited: self.isEdited,
hasLike: self.hasLike myReaction: self.myReaction
) )
} }
} }
@ -520,6 +526,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in views: item.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init) return transaction.getPeer(id).flatMap(EnginePeer.init)
} }
@ -535,7 +542,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
items.append(mappedItem) items.append(mappedItem)
@ -646,6 +653,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in views: item.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init) return transaction.getPeer(id).flatMap(EnginePeer.init)
} }
@ -661,7 +669,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
storyItems.append(mappedItem) storyItems.append(mappedItem)
} }
@ -796,6 +804,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in views: item.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init) return peers[id].flatMap(EnginePeer.init)
} }
@ -811,7 +820,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
finalUpdatedState = updatedState finalUpdatedState = updatedState
} }
@ -837,6 +846,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in views: item.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init) return peers[id].flatMap(EnginePeer.init)
} }
@ -852,7 +862,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
finalUpdatedState = updatedState finalUpdatedState = updatedState
} else { } else {
@ -880,6 +890,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in views: item.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init) return peers[id].flatMap(EnginePeer.init)
} }
@ -895,7 +906,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
)) ))
updatedState.items.sort(by: { lhs, rhs in updatedState.items.sort(by: { lhs, rhs in
return lhs.timestamp > rhs.timestamp return lhs.timestamp > rhs.timestamp
@ -919,6 +930,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in views: item.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init) return peers[id].flatMap(EnginePeer.init)
} }
@ -934,7 +946,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
)) ))
updatedState.items.sort(by: { lhs, rhs in updatedState.items.sort(by: { lhs, rhs in
return lhs.timestamp > rhs.timestamp return lhs.timestamp > rhs.timestamp
@ -1082,6 +1094,7 @@ public final class PeerExpiringStoryListContext {
views: item.views.flatMap { views in views: item.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init) return transaction.getPeer(id).flatMap(EnginePeer.init)
} }
@ -1097,7 +1110,7 @@ public final class PeerExpiringStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
items.append(.item(mappedItem)) items.append(.item(mappedItem))
} }

View File

@ -1035,7 +1035,7 @@ public extension TelegramEngine {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) 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) return _internal_updateStoriesArePinned(account: self.account, ids: ids, isPinned: isPinned)
} }
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit)
}
public func storyViewList(id: Int32, views: EngineStoryItem.Views) -> EngineStoryViewListContext { public func storyViewList(id: Int32, views: EngineStoryItem.Views) -> EngineStoryViewListContext {
return EngineStoryViewListContext(account: self.account, storyId: id, views: views) return EngineStoryViewListContext(account: self.account, storyId: id, views: views)
} }
@ -1118,8 +1114,8 @@ public extension TelegramEngine {
return _internal_enableStoryStealthMode(account: self.account) return _internal_enableStoryStealthMode(account: self.account)
} }
public func setStoryLike(peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal<Never, NoError> { public func setStoryReaction(peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal<Never, NoError> {
return _internal_setStoryLike(account: self.account, peerId: peerId, id: id, hasLike: hasLike) return _internal_setStoryReaction(account: self.account, peerId: peerId, id: id, reaction: reaction)
} }
} }
} }

View File

@ -177,6 +177,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case storiesCameraTooltip = 43 case storiesCameraTooltip = 43
case storiesDualCameraTooltip = 44 case storiesDualCameraTooltip = 44
case displayChatListArchiveTooltip = 45 case displayChatListArchiveTooltip = 45
case displayStoryReactionTooltip = 46
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -414,6 +415,10 @@ private struct ApplicationSpecificNoticeKeys {
static func displayChatListArchiveTooltip() -> NoticeEntryKey { static func displayChatListArchiveTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayChatListArchiveTooltip.key) 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 { public struct ApplicationSpecificNotice {
@ -1546,6 +1551,27 @@ public struct ApplicationSpecificNotice {
|> take(1) |> take(1)
} }
public static func setDisplayStoryReactionTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Never, NoError> {
return accountManager.transaction { transaction -> Void in
if let entry = CodableEntry(ApplicationSpecificBoolNotice()) {
transaction.setNotice(ApplicationSpecificNoticeKeys.displayStoryReactionTooltip(), entry)
}
}
|> ignoreValues
}
public static func displayStoryReactionTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Bool, NoError> {
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<TelegramAccountManagerTypes>) -> Signal<Never, NoError> { public static func setDisplayChatListArchiveTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Never, NoError> {
return accountManager.transaction { transaction -> Void in return accountManager.transaction { transaction -> Void in
if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { if let entry = CodableEntry(ApplicationSpecificBoolNotice()) {

View File

@ -1113,8 +1113,9 @@ final class MediaEditorScreenComponent: Component {
stopAndPreviewMediaRecording: nil, stopAndPreviewMediaRecording: nil,
discardMediaRecordingPreview: nil, discardMediaRecordingPreview: nil,
attachmentAction: nil, attachmentAction: nil,
hasLike: false, myReaction: nil,
likeAction: nil, likeAction: nil,
likeOptionsAction: nil,
inputModeAction: { [weak self] in inputModeAction: { [weak self] in
if let self { if let self {
switch self.currentInputMode { switch self.currentInputMode {

View File

@ -266,8 +266,9 @@ final class StoryPreviewComponent: Component {
stopAndPreviewMediaRecording: nil, stopAndPreviewMediaRecording: nil,
discardMediaRecordingPreview: nil, discardMediaRecordingPreview: nil,
attachmentAction: { }, attachmentAction: { },
hasLike: false, myReaction: nil,
likeAction: nil, likeAction: nil,
likeOptionsAction: nil,
inputModeAction: nil, inputModeAction: nil,
timeoutAction: nil, timeoutAction: nil,
forwardAction: {}, forwardAction: {},

View File

@ -32,6 +32,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/StickerPeekUI", "//submodules/StickerPeekUI",
"//submodules/Components/ReactionButtonListComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -9,6 +9,8 @@ import TelegramPresentationData
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import MoreHeaderButton import MoreHeaderButton
import ContextUI import ContextUI
import ReactionButtonListComponent
import TelegramCore
private extension MessageInputActionButtonComponent.Mode { private extension MessageInputActionButtonComponent.Mode {
var iconName: String? { var iconName: String? {
@ -19,12 +21,8 @@ private extension MessageInputActionButtonComponent.Mode {
return "Chat/Input/Text/IconAttachment" return "Chat/Input/Text/IconAttachment"
case .forward: case .forward:
return "Chat/Input/Text/IconForwardSend" return "Chat/Input/Text/IconForwardSend"
case let .like(isActive): case .like:
if isActive { return "Stories/InputLikeOff"
return "Stories/InputLikeOn"
} else {
return "Stories/InputLikeOff"
}
default: default:
return nil return nil
} }
@ -43,7 +41,7 @@ public final class MessageInputActionButtonComponent: Component {
case attach case attach
case forward case forward
case more case more
case like(isActive: Bool) case like(reaction: MessageReaction.Reaction?, file: TelegramMediaFile?, animationFileId: Int64?)
} }
public enum Action { public enum Action {
@ -127,12 +125,18 @@ public final class MessageInputActionButtonComponent: Component {
public let referenceNode: ContextReferenceContentNode public let referenceNode: ContextReferenceContentNode
public let containerNode: ContextControllerSourceNode public let containerNode: ContextControllerSourceNode
private let sendIconView: UIImageView private let sendIconView: UIImageView
private var moreButton: MoreHeaderButton? private var moreButton: MoreHeaderButton?
private var reactionIconView: ReactionIconView?
private var component: MessageInputActionButtonComponent? private var component: MessageInputActionButtonComponent?
private weak var componentState: EmptyComponentState? private weak var componentState: EmptyComponentState?
private var acceptNextButtonPress: Bool = false
public var likeIconView: UIView? {
return self.reactionIconView
}
override init(frame: CGRect) { override init(frame: CGRect) {
self.sendIconView = UIImageView() self.sendIconView = UIImageView()
@ -157,6 +161,7 @@ public final class MessageInputActionButtonComponent: Component {
guard let self, let component = self.component, let longPressAction = component.longPressAction else { guard let self, let component = self.component, let longPressAction = component.longPressAction else {
return return
} }
self.acceptNextButtonPress = false
longPressAction(self, gesture) 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.touchDown), forControlEvents: .touchDown)
self.button.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) 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) { required init?(coder: NSCoder) {
@ -182,6 +185,8 @@ public final class MessageInputActionButtonComponent: Component {
} }
@objc private func touchDown() { @objc private func touchDown() {
self.acceptNextButtonPress = true
guard let component = self.component else { guard let component = self.component else {
return return
} }
@ -189,6 +194,10 @@ public final class MessageInputActionButtonComponent: Component {
} }
@objc private func pressed() { @objc private func pressed() {
if !self.acceptNextButtonPress {
return
}
guard let component = self.component else { guard let component = self.component else {
return return
} }
@ -207,6 +216,11 @@ public final class MessageInputActionButtonComponent: Component {
let themeUpdated = previousComponent?.theme !== component.theme 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 self.containerNode.isUserInteractionEnabled = component.longPressAction != nil
if self.micButton == nil { if self.micButton == nil {
@ -306,8 +320,14 @@ public final class MessageInputActionButtonComponent: Component {
switch component.mode { switch component.mode {
case .none: case .none:
break break
case .send, .apply, .attach, .delete, .forward, .like: case .send, .apply, .attach, .delete, .forward:
sendAlpha = 1.0 sendAlpha = 1.0
case let .like(reaction, _, _):
if reaction != nil {
sendAlpha = 0.0
} else {
sendAlpha = 1.0
}
case .more: case .more:
moreAlpha = 1.0 moreAlpha = 1.0
case .videoInput, .voiceInput: case .videoInput, .voiceInput:
@ -318,10 +338,7 @@ public final class MessageInputActionButtonComponent: Component {
if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName { if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName {
if let iconName = component.mode.iconName { if let iconName = component.mode.iconName {
var tintColor: UIColor = .white let tintColor: UIColor = .white
if case .like(true) = component.mode {
tintColor = UIColor(rgb: 0xFF3B30)
}
self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: tintColor) self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: tintColor)
} else if case .apply = component.mode { } else if case .apply = component.mode {
self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), contextGenerator: { size, context in 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.button.view, frame: CGRect(origin: .zero, size: availableSize))
transition.setFrame(view: self.containerNode.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)) transition.setFrame(view: self.referenceNode.view, frame: CGRect(origin: .zero, size: availableSize))

View File

@ -44,6 +44,18 @@ public final class MessageInputPanelComponent: Component {
case emoji 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 final class ExternalState {
public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var isEditing: Bool = false
public fileprivate(set) var hasText: Bool = false public fileprivate(set) var hasText: Bool = false
@ -79,8 +91,9 @@ public final class MessageInputPanelComponent: Component {
public let stopAndPreviewMediaRecording: (() -> Void)? public let stopAndPreviewMediaRecording: (() -> Void)?
public let discardMediaRecordingPreview: (() -> Void)? public let discardMediaRecordingPreview: (() -> Void)?
public let attachmentAction: (() -> Void)? public let attachmentAction: (() -> Void)?
public let hasLike: Bool public let myReaction: MyReaction?
public let likeAction: (() -> Void)? public let likeAction: (() -> Void)?
public let likeOptionsAction: ((UIView, ContextGesture?) -> Void)?
public let inputModeAction: (() -> Void)? public let inputModeAction: (() -> Void)?
public let timeoutAction: ((UIView) -> Void)? public let timeoutAction: ((UIView) -> Void)?
public let forwardAction: (() -> Void)? public let forwardAction: (() -> Void)?
@ -126,8 +139,9 @@ public final class MessageInputPanelComponent: Component {
stopAndPreviewMediaRecording: (() -> Void)?, stopAndPreviewMediaRecording: (() -> Void)?,
discardMediaRecordingPreview: (() -> Void)?, discardMediaRecordingPreview: (() -> Void)?,
attachmentAction: (() -> Void)?, attachmentAction: (() -> Void)?,
hasLike: Bool, myReaction: MyReaction?,
likeAction: (() -> Void)?, likeAction: (() -> Void)?,
likeOptionsAction: ((UIView, ContextGesture?) -> Void)?,
inputModeAction: (() -> Void)?, inputModeAction: (() -> Void)?,
timeoutAction: ((UIView) -> Void)?, timeoutAction: ((UIView) -> Void)?,
forwardAction: (() -> Void)?, forwardAction: (() -> Void)?,
@ -172,8 +186,9 @@ public final class MessageInputPanelComponent: Component {
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
self.discardMediaRecordingPreview = discardMediaRecordingPreview self.discardMediaRecordingPreview = discardMediaRecordingPreview
self.attachmentAction = attachmentAction self.attachmentAction = attachmentAction
self.hasLike = hasLike self.myReaction = myReaction
self.likeAction = likeAction self.likeAction = likeAction
self.likeOptionsAction = likeOptionsAction
self.inputModeAction = inputModeAction self.inputModeAction = inputModeAction
self.timeoutAction = timeoutAction self.timeoutAction = timeoutAction
self.forwardAction = forwardAction self.forwardAction = forwardAction
@ -280,12 +295,15 @@ public final class MessageInputPanelComponent: Component {
if (lhs.attachmentAction == nil) != (rhs.attachmentAction == nil) { if (lhs.attachmentAction == nil) != (rhs.attachmentAction == nil) {
return false return false
} }
if lhs.hasLike != rhs.hasLike { if lhs.myReaction != rhs.myReaction {
return false return false
} }
if (lhs.likeAction == nil) != (rhs.likeAction == nil) { if (lhs.likeAction == nil) != (rhs.likeAction == nil) {
return false return false
} }
if (lhs.likeOptionsAction == nil) != (rhs.likeOptionsAction == nil) {
return false
}
return true return true
} }
@ -345,6 +363,10 @@ public final class MessageInputPanelComponent: Component {
return self.likeButton.view return self.likeButton.view
} }
public var likeIconView: UIView? {
return (self.likeButton.view as? MessageInputActionButtonComponent.View)?.likeIconView
}
override init(frame: CGRect) { override init(frame: CGRect) {
self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true) 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( let likeButtonSize = self.likeButton.update(
transition: transition, transition: transition,
component: AnyComponent(MessageInputActionButtonComponent( 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 action: { [weak self] _, action, _ in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
@ -1074,7 +1096,7 @@ public final class MessageInputPanelComponent: Component {
} }
component.likeAction?() component.likeAction?()
}, },
longPressAction: nil, longPressAction: component.likeOptionsAction,
switchMediaInputMode: { switchMediaInputMode: {
}, },
updateMediaCancelFraction: { _ in updateMediaCancelFraction: { _ in

View File

@ -25,7 +25,9 @@ swift_library(
"//submodules/AppBundle", "//submodules/AppBundle",
"//submodules/PeerPresenceStatusManager", "//submodules/PeerPresenceStatusManager",
"//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/ContextUI", "//submodules/ContextUI",
"//submodules/TextFormat",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -16,6 +16,8 @@ import AppBundle
import PeerPresenceStatusManager import PeerPresenceStatusManager
import EmojiStatusComponent import EmojiStatusComponent
import ContextUI import ContextUI
import EmojiTextAttachmentView
import TextFormat
private let avatarFont = avatarPlaceholderFont(size: 15.0) private let avatarFont = avatarPlaceholderFont(size: 15.0)
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) 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 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 context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
@ -55,7 +90,7 @@ public final class PeerListItemComponent: Component {
let subtitle: String? let subtitle: String?
let subtitleAccessory: SubtitleAccessory let subtitleAccessory: SubtitleAccessory
let presence: EnginePeer.Presence? let presence: EnginePeer.Presence?
let displayLike: Bool let reaction: Reaction?
let selectionState: SelectionState let selectionState: SelectionState
let hasNext: Bool let hasNext: Bool
let action: (EnginePeer) -> Void let action: (EnginePeer) -> Void
@ -74,7 +109,7 @@ public final class PeerListItemComponent: Component {
subtitle: String?, subtitle: String?,
subtitleAccessory: SubtitleAccessory, subtitleAccessory: SubtitleAccessory,
presence: EnginePeer.Presence?, presence: EnginePeer.Presence?,
displayLike: Bool = false, reaction: Reaction? = nil,
selectionState: SelectionState, selectionState: SelectionState,
hasNext: Bool, hasNext: Bool,
action: @escaping (EnginePeer) -> Void, action: @escaping (EnginePeer) -> Void,
@ -92,7 +127,7 @@ public final class PeerListItemComponent: Component {
self.subtitle = subtitle self.subtitle = subtitle
self.subtitleAccessory = subtitleAccessory self.subtitleAccessory = subtitleAccessory
self.presence = presence self.presence = presence
self.displayLike = displayLike self.reaction = reaction
self.selectionState = selectionState self.selectionState = selectionState
self.hasNext = hasNext self.hasNext = hasNext
self.action = action self.action = action
@ -134,7 +169,7 @@ public final class PeerListItemComponent: Component {
if lhs.presence != rhs.presence { if lhs.presence != rhs.presence {
return false return false
} }
if lhs.displayLike != rhs.displayLike { if lhs.reaction != rhs.reaction {
return false return false
} }
if lhs.selectionState != rhs.selectionState { if lhs.selectionState != rhs.selectionState {
@ -160,7 +195,10 @@ public final class PeerListItemComponent: Component {
private var iconView: UIImageView? private var iconView: UIImageView?
private var checkLayer: CheckLayer? 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 var component: PeerListItemComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -251,6 +289,10 @@ public final class PeerListItemComponent: Component {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
self.fileDisposable?.dispose()
}
@objc private func pressed() { @objc private func pressed() {
guard let component = self.component, let peer = component.peer else { guard let component = self.component, let peer = component.peer else {
return return
@ -265,7 +307,49 @@ public final class PeerListItemComponent: Component {
component.openStories?(peer, self.avatarNode) 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<Empty>, transition: Transition) -> CGSize { func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousComponent = self.component
var synchronousLoad = false var synchronousLoad = false
if let hint = transition.userData(TransitionHint.self) { if let hint = transition.userData(TransitionHint.self) {
synchronousLoad = hint.synchronousLoad synchronousLoad = hint.synchronousLoad
@ -351,7 +435,7 @@ public final class PeerListItemComponent: Component {
leftInset += 9.0 leftInset += 9.0
} }
var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
if component.displayLike { if component.reaction != nil {
rightInset += 32.0 rightInset += 32.0
} }
@ -581,25 +665,29 @@ public final class PeerListItemComponent: Component {
transition.setFrame(view: labelView, frame: labelFrame) transition.setFrame(view: labelView, frame: labelFrame)
} }
if component.displayLike { let imageSize = CGSize(width: 22.0, height: 22.0)
let likeIconView: UIImageView 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 let current = self.likeIconView {
likeIconView = current 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 { } else {
likeIconView = UIImageView() self.file = nil
self.likeIconView = likeIconView self.updateReactionLayer()
self.containerButton.addSubview(likeIconView)
likeIconView.image = PresentationResourcesChat.storyViewListLikeIcon(component.theme)
} }
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 { if themeUpdated {

View File

@ -158,6 +158,7 @@ public final class StoryContentContextImpl: StoryContentContext {
views: item.views.flatMap { views in views: item.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init) return peers[id].flatMap(EnginePeer.init)
} }
@ -173,7 +174,7 @@ public final class StoryContentContextImpl: StoryContentContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
hasLike: item.hasLike myReaction: item.myReaction
) )
} }
var totalCount = peerStoryItemsView.items.count var totalCount = peerStoryItemsView.items.count
@ -198,7 +199,7 @@ public final class StoryContentContextImpl: StoryContentContext {
isSelectedContacts: item.privacy.base == .nobody, isSelectedContacts: item.privacy.base == .nobody,
isForwardingDisabled: false, isForwardingDisabled: false,
isEdited: false, isEdited: false,
hasLike: false myReaction: nil
)) ))
totalCount += 1 totalCount += 1
} }
@ -1029,6 +1030,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
views: itemValue.views.flatMap { views in views: itemValue.views.flatMap { views in
return EngineStoryItem.Views( return EngineStoryItem.Views(
seenCount: views.seenCount, seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init) return peers[id].flatMap(EnginePeer.init)
} }
@ -1044,7 +1046,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
isSelectedContacts: itemValue.isSelectedContacts, isSelectedContacts: itemValue.isSelectedContacts,
isForwardingDisabled: itemValue.isForwardingDisabled, isForwardingDisabled: itemValue.isForwardingDisabled,
isEdited: itemValue.isEdited, isEdited: itemValue.isEdited,
hasLike: itemValue.hasLike myReaction: itemValue.myReaction
) )
let mainItem = StoryContentItem( let mainItem = StoryContentItem(

View File

@ -20,6 +20,7 @@ import VolumeButtons
import TooltipUI import TooltipUI
import ChatEntityKeyboardInputNode import ChatEntityKeyboardInputNode
import notify import notify
import TelegramNotices
func hasFirstResponder(_ view: UIView) -> Bool { func hasFirstResponder(_ view: UIView) -> Bool {
if view.isFirstResponder { if view.isFirstResponder {
@ -383,6 +384,8 @@ private final class StoryContainerScreenComponent: Component {
private var pendingNavigationToItemId: (peerId: EnginePeer.Id, id: Int32)? private var pendingNavigationToItemId: (peerId: EnginePeer.Id, id: Int32)?
private var didDisplayReactionTooltip: Bool = false
override init(frame: CGRect) { override init(frame: CGRect) {
self.backgroundLayer = SimpleLayer() self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor.black.cgColor self.backgroundLayer.backgroundColor = UIColor.black.cgColor
@ -912,6 +915,30 @@ private final class StoryContainerScreenComponent: Component {
self?.layer.allowsGroupOpacity = false 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) { func animateOut(completion: @escaping () -> Void) {
@ -1095,6 +1122,7 @@ private final class StoryContainerScreenComponent: Component {
} }
} }
}) })
update = true update = true
} }

View File

@ -421,6 +421,8 @@ public final class StoryItemSetContainerComponent: Component {
var reactionContextNode: ReactionContextNode? var reactionContextNode: ReactionContextNode?
weak var disappearingReactionContextNode: ReactionContextNode? weak var disappearingReactionContextNode: ReactionContextNode?
var displayLikeReactions: Bool = false
var waitingForReactionAnimateOutToLike: MessageReaction.Reaction?
weak var contextController: ContextController? weak var contextController: ContextController?
weak var privacyController: ShareWithPeersScreen? weak var privacyController: ShareWithPeersScreen?
@ -781,7 +783,11 @@ public final class StoryItemSetContainerComponent: Component {
if let _ = self.sendMessageContext.menuController { if let _ = self.sendMessageContext.menuController {
return 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 { Queue.mainQueue().justDispatch {
self.deactivateInput() self.deactivateInput()
} }
@ -947,10 +953,21 @@ public final class StoryItemSetContainerComponent: Component {
} }
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 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) { if let inputView = self.inputPanel.view, let inputViewHitTest = inputView.hitTest(self.convert(point, to: inputView), with: event) {
return inputViewHitTest return inputViewHitTest
} }
guard let result = super.hitTest(point, with: event) else {
guard let result else {
return nil return nil
} }
@ -1065,6 +1082,9 @@ public final class StoryItemSetContainerComponent: Component {
if let captionItem = self.captionItem, captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText { if let captionItem = self.captionItem, captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText {
return .blurred return .blurred
} }
if self.displayLikeReactions {
return .blurred
}
return .play 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<Empty>, transition: Transition) -> CGSize { func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let isFirstTime = self.component == nil let isFirstTime = self.component == nil
@ -2042,13 +2123,44 @@ public final class StoryItemSetContainerComponent: Component {
} }
self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default) 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 likeAction: component.slice.peer.isService ? nil : { [weak self] in
guard let self else { guard let self else {
return return
} }
self.performLikeAction() 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 inputModeAction: { [weak self] in
guard let self else { guard let self else {
return return
@ -2320,6 +2432,7 @@ public final class StoryItemSetContainerComponent: Component {
minimizedContentHeight: 325.0, minimizedContentHeight: 325.0,
outerExpansionFraction: outerExpansionFraction, outerExpansionFraction: outerExpansionFraction,
outerExpansionDirection: outerExpansionDirection, outerExpansionDirection: outerExpansionDirection,
availableReactions: component.availableReactions,
close: { [weak self] in close: { [weak self] in
guard let self else { guard let self else {
return 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 var effectiveDisplayReactions = false
if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText { if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText {
effectiveDisplayReactions = true
}
if self.displayLikeReactions {
effectiveDisplayReactions = true effectiveDisplayReactions = true
} }
if self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil { if self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil {
@ -3262,7 +3387,7 @@ public final class StoryItemSetContainerComponent: Component {
animationCache: component.context.animationCache, animationCache: component.context.animationCache,
presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme), presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme),
items: reactionItems.map(ReactionContextItem.reaction), items: reactionItems.map(ReactionContextItem.reaction),
selectedItems: Set(), selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(),
getEmojiContent: { [weak self] animationCache, animationRenderer in getEmojiContent: { [weak self] animationCache, animationRenderer in
guard let self, let component = self.component else { guard let self, let component = self.component else {
preconditionFailure() preconditionFailure()
@ -3308,34 +3433,30 @@ public final class StoryItemSetContainerComponent: Component {
self.state?.updated(transition: Transition(transition)) self.state?.updated(transition: Transition(transition))
} }
) )
reactionContextNode.displayTail = false reactionContextNode.displayTail = self.displayLikeReactions
reactionContextNode.forceTailToRight = self.displayLikeReactions
self.reactionContextNode = reactionContextNode 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 { guard let self, let component = self.component else {
return return
} }
let _ = (component.context.engine.stickers.availableReactions() if self.displayLikeReactions {
|> take(1) if component.slice.item.storyItem.myReaction == updateReaction.reaction {
|> deliverOnMainQueue).start(next: { [weak self] availableReactions in let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).start()
guard let self, let component = self.component, let availableReactions else { self.displayLikeReactions = false
return 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()
} }
} else {
var animation: TelegramMediaFile?
for reaction in availableReactions.reactions {
if reaction.value == updateReaction.reaction {
animation = reaction.centerAnimation
break
}
}
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))) 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 targetView.isUserInteractionEnabled = false
self.addSubview(targetView) self.addSubview(targetView)
if let reactionContextNode { if let reactionContextNode = self.reactionContextNode {
reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction) reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction)
reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in
guard let self else { guard let self else {
@ -3359,74 +3480,9 @@ public final class StoryItemSetContainerComponent: Component {
self.endEditing(true) self.endEditing(true)
} }
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) 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 _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start()
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)
}
})
})
} }
reactionContextNode.premiumReactionsSelected = { [weak self] file in reactionContextNode.premiumReactionsSelected = { [weak self] file in
@ -3487,12 +3543,41 @@ public final class StoryItemSetContainerComponent: Component {
} }
} else { } else {
reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) 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 { if animateReactionsIn {
reactionContextNode.animateIn(from: reactionsAnchorRect) 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 { } else {
if let reactionContextNode = self.reactionContextNode { if let reactionContextNode = self.reactionContextNode {
if let disappearingReactionContextNode = self.disappearingReactionContextNode { if let disappearingReactionContextNode = self.disappearingReactionContextNode {
@ -4215,9 +4300,9 @@ public final class StoryItemSetContainerComponent: Component {
return 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 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() { func dismissAllTooltips() {
guard let component = self.component, let controller = component.controller() else { guard let component = self.component, let controller = component.controller() else {
return return

View File

@ -67,6 +67,7 @@ final class StoryItemSetViewListComponent: Component {
let minimizedContentHeight: CGFloat let minimizedContentHeight: CGFloat
let outerExpansionFraction: CGFloat let outerExpansionFraction: CGFloat
let outerExpansionDirection: Bool let outerExpansionDirection: Bool
let availableReactions: StoryAvailableReactions?
let close: () -> Void let close: () -> Void
let expandViewStats: () -> Void let expandViewStats: () -> Void
let deleteAction: () -> Void let deleteAction: () -> Void
@ -88,6 +89,7 @@ final class StoryItemSetViewListComponent: Component {
minimizedContentHeight: CGFloat, minimizedContentHeight: CGFloat,
outerExpansionFraction: CGFloat, outerExpansionFraction: CGFloat,
outerExpansionDirection: Bool, outerExpansionDirection: Bool,
availableReactions: StoryAvailableReactions?,
close: @escaping () -> Void, close: @escaping () -> Void,
expandViewStats: @escaping () -> Void, expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void, deleteAction: @escaping () -> Void,
@ -108,6 +110,7 @@ final class StoryItemSetViewListComponent: Component {
self.minimizedContentHeight = minimizedContentHeight self.minimizedContentHeight = minimizedContentHeight
self.outerExpansionFraction = outerExpansionFraction self.outerExpansionFraction = outerExpansionFraction
self.outerExpansionDirection = outerExpansionDirection self.outerExpansionDirection = outerExpansionDirection
self.availableReactions = availableReactions
self.close = close self.close = close
self.expandViewStats = expandViewStats self.expandViewStats = expandViewStats
self.deleteAction = deleteAction self.deleteAction = deleteAction
@ -143,6 +146,9 @@ final class StoryItemSetViewListComponent: Component {
if lhs.outerExpansionDirection != rhs.outerExpansionDirection { if lhs.outerExpansionDirection != rhs.outerExpansionDirection {
return false return false
} }
if lhs.availableReactions !== rhs.availableReactions {
return false
}
return true return true
} }
@ -517,7 +523,29 @@ final class StoryItemSetViewListComponent: Component {
subtitle: dateText, subtitle: dateText,
subtitleAccessory: .checks, subtitleAccessory: .checks,
presence: nil, 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, selectionState: .none,
hasNext: index != viewListState.totalCount - 1, hasNext: index != viewListState.totalCount - 1,
action: { [weak self] peer in action: { [weak self] peer in
@ -670,7 +698,7 @@ final class StoryItemSetViewListComponent: Component {
applyState = true applyState = true
let _ = synchronous let _ = synchronous
} else { } 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 var externalViews: EngineStoryItem.Views? = component.storyItem.views
if let viewListState = self.viewListState, !viewListState.items.isEmpty { 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( let navigationPanelSize = self.navigationPanel.update(

View File

@ -76,6 +76,9 @@ public final class StoryFooterPanelComponent: Component {
private let viewStatsExpandedText: AnimatedCountLabelView private let viewStatsExpandedText: AnimatedCountLabelView
private let deleteButton = ComponentView<Empty>() private let deleteButton = ComponentView<Empty>()
private var reactionStatsIcon: UIImageView?
private var reactionStatsText: AnimatedCountLabelView?
private var statusButton: HighlightableButton? private var statusButton: HighlightableButton?
private var statusNode: SemanticStatusNode? private var statusNode: SemanticStatusNode?
private var uploadingText: ComponentView<Empty>? private var uploadingText: ComponentView<Empty>?
@ -114,9 +117,13 @@ public final class StoryFooterPanelComponent: Component {
if highlighted { if highlighted {
self.avatarsView.alpha = 0.7 self.avatarsView.alpha = 0.7
self.viewStatsText.alpha = 0.7 self.viewStatsText.alpha = 0.7
self.reactionStatsIcon?.alpha = 0.7
self.reactionStatsText?.alpha = 0.7
} else { } else {
self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) 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.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) self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside)
@ -278,8 +285,10 @@ public final class StoryFooterPanelComponent: Component {
} }
var viewCount = 0 var viewCount = 0
var reactionCount = 0
if let views = component.externalViews ?? component.storyItem?.views, views.seenCount != 0 { if let views = component.externalViews ?? component.storyItem?.views, views.seenCount != 0 {
viewCount = views.seenCount viewCount = views.seenCount
reactionCount = views.reactedCount
} }
let viewsText: String let viewsText: String
@ -353,7 +362,65 @@ public final class StoryFooterPanelComponent: Component {
transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width) 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 self.viewStatsButton.isUserInteractionEnabled = component.expandFraction == 0.0
var rightContentOffset: CGFloat = availableSize.width - 12.0 var rightContentOffset: CGFloat = availableSize.width - 12.0