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

View File

@ -233,6 +233,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private var animationHideNode: Bool = false
public var displayTail: Bool = true
public var forceTailToRight: Bool = true
private var didAnimateIn: Bool = false
public private(set) var isAnimatingOut: Bool = false
@ -636,12 +637,20 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
contentSize.width = max(46.0, contentSize.width)
contentSize.height = self.currentContentHeight
let sideInset: CGFloat = 11.0 + insets.left
let sideInset: CGFloat
if self.forceTailToRight {
sideInset = insets.left
} else {
sideInset = 11.0 + insets.left
}
let backgroundOffset: CGPoint = CGPoint(x: 22.0, y: -7.0)
var rect: CGRect
let isLeftAligned: Bool
if anchorRect.minX < containerSize.width - anchorRect.maxX {
if self.forceTailToRight {
rect = CGRect(origin: CGPoint(x: anchorRect.minX - backgroundOffset.x - 4.0, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize)
isLeftAligned = false
} else if anchorRect.minX < containerSize.width - anchorRect.maxX {
rect = CGRect(origin: CGPoint(x: anchorRect.maxX - contentSize.width + backgroundOffset.x, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize)
isLeftAligned = true
} else {
@ -665,7 +674,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
let cloudSourcePoint: CGFloat
if isLeftAligned {
if self.forceTailToRight {
cloudSourcePoint = min(rect.maxX - 46.0 / 2.0, anchorRect.maxX - 4.0)
} else if isLeftAligned {
cloudSourcePoint = min(rect.maxX - 46.0 / 2.0, anchorRect.maxX - 4.0)
} else {
cloudSourcePoint = max(rect.minX + 46.0 / 2.0, anchorRect.minX)
@ -1190,6 +1201,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
isMinimized: self.highlightedReaction != nil && !self.highlightedByHover,
isCoveredByInput: isCoveredByInput,
displayTail: self.displayTail,
forceTailToRight: self.forceTailToRight,
transition: transition
)
@ -1776,7 +1788,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
}
public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) {
public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) {
self.isAnimatingOutToReaction = true
var foundItemNode: ReactionNode?
@ -1808,7 +1820,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji {
switch itemNode.item.reaction.rawValue {
case .builtin:
switchToInlineImmediately = false
switchToInlineImmediately = forceSwitchToInlineImmediately
case .custom:
switchToInlineImmediately = !self.didTriggerExpandedReaction
}

View File

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

View File

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

View File

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

View File

@ -8472,15 +8472,15 @@ public extension Api.functions.stickers {
}
}
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()
buffer.appendInt32(299359662)
buffer.appendInt32(1471926630)
serializeInt32(flags, buffer: buffer, boxed: false)
return (FunctionDescription(name: "stories.activateStealthMode", parameters: [("flags", String(describing: flags))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
return (FunctionDescription(name: "stories.activateStealthMode", parameters: [("flags", String(describing: flags))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Bool?
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
@ -8658,14 +8658,15 @@ public extension Api.functions.stories {
}
}
public extension Api.functions.stories {
static func getStoryViewsList(id: Int32, offsetDate: Int32, offsetId: Int64, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<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()
buffer.appendInt32(1262182039)
buffer.appendInt32(-111189596)
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeString(q!, buffer: buffer, boxed: false)}
serializeInt32(id, buffer: buffer, boxed: false)
serializeInt32(offsetDate, buffer: buffer, boxed: false)
serializeInt64(offsetId, buffer: buffer, boxed: false)
serializeString(offset, buffer: buffer, boxed: false)
serializeInt32(limit, buffer: buffer, boxed: false)
return (FunctionDescription(name: "stories.getStoryViewsList", parameters: [("id", String(describing: id)), ("offsetDate", String(describing: offsetDate)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.StoryViewsList? in
return (FunctionDescription(name: "stories.getStoryViewsList", parameters: [("flags", String(describing: flags)), ("q", String(describing: q)), ("id", String(describing: id)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.StoryViewsList? in
let reader = BufferReader(buffer)
var result: Api.stories.StoryViewsList?
if let signature = reader.readInt32() {
@ -8748,6 +8749,24 @@ public extension Api.functions.stories {
})
}
}
public extension Api.functions.stories {
static func sendReaction(flags: Int32, userId: Api.InputUser, storyId: Int32, reaction: Api.Reaction) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<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 {
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()

View File

@ -123,7 +123,7 @@ enum AccountStateMutationOperation {
case UpdateStory(peerId: PeerId, story: Api.StoryItem)
case UpdateReadStories(peerId: PeerId, maxId: Int32)
case UpdateStoryStealthMode(data: Api.StoriesStealthMode)
case UpdateStoryStealth(expireDate: Int32)
case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction)
}
struct HoleFromPreviousState {
@ -649,13 +649,13 @@ struct AccountMutableState {
self.addOperation(.UpdateStoryStealthMode(data: data))
}
mutating func updateStoryStealth(expireDate: Int32) {
self.addOperation(.UpdateStoryStealth(expireDate: expireDate))
mutating func updateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) {
self.addOperation(.UpdateStorySentReaction(peerId: peerId, id: id, reaction: reaction))
}
mutating func addOperation(_ operation: AccountStateMutationOperation) {
switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStoryStealth:
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction:
break
case let .AddMessages(messages, location):
for message in messages {

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)
case let .updateStoriesStealthMode(stealthMode):
updatedState.updateStoryStealthMode(stealthMode)
case let .updateSentStoryReaction(userId, storyId, reaction):
updatedState.updateStorySentReaction(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId, reaction: reaction)
default:
break
}
@ -3167,7 +3169,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
var currentAddScheduledMessages: OptimizeAddMessagesState?
for operation in operations {
switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStoryStealth:
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction:
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
}
@ -4557,10 +4559,65 @@ func replayFinalState(
var configuration = _internal_getStoryConfigurationState(transaction: transaction)
configuration.stealthModeState = Stories.StealthModeState(apiMode: data)
_internal_setStoryConfigurationState(transaction: transaction, state: configuration)
case let .UpdateStoryStealth(expireDate):
var configuration = _internal_getStoryConfigurationState(transaction: transaction)
configuration.stealthModeState.activeUntilTimestamp = expireDate
_internal_setStoryConfigurationState(transaction: transaction, state: configuration)
case let .UpdateStorySentReaction(peerId, id, reaction):
var updatedPeerEntries: [StoryItemsTableEntry] = transaction.getStoryItems(peerId: peerId)
if let index = updatedPeerEntries.firstIndex(where: { item in
return item.id == id
}) {
if let value = updatedPeerEntries[index].value.get(Stories.StoredItem.self), case let .item(item) = value {
let updatedItem: Stories.StoredItem = .item(Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: item.views,
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
myReaction: MessageReaction.Reaction(apiReaction: reaction)
))
if let entry = CodableEntry(updatedItem) {
updatedPeerEntries[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: item.expirationTimestamp, isCloseFriends: item.isCloseFriends)
}
}
}
transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries)
if let value = transaction.getStory(id: StoryId(peerId: peerId, id: id))?.get(Stories.StoredItem.self), case let .item(item) = value {
let updatedItem: Stories.StoredItem = .item(Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: item.views,
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
myReaction: MessageReaction.Reaction(apiReaction: reaction)
))
if let entry = CodableEntry(updatedItem) {
transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry)
}
}
}
}

View File

@ -41,14 +41,17 @@ public enum Stories {
public struct Views: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case seenCount = "seenCount"
case reactedCount = "reactedCount"
case seenPeerIds = "seenPeerIds"
}
public var seenCount: Int
public var reactedCount: Int
public var seenPeerIds: [PeerId]
public init(seenCount: Int, seenPeerIds: [PeerId]) {
public init(seenCount: Int, reactedCount: Int, seenPeerIds: [PeerId]) {
self.seenCount = seenCount
self.reactedCount = reactedCount
self.seenPeerIds = seenPeerIds
}
@ -56,6 +59,7 @@ public enum Stories {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount))
self.reactedCount = Int(try container.decodeIfPresent(Int32.self, forKey: .reactedCount) ?? 0)
self.seenPeerIds = try container.decode([Int64].self, forKey: .seenPeerIds).map(PeerId.init)
}
@ -63,6 +67,7 @@ public enum Stories {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(Int32(clamping: self.seenCount), forKey: .seenCount)
try container.encode(Int32(clamping: self.reactedCount), forKey: .reactedCount)
try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds)
}
}
@ -137,7 +142,7 @@ public enum Stories {
case isSelectedContacts
case isForwardingDisabled
case isEdited
case hasLike
case myReaction
}
public let id: Int32
@ -157,7 +162,7 @@ public enum Stories {
public let isSelectedContacts: Bool
public let isForwardingDisabled: Bool
public let isEdited: Bool
public let hasLike: Bool
public let myReaction: MessageReaction.Reaction?
public init(
id: Int32,
@ -177,7 +182,7 @@ public enum Stories {
isSelectedContacts: Bool,
isForwardingDisabled: Bool,
isEdited: Bool,
hasLike: Bool
myReaction: MessageReaction.Reaction?
) {
self.id = id
self.timestamp = timestamp
@ -196,7 +201,7 @@ public enum Stories {
self.isSelectedContacts = isSelectedContacts
self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited
self.hasLike = hasLike
self.myReaction = myReaction
}
public init(from decoder: Decoder) throws {
@ -225,7 +230,7 @@ public enum Stories {
self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false
self.hasLike = try container.decodeIfPresent(Bool.self, forKey: .hasLike) ?? false
self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction)
}
public func encode(to encoder: Encoder) throws {
@ -255,7 +260,7 @@ public enum Stories {
try container.encode(self.isSelectedContacts, forKey: .isSelectedContacts)
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
try container.encode(self.isEdited, forKey: .isEdited)
try container.encode(self.hasLike, forKey: .hasLike)
try container.encodeIfPresent(self.myReaction, forKey: .myReaction)
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -317,7 +322,7 @@ public enum Stories {
if lhs.isEdited != rhs.isEdited {
return false
}
if lhs.hasLike != rhs.hasLike {
if lhs.myReaction != rhs.myReaction {
return false
}
@ -949,7 +954,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
for update in updates.allUpdates {
if case let .updateStory(_, story) = update {
switch story {
case let .storyItem(_, idValue, _, _, _, _, media, _, _, _):
case let .storyItem(_, idValue, _, _, _, _, media, _, _, _, _):
if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: accountPeerId, transaction: transaction) {
var items = transaction.getStoryItems(peerId: accountPeerId)
var updatedItems: [Stories.Item] = []
@ -972,7 +977,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends))
@ -1094,7 +1099,7 @@ func _internal_editStory(account: Account, id: Int32, media: EngineStoryInputMed
for update in updates.allUpdates {
if case let .updateStory(_, story) = update {
switch story {
case let .storyItem(_, _, _, _, _, _, media, _, _, _):
case let .storyItem(_, _, _, _, _, _, media, _, _, _, _):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
if let parsedMedia = parsedMedia, let originalMedia = originalMedia {
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false)
@ -1136,7 +1141,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
transaction.setStory(id: storyId, value: entry)
@ -1164,7 +1169,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
@ -1292,7 +1297,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
@ -1319,7 +1324,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
updatedItems.append(updatedItem)
}
@ -1345,7 +1350,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
extension Api.StoryItem {
var id: Int32 {
switch self {
case let .storyItem(_, id, _, _, _, _, _, _, _, _):
case let .storyItem(_, id, _, _, _, _, _, _, _, _, _):
return id
case let .storyItemDeleted(id):
return id
@ -1358,12 +1363,12 @@ extension Api.StoryItem {
extension Stories.Item.Views {
init(apiViews: Api.StoryViews) {
switch apiViews {
case let .storyViews(_, viewsCount, recentViewers):
case let .storyViews(_, viewsCount, reactionsCount, recentViewers):
var seenPeerIds: [PeerId] = []
if let recentViewers = recentViewers {
seenPeerIds = recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }
}
self.init(seenCount: Int(viewsCount), seenPeerIds: seenPeerIds)
self.init(seenCount: Int(viewsCount), reactedCount: Int(reactionsCount), seenPeerIds: seenPeerIds)
}
}
}
@ -1371,7 +1376,7 @@ extension Stories.Item.Views {
extension Stories.StoredItem {
init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) {
switch apiStoryItem {
case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views):
case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let parsedMedia = parsedMedia {
var parsedPrivacy: Stories.Item.Privacy?
@ -1428,6 +1433,13 @@ extension Stories.StoredItem {
mergedViews = views.flatMap(Stories.Item.Views.init(apiViews:))
}
var mergedMyReaction: MessageReaction.Reaction?
if isMin, let existingItem = existingItem {
mergedMyReaction = existingItem.myReaction
} else {
mergedMyReaction = sentReaction.flatMap(MessageReaction.Reaction.init(apiReaction:))
}
let item = Stories.Item(
id: id,
timestamp: date,
@ -1446,7 +1458,7 @@ extension Stories.StoredItem {
isSelectedContacts: isSelectedContacts,
isForwardingDisabled: isForwardingDisabled,
isEdited: isEdited,
hasLike: false
myReaction: mergedMyReaction
)
self = .item(item)
} else {
@ -1533,42 +1545,12 @@ public final class StoryViewList {
public let items: [Item]
public let totalCount: Int
public let totalReactedCount: Int
public init(items: [Item], totalCount: Int) {
public init(items: [Item], totalCount: Int, totalReactedCount: Int) {
self.items = items
self.totalCount = totalCount
}
}
func _internal_getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<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))
}
}
self.totalReactedCount = totalReactedCount
}
}
@ -1603,26 +1585,28 @@ func _internal_getStoryViews(account: Account, ids: [Int32]) -> Signal<[Int32: S
public final class EngineStoryViewListContext {
public struct LoadMoreToken: Equatable {
var id: Int64
var timestamp: Int32
var value: String
}
public final class Item: Equatable {
public let peer: EnginePeer
public let timestamp: Int32
public let storyStats: PeerStoryStats?
public let isLike: Bool
public let reaction: MessageReaction.Reaction?
public let reactionFile: TelegramMediaFile?
public init(
peer: EnginePeer,
timestamp: Int32,
storyStats: PeerStoryStats?,
isLike: Bool
reaction: MessageReaction.Reaction?,
reactionFile: TelegramMediaFile?
) {
self.peer = peer
self.timestamp = timestamp
self.storyStats = storyStats
self.isLike = isLike
self.reaction = reaction
self.reactionFile = reactionFile
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -1635,7 +1619,10 @@ public final class EngineStoryViewListContext {
if lhs.storyStats != rhs.storyStats {
return false
}
if lhs.isLike != rhs.isLike {
if lhs.reaction != rhs.reaction {
return false
}
if lhs.reactionFile?.fileId != rhs.reactionFile?.fileId {
return false
}
return true
@ -1644,15 +1631,18 @@ public final class EngineStoryViewListContext {
public struct State: Equatable {
public var totalCount: Int
public var totalReactedCount: Int
public var items: [Item]
public var loadMoreToken: LoadMoreToken?
public init(
totalCount: Int,
totalReactedCount: Int,
items: [Item],
loadMoreToken: LoadMoreToken?
) {
self.totalCount = totalCount
self.totalReactedCount = totalReactedCount
self.items = items
self.loadMoreToken = loadMoreToken
}
@ -1660,12 +1650,12 @@ public final class EngineStoryViewListContext {
private final class Impl {
struct NextOffset: Equatable {
var id: Int64
var timestamp: Int32
var value: String
}
struct InternalState: Equatable {
var totalCount: Int
var totalReactedCount: Int
var items: [Item]
var canLoadMore: Bool
var nextOffset: NextOffset?
@ -1689,8 +1679,8 @@ public final class EngineStoryViewListContext {
self.account = account
self.storyId = storyId
let initialState = State(totalCount: views.seenCount, items: [], loadMoreToken: LoadMoreToken(id: 0, timestamp: 0))
self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil)
let initialState = State(totalCount: views.seenCount, totalReactedCount: views.reactedCount, items: [], loadMoreToken: LoadMoreToken(value: ""))
self.state = InternalState(totalCount: initialState.totalCount, totalReactedCount: initialState.totalReactedCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil)
self.statePromise.set(.single(self.state))
if initialState.loadMoreToken != nil {
@ -1722,7 +1712,7 @@ public final class EngineStoryViewListContext {
let signal: Signal<InternalState, NoError> = self.account.postbox.transaction { transaction -> Void 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)
|> `catch` { _ -> Signal<Api.stories.StoryViewsList?, NoError> in
return .single(nil)
@ -1730,14 +1720,13 @@ public final class EngineStoryViewListContext {
|> mapToSignal { result -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in
switch result {
case let .storyViewsList(count, views, users):
case let .storyViewsList(_, count, reactionsCount, views, users, nextOffset):
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
var items: [Item] = []
var nextOffset: NextOffset?
for view in views {
switch view {
case let .storyView(flags, userId, date):
case let .storyView(flags, userId, date, reaction):
let isBlocked = (flags & (1 << 0)) != 0
let isBlockedFromStories = (flags & (1 << 1)) != 0
@ -1758,9 +1747,21 @@ public final class EngineStoryViewListContext {
return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags)
})
if let peer = transaction.getPeer(peerId) {
items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId), isLike: false))
nextOffset = NextOffset(id: userId, timestamp: date)
let parsedReaction = reaction.flatMap(MessageReaction.Reaction.init(apiReaction:))
items.append(Item(
peer: EnginePeer(peer),
timestamp: date,
storyStats: transaction.getPeerStoryStats(peerId: peerId),
reaction: parsedReaction,
reactionFile: parsedReaction.flatMap { reaction -> TelegramMediaFile? in
switch reaction {
case .builtin:
return nil
case let .custom(fileId):
return transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile
}
}
))
}
}
}
@ -1774,7 +1775,7 @@ public final class EngineStoryViewListContext {
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds),
views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
@ -1784,7 +1785,7 @@ public final class EngineStoryViewListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
))
if let entry = CodableEntry(updatedItem) {
transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry)
@ -1803,7 +1804,7 @@ public final class EngineStoryViewListContext {
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds),
views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
@ -1813,7 +1814,7 @@ public final class EngineStoryViewListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
))
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
@ -1823,9 +1824,9 @@ public final class EngineStoryViewListContext {
}
transaction.setStoryItems(peerId: account.peerId, items: currentItems)
return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset)
return InternalState(totalCount: Int(count), totalReactedCount: Int(reactionsCount), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) })
case .none:
return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil)
return InternalState(totalCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil)
}
}
}
@ -1853,10 +1854,22 @@ public final class EngineStoryViewListContext {
existingItems.insert(itemHash)
strongSelf.state.items.append(item)
}
var allReactedCount = 0
for item in strongSelf.state.items {
if item.reaction != nil {
allReactedCount += 1
} else {
break
}
}
if state.canLoadMore {
strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count)
strongSelf.state.totalReactedCount = max(state.totalReactedCount, allReactedCount)
} else {
strongSelf.state.totalCount = strongSelf.state.items.count
strongSelf.state.totalReactedCount = allReactedCount
}
strongSelf.state.canLoadMore = state.canLoadMore
strongSelf.state.nextOffset = state.nextOffset
@ -1884,7 +1897,8 @@ public final class EngineStoryViewListContext {
peer: item.peer,
timestamp: item.timestamp,
storyStats: value,
isLike: false
reaction: item.reaction,
reactionFile: item.reactionFile
)
}
}
@ -1907,10 +1921,11 @@ public final class EngineStoryViewListContext {
disposable.set(impl.statePromise.get().start(next: { state in
var loadMoreToken: LoadMoreToken?
if let nextOffset = state.nextOffset {
loadMoreToken = LoadMoreToken(id: nextOffset.id, timestamp: nextOffset.timestamp)
loadMoreToken = LoadMoreToken(value: nextOffset.value)
}
subscriber.putNext(State(
totalCount: state.totalCount,
totalReactedCount: state.totalReactedCount,
items: state.items,
loadMoreToken: loadMoreToken
))
@ -2113,10 +2128,15 @@ func _internal_enableStoryStealthMode(account: Account) -> Signal<Never, NoError
flags |= 1 << 0
flags |= 1 << 1
return account.network.request(Api.functions.stories.activateStealthMode(flags: flags))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
if let result = result {
account.stateManager.addUpdates(result)
}
return account.postbox.transaction { transaction in
let appConfig = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue
@ -2158,8 +2178,15 @@ public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction,
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data()))
}
func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
guard let peer = transaction.getPeer(peerId) else {
return nil
}
guard let inputUser = apiInputUser(peer) else {
return nil
}
var currentItems = transaction.getStoryItems(peerId: peerId)
for i in 0 ..< currentItems.count {
if currentItems[i].id == id {
@ -2182,7 +2209,7 @@ func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: hasLike
myReaction: reaction
))
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
@ -2211,12 +2238,30 @@ func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: hasLike
myReaction: reaction
))
if let entry = CodableEntry(updatedItem) {
transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry)
}
}
return inputUser
}
|> mapToSignal { inputUser -> Signal<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 Views: Equatable {
public let seenCount: Int
public let reactedCount: Int
public let seenPeers: [EnginePeer]
public init(seenCount: Int, seenPeers: [EnginePeer]) {
public init(seenCount: Int, reactedCount: Int, seenPeers: [EnginePeer]) {
self.seenCount = seenCount
self.reactedCount = reactedCount
self.seenPeers = seenPeers
}
@ -24,6 +26,9 @@ public final class EngineStoryItem: Equatable {
if lhs.seenCount != rhs.seenCount {
return false
}
if lhs.reactedCount != rhs.reactedCount {
return false
}
if lhs.seenPeers != rhs.seenPeers {
return false
}
@ -49,9 +54,9 @@ public final class EngineStoryItem: Equatable {
public let isSelectedContacts: Bool
public let isForwardingDisabled: Bool
public let isEdited: Bool
public let hasLike: Bool
public let myReaction: MessageReaction.Reaction?
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, hasLike: Bool) {
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, myReaction: MessageReaction.Reaction?) {
self.id = id
self.timestamp = timestamp
self.expirationTimestamp = expirationTimestamp
@ -70,7 +75,7 @@ public final class EngineStoryItem: Equatable {
self.isSelectedContacts = isSelectedContacts
self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited
self.hasLike = hasLike
self.myReaction = myReaction
}
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
@ -128,7 +133,7 @@ public final class EngineStoryItem: Equatable {
if lhs.isEdited != rhs.isEdited {
return false
}
if lhs.hasLike != rhs.hasLike {
if lhs.myReaction != rhs.myReaction {
return false
}
return true
@ -148,6 +153,7 @@ extension EngineStoryItem {
views: self.views.flatMap { views in
return Stories.Item.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeerIds: views.seenPeers.map(\.id)
)
},
@ -165,7 +171,7 @@ extension EngineStoryItem {
isSelectedContacts: self.isSelectedContacts,
isForwardingDisabled: self.isForwardingDisabled,
isEdited: self.isEdited,
hasLike: self.hasLike
myReaction: self.myReaction
)
}
}
@ -520,6 +526,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init)
}
@ -535,7 +542,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
items.append(mappedItem)
@ -646,6 +653,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init)
}
@ -661,7 +669,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
storyItems.append(mappedItem)
}
@ -796,6 +804,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init)
}
@ -811,7 +820,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
finalUpdatedState = updatedState
}
@ -837,6 +846,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init)
}
@ -852,7 +862,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
finalUpdatedState = updatedState
} else {
@ -880,6 +890,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init)
}
@ -895,7 +906,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
))
updatedState.items.sort(by: { lhs, rhs in
return lhs.timestamp > rhs.timestamp
@ -919,6 +930,7 @@ public final class PeerStoryListContext {
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init)
}
@ -934,7 +946,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
))
updatedState.items.sort(by: { lhs, rhs in
return lhs.timestamp > rhs.timestamp
@ -1082,6 +1094,7 @@ public final class PeerExpiringStoryListContext {
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init)
}
@ -1097,7 +1110,7 @@ public final class PeerExpiringStoryListContext {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
)
items.append(.item(mappedItem))
}

View File

@ -1035,7 +1035,7 @@ public extension TelegramEngine {
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
hasLike: item.hasLike
myReaction: item.myReaction
))
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
@ -1102,10 +1102,6 @@ public extension TelegramEngine {
return _internal_updateStoriesArePinned(account: self.account, ids: ids, isPinned: isPinned)
}
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit)
}
public func storyViewList(id: Int32, views: EngineStoryItem.Views) -> EngineStoryViewListContext {
return EngineStoryViewListContext(account: self.account, storyId: id, views: views)
}
@ -1118,8 +1114,8 @@ public extension TelegramEngine {
return _internal_enableStoryStealthMode(account: self.account)
}
public func setStoryLike(peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal<Never, NoError> {
return _internal_setStoryLike(account: self.account, peerId: peerId, id: id, hasLike: hasLike)
public func setStoryReaction(peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal<Never, NoError> {
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 storiesDualCameraTooltip = 44
case displayChatListArchiveTooltip = 45
case displayStoryReactionTooltip = 46
var key: ValueBoxKey {
let v = ValueBoxKey(length: 4)
@ -414,6 +415,10 @@ private struct ApplicationSpecificNoticeKeys {
static func displayChatListArchiveTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayChatListArchiveTooltip.key)
}
static func displayStoryReactionTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayStoryReactionTooltip.key)
}
}
public struct ApplicationSpecificNotice {
@ -1546,6 +1551,27 @@ public struct ApplicationSpecificNotice {
|> take(1)
}
public static func setDisplayStoryReactionTooltip(accountManager: AccountManager<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> {
return accountManager.transaction { transaction -> Void in
if let entry = CodableEntry(ApplicationSpecificBoolNotice()) {

View File

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

View File

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

View File

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

View File

@ -9,6 +9,8 @@ import TelegramPresentationData
import ChatPresentationInterfaceState
import MoreHeaderButton
import ContextUI
import ReactionButtonListComponent
import TelegramCore
private extension MessageInputActionButtonComponent.Mode {
var iconName: String? {
@ -19,12 +21,8 @@ private extension MessageInputActionButtonComponent.Mode {
return "Chat/Input/Text/IconAttachment"
case .forward:
return "Chat/Input/Text/IconForwardSend"
case let .like(isActive):
if isActive {
return "Stories/InputLikeOn"
} else {
return "Stories/InputLikeOff"
}
case .like:
return "Stories/InputLikeOff"
default:
return nil
}
@ -43,7 +41,7 @@ public final class MessageInputActionButtonComponent: Component {
case attach
case forward
case more
case like(isActive: Bool)
case like(reaction: MessageReaction.Reaction?, file: TelegramMediaFile?, animationFileId: Int64?)
}
public enum Action {
@ -127,12 +125,18 @@ public final class MessageInputActionButtonComponent: Component {
public let referenceNode: ContextReferenceContentNode
public let containerNode: ContextControllerSourceNode
private let sendIconView: UIImageView
private var moreButton: MoreHeaderButton?
private var reactionIconView: ReactionIconView?
private var component: MessageInputActionButtonComponent?
private weak var componentState: EmptyComponentState?
private var acceptNextButtonPress: Bool = false
public var likeIconView: UIView? {
return self.reactionIconView
}
override init(frame: CGRect) {
self.sendIconView = UIImageView()
@ -157,6 +161,7 @@ public final class MessageInputActionButtonComponent: Component {
guard let self, let component = self.component, let longPressAction = component.longPressAction else {
return
}
self.acceptNextButtonPress = false
longPressAction(self, gesture)
}
@ -173,8 +178,6 @@ public final class MessageInputActionButtonComponent: Component {
self.button.addTarget(self, action: #selector(self.touchDown), forControlEvents: .touchDown)
self.button.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
// but.addTarget(self, action: #selector(self.touchDown), for: .touchDown)
// self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
@ -182,6 +185,8 @@ public final class MessageInputActionButtonComponent: Component {
}
@objc private func touchDown() {
self.acceptNextButtonPress = true
guard let component = self.component else {
return
}
@ -189,6 +194,10 @@ public final class MessageInputActionButtonComponent: Component {
}
@objc private func pressed() {
if !self.acceptNextButtonPress {
return
}
guard let component = self.component else {
return
}
@ -207,6 +216,11 @@ public final class MessageInputActionButtonComponent: Component {
let themeUpdated = previousComponent?.theme !== component.theme
var transition = transition
if transition.animation.isImmediate, let previousComponent, case .like = previousComponent.mode, case .like = component.mode, previousComponent.mode != component.mode {
transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
}
self.containerNode.isUserInteractionEnabled = component.longPressAction != nil
if self.micButton == nil {
@ -306,8 +320,14 @@ public final class MessageInputActionButtonComponent: Component {
switch component.mode {
case .none:
break
case .send, .apply, .attach, .delete, .forward, .like:
case .send, .apply, .attach, .delete, .forward:
sendAlpha = 1.0
case let .like(reaction, _, _):
if reaction != nil {
sendAlpha = 0.0
} else {
sendAlpha = 1.0
}
case .more:
moreAlpha = 1.0
case .videoInput, .voiceInput:
@ -318,10 +338,7 @@ public final class MessageInputActionButtonComponent: Component {
if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName {
if let iconName = component.mode.iconName {
var tintColor: UIColor = .white
if case .like(true) = component.mode {
tintColor = UIColor(rgb: 0xFF3B30)
}
let tintColor: UIColor = .white
self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: tintColor)
} else if case .apply = component.mode {
self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), contextGenerator: { size, context in
@ -379,6 +396,44 @@ public final class MessageInputActionButtonComponent: Component {
}
}
if case let .like(reactionValue, reactionFile, animationFileId) = component.mode, let reaction = reactionValue {
let reactionIconFrame = CGRect(origin: .zero, size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: 2.0, dy: 2.0)
let reactionIconView: ReactionIconView
if let current = self.reactionIconView {
reactionIconView = current
} else {
reactionIconView = ReactionIconView(frame: reactionIconFrame)
reactionIconView.isUserInteractionEnabled = false
self.reactionIconView = reactionIconView
self.addSubview(reactionIconView)
if previousComponent != nil {
reactionIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
reactionIconView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
}
}
transition.setFrame(view: reactionIconView, frame: reactionIconFrame)
reactionIconView.update(
size: reactionIconFrame.size,
context: component.context,
file: reactionFile,
fileId: animationFileId ?? reactionFile?.fileId.id ?? 0,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 1.0, alpha: 0.2),
animateIdle: false,
reaction: reaction,
transition: .immediate
)
} else if let reactionIconView = self.reactionIconView {
self.reactionIconView = nil
reactionIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak reactionIconView] _ in
reactionIconView?.removeFromSuperview()
})
reactionIconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
}
transition.setFrame(view: self.button.view, frame: CGRect(origin: .zero, size: availableSize))
transition.setFrame(view: self.containerNode.view, frame: CGRect(origin: .zero, size: availableSize))
transition.setFrame(view: self.referenceNode.view, frame: CGRect(origin: .zero, size: availableSize))

View File

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

View File

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

View File

@ -16,6 +16,8 @@ import AppBundle
import PeerPresenceStatusManager
import EmojiStatusComponent
import ContextUI
import EmojiTextAttachmentView
import TextFormat
private let avatarFont = avatarPlaceholderFont(size: 15.0)
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
@ -44,6 +46,39 @@ public final class PeerListItemComponent: Component {
case checks
}
public final class Reaction: Equatable {
public let reaction: MessageReaction.Reaction
public let file: TelegramMediaFile?
public let animationFileId: Int64?
public init(
reaction: MessageReaction.Reaction,
file: TelegramMediaFile?,
animationFileId: Int64?
) {
self.reaction = reaction
self.file = file
self.animationFileId = animationFileId
}
public static func ==(lhs: Reaction, rhs: Reaction) -> Bool {
if lhs === rhs {
return true
}
if lhs.reaction != rhs.reaction {
return false
}
if lhs.file?.fileId != rhs.file?.fileId {
return false
}
if lhs.animationFileId != rhs.animationFileId {
return false
}
return true
}
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
@ -55,7 +90,7 @@ public final class PeerListItemComponent: Component {
let subtitle: String?
let subtitleAccessory: SubtitleAccessory
let presence: EnginePeer.Presence?
let displayLike: Bool
let reaction: Reaction?
let selectionState: SelectionState
let hasNext: Bool
let action: (EnginePeer) -> Void
@ -74,7 +109,7 @@ public final class PeerListItemComponent: Component {
subtitle: String?,
subtitleAccessory: SubtitleAccessory,
presence: EnginePeer.Presence?,
displayLike: Bool = false,
reaction: Reaction? = nil,
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (EnginePeer) -> Void,
@ -92,7 +127,7 @@ public final class PeerListItemComponent: Component {
self.subtitle = subtitle
self.subtitleAccessory = subtitleAccessory
self.presence = presence
self.displayLike = displayLike
self.reaction = reaction
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
@ -134,7 +169,7 @@ public final class PeerListItemComponent: Component {
if lhs.presence != rhs.presence {
return false
}
if lhs.displayLike != rhs.displayLike {
if lhs.reaction != rhs.reaction {
return false
}
if lhs.selectionState != rhs.selectionState {
@ -160,7 +195,10 @@ public final class PeerListItemComponent: Component {
private var iconView: UIImageView?
private var checkLayer: CheckLayer?
private var likeIconView: UIImageView?
private var reactionLayer: InlineStickerItemLayer?
private var iconFrame: CGRect?
private var file: TelegramMediaFile?
private var fileDisposable: Disposable?
private var component: PeerListItemComponent?
private weak var state: EmptyComponentState?
@ -251,6 +289,10 @@ public final class PeerListItemComponent: Component {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fileDisposable?.dispose()
}
@objc private func pressed() {
guard let component = self.component, let peer = component.peer else {
return
@ -265,7 +307,49 @@ public final class PeerListItemComponent: Component {
component.openStories?(peer, self.avatarNode)
}
private func updateReactionLayer() {
guard let component = self.component else {
return
}
if let reactionLayer = self.reactionLayer {
self.reactionLayer = nil
reactionLayer.removeFromSuperlayer()
}
guard let file = self.file else {
return
}
let reactionLayer = InlineStickerItemLayer(
context: component.context,
userLocation: .other,
attemptSynchronousLoad: false,
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file),
file: file,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.0, alpha: 0.1),
pointSize: CGSize(width: 64.0, height: 64.0)
)
self.reactionLayer = reactionLayer
if let reaction = component.reaction, case .custom = reaction.reaction {
reactionLayer.isVisibleForAnimations = true
}
self.layer.addSublayer(reactionLayer)
if var iconFrame = self.iconFrame {
if let reaction = component.reaction, case .builtin = reaction.reaction {
iconFrame = iconFrame.insetBy(dx: -iconFrame.width * 0.5, dy: -iconFrame.height * 0.5)
}
reactionLayer.frame = iconFrame
}
}
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousComponent = self.component
var synchronousLoad = false
if let hint = transition.userData(TransitionHint.self) {
synchronousLoad = hint.synchronousLoad
@ -351,7 +435,7 @@ public final class PeerListItemComponent: Component {
leftInset += 9.0
}
var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
if component.displayLike {
if component.reaction != nil {
rightInset += 32.0
}
@ -581,25 +665,29 @@ public final class PeerListItemComponent: Component {
transition.setFrame(view: labelView, frame: labelFrame)
}
if component.displayLike {
let likeIconView: UIImageView
if let current = self.likeIconView {
likeIconView = current
let imageSize = CGSize(width: 22.0, height: 22.0)
self.iconFrame = CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 14.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize)
if previousComponent?.reaction != component.reaction {
if let reaction = component.reaction {
switch reaction.reaction {
case .builtin:
self.file = reaction.file
self.updateReactionLayer()
case let .custom(fileId):
self.fileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|> deliverOnMainQueue).start(next: { [weak self] files in
guard let self, let file = files[fileId] else {
return
}
self.file = file
self.updateReactionLayer()
})
}
} else {
likeIconView = UIImageView()
self.likeIconView = likeIconView
self.containerButton.addSubview(likeIconView)
likeIconView.image = PresentationResourcesChat.storyViewListLikeIcon(component.theme)
self.file = nil
self.updateReactionLayer()
}
if let _ = likeIconView.image {
let imageSize = CGSize(width: 32.0, height: 32.0)
transition.setFrame(view: likeIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 11.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize))
}
} else if let likeIconView = self.likeIconView {
self.likeIconView = nil
likeIconView.removeFromSuperview()
}
if themeUpdated {

View File

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

View File

@ -20,6 +20,7 @@ import VolumeButtons
import TooltipUI
import ChatEntityKeyboardInputNode
import notify
import TelegramNotices
func hasFirstResponder(_ view: UIView) -> Bool {
if view.isFirstResponder {
@ -383,6 +384,8 @@ private final class StoryContainerScreenComponent: Component {
private var pendingNavigationToItemId: (peerId: EnginePeer.Id, id: Int32)?
private var didDisplayReactionTooltip: Bool = false
override init(frame: CGRect) {
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor.black.cgColor
@ -912,6 +915,30 @@ private final class StoryContainerScreenComponent: Component {
self?.layer.allowsGroupOpacity = false
})
}
Queue.mainQueue().after(0.4, { [weak self] in
guard let self, let component = self.component else {
return
}
let _ = (ApplicationSpecificNotice.displayStoryReactionTooltip(accountManager: component.context.sharedContext.accountManager)
|> delay(1.0, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let self else {
return
}
if !value {
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
currentItemView.maybeDisplayReactionTooltip()
}
}
self.didDisplayReactionTooltip = true
#if !DEBUG
let _ = ApplicationSpecificNotice.setDisplayStoryReactionTooltip(accountManager: component.context.sharedContext.accountManager).start()
#endif
})
})
}
func animateOut(completion: @escaping () -> Void) {
@ -1095,6 +1122,7 @@ private final class StoryContainerScreenComponent: Component {
}
}
})
update = true
}

View File

@ -421,6 +421,8 @@ public final class StoryItemSetContainerComponent: Component {
var reactionContextNode: ReactionContextNode?
weak var disappearingReactionContextNode: ReactionContextNode?
var displayLikeReactions: Bool = false
var waitingForReactionAnimateOutToLike: MessageReaction.Reaction?
weak var contextController: ContextController?
weak var privacyController: ShareWithPeersScreen?
@ -781,7 +783,11 @@ public final class StoryItemSetContainerComponent: Component {
if let _ = self.sendMessageContext.menuController {
return
}
if self.hasActiveDeactivateableInput() {
if self.displayLikeReactions {
self.displayLikeReactions = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
self.updateIsProgressPaused()
} else if self.hasActiveDeactivateableInput() {
Queue.mainQueue().justDispatch {
self.deactivateInput()
}
@ -947,10 +953,21 @@ public final class StoryItemSetContainerComponent: Component {
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if self.displayLikeReactions, let reactionContextNode = self.reactionContextNode {
if let result, result.isDescendant(of: reactionContextNode.view) {
return result
} else {
return self.itemsContainerView
}
}
if let inputView = self.inputPanel.view, let inputViewHitTest = inputView.hitTest(self.convert(point, to: inputView), with: event) {
return inputViewHitTest
}
guard let result = super.hitTest(point, with: event) else {
guard let result else {
return nil
}
@ -1065,6 +1082,9 @@ public final class StoryItemSetContainerComponent: Component {
if let captionItem = self.captionItem, captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText {
return .blurred
}
if self.displayLikeReactions {
return .blurred
}
return .play
}
@ -1817,6 +1837,67 @@ public final class StoryItemSetContainerComponent: Component {
})
}
func maybeDisplayReactionTooltip() {
if "".isEmpty {
return
}
guard let component = self.component else {
return
}
guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView else {
return
}
if inputPanelView.isHidden || inputPanelView.alpha == 0.0 {
return
}
if !likeButtonView.isDescendant(of: self) {
return
}
let rect = likeButtonView.convert(likeButtonView.bounds, to: nil)
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let text = "Long tap for more reactions"
let controller = TooltipController(content: .text(text), baseFontSize: presentationData.listsFontSize.baseDisplaySize, padding: 2.0)
controller.dismissed = { [weak self] _ in
if let self {
self.voiceMessagesRestrictedTooltipController = nil
self.updateIsProgressPaused()
}
}
component.presentController(controller, TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in
if let self {
return (self, rect)
}
return nil
}))
self.voiceMessagesRestrictedTooltipController = controller
self.updateIsProgressPaused()
//TODO:localize
/*let tooltipScreen = TooltipScreen(
account: component.context.account,
sharedContext: component.context.sharedContext,
text: .markdown(text: "Long tap for more reactions"),
balancedTextLayout: true,
style: .default,
location: TooltipScreen.Location.point(likeButtonView.convert(likeButtonView.bounds, to: nil).offsetBy(dx: 0.0, dy: 0.0), .bottom), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: true)
}
)
tooltipScreen.willBecomeDismissed = { [weak self] _ in
guard let self else {
return
}
self.sendMessageContext.tooltipScreen = nil
self.updateIsProgressPaused()
}
self.sendMessageContext.tooltipScreen?.dismiss()
self.sendMessageContext.tooltipScreen = tooltipScreen
self.updateIsProgressPaused()
component.controller()?.present(tooltipScreen, in: .current)*/
}
func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let isFirstTime = self.component == nil
@ -2042,13 +2123,44 @@ public final class StoryItemSetContainerComponent: Component {
}
self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default)
},
hasLike: component.slice.item.storyItem.hasLike,
myReaction: component.slice.item.storyItem.myReaction.flatMap { value -> MessageInputPanelComponent.MyReaction? in
var centerAnimation: TelegramMediaFile?
var animationFileId: Int64?
switch value {
case .builtin:
if let availableReactions = component.availableReactions {
for availableReaction in availableReactions.reactionItems {
if availableReaction.reaction.rawValue == value {
centerAnimation = availableReaction.listAnimation
break
}
}
}
case let .custom(fileId):
animationFileId = fileId
}
if animationFileId == nil && centerAnimation == nil {
return nil
}
return MessageInputPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId)
},
likeAction: component.slice.peer.isService ? nil : { [weak self] in
guard let self else {
return
}
self.performLikeAction()
},
likeOptionsAction: component.slice.peer.isService ? nil : { [weak self] sourceView, gesture in
gesture?.cancel()
guard let self else {
return
}
self.performLikeOptionsAction(sourceView: sourceView)
},
inputModeAction: { [weak self] in
guard let self else {
return
@ -2320,6 +2432,7 @@ public final class StoryItemSetContainerComponent: Component {
minimizedContentHeight: 325.0,
outerExpansionFraction: outerExpansionFraction,
outerExpansionDirection: outerExpansionDirection,
availableReactions: component.availableReactions,
close: { [weak self] in
guard let self else {
return
@ -3224,10 +3337,22 @@ public final class StoryItemSetContainerComponent: Component {
}
}
let reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0)
let reactionsAnchorRect: CGRect
if self.displayLikeReactions, let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView {
var likeRect = likeButtonView.convert(likeButtonView.bounds, to: self)
likeRect.origin.y -= 15.0
likeRect.size.height += 15.0
likeRect.origin.x -= 30.0
reactionsAnchorRect = likeRect
} else {
reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0)
}
var effectiveDisplayReactions = false
if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText {
if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText {
effectiveDisplayReactions = true
}
if self.displayLikeReactions {
effectiveDisplayReactions = true
}
if self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil {
@ -3262,7 +3387,7 @@ public final class StoryItemSetContainerComponent: Component {
animationCache: component.context.animationCache,
presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme),
items: reactionItems.map(ReactionContextItem.reaction),
selectedItems: Set(),
selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(),
getEmojiContent: { [weak self] animationCache, animationRenderer in
guard let self, let component = self.component else {
preconditionFailure()
@ -3308,34 +3433,30 @@ public final class StoryItemSetContainerComponent: Component {
self.state?.updated(transition: Transition(transition))
}
)
reactionContextNode.displayTail = false
reactionContextNode.displayTail = self.displayLikeReactions
reactionContextNode.forceTailToRight = self.displayLikeReactions
self.reactionContextNode = reactionContextNode
reactionContextNode.reactionSelected = { [weak self, weak reactionContextNode] updateReaction, _ in
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
guard let self, let component = self.component else {
return
}
let _ = (component.context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] availableReactions in
guard let self, let component = self.component, let availableReactions else {
return
if self.displayLikeReactions {
if component.slice.item.storyItem.myReaction == updateReaction.reaction {
let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).start()
self.displayLikeReactions = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
} else {
self.waitingForReactionAnimateOutToLike = updateReaction.reaction
let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start()
}
var animation: TelegramMediaFile?
for reaction in availableReactions.reactions {
if reaction.value == updateReaction.reaction {
animation = reaction.centerAnimation
break
}
}
} else {
let targetView = UIView(frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - 100.0) * 0.5), y: floor((self.bounds.height - 100.0) * 0.5)), size: CGSize(width: 100.0, height: 100.0)))
targetView.isUserInteractionEnabled = false
self.addSubview(targetView)
if let reactionContextNode {
if let reactionContextNode = self.reactionContextNode {
reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction)
reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in
guard let self else {
@ -3359,74 +3480,9 @@ public final class StoryItemSetContainerComponent: Component {
self.endEditing(true)
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
var text = ""
var messageAttributes: [MessageAttribute] = []
var inlineStickers: [MediaId : Media] = [:]
switch updateReaction {
case let .builtin(textValue):
text = textValue
case let .custom(fileId, file):
if let file {
animation = file
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, _):
text = displayText
let length = (text as NSString).length
messageAttributes = [TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< length, type: .CustomEmoji(stickerPack: nil, fileId: fileId))])]
inlineStickers = [file.fileId: file]
break loop
default:
break
}
}
}
}
let message: EnqueueMessage = .message(
text: text,
attributes: messageAttributes,
inlineStickers: inlineStickers,
mediaReference: nil,
replyToMessageId: nil,
replyToStoryId: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id),
localGroupingKey: nil,
correlationId: nil,
bubbleUpEmojiOrStickersets: []
)
let context = component.context
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let presentController = component.presentController
let peer = component.slice.peer
let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: [message])
|> deliverOnMainQueue).start(next: { [weak self] messageIds in
if let animation, let self, let component = self.component {
let controller = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: animation, loop: false, title: nil, text: component.strings.Story_ToastReactionSent, undoText: component.strings.Story_ToastViewInChat, customAction: { [weak self] in
if let messageId = messageIds.first, let self {
self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) })
}
}),
elevatedLayout: false,
animateInAsReplacement: false,
blurred: true,
action: { [weak self] _ in
self?.sendMessageContext.tooltipScreen = nil
self?.updateIsProgressPaused()
return false
}
)
self.sendMessageContext.tooltipScreen?.dismiss()
self.sendMessageContext.tooltipScreen = controller
self.updateIsProgressPaused()
presentController(controller, nil)
}
})
})
let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start()
}
}
reactionContextNode.premiumReactionsSelected = { [weak self] file in
@ -3487,12 +3543,41 @@ public final class StoryItemSetContainerComponent: Component {
}
} else {
reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition)
reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: !self.displayLikeReactions, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition)
if animateReactionsIn {
reactionContextNode.animateIn(from: reactionsAnchorRect)
}
}
if let waitingReaction = self.waitingForReactionAnimateOutToLike, component.slice.item.storyItem.myReaction == waitingReaction {
self.waitingForReactionAnimateOutToLike = nil
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeIconView {
reactionContextNode.willAnimateOutToReaction(value: waitingReaction)
reactionContextNode.animateOutToReaction(value: waitingReaction, targetView: likeButtonView, hideNode: true, animateTargetContainer: nil, addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in
guard let self else {
return
}
standaloneReactionAnimation.frame = self.bounds
self.addSubview(standaloneReactionAnimation.view)
}, completion: { [weak reactionContextNode] in
if let reactionContextNode {
reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in
reactionContextNode?.view.removeFromSuperview()
})
}
})
}
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.displayLikeReactions = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
}
}
} else {
if let reactionContextNode = self.reactionContextNode {
if let disappearingReactionContextNode = self.disappearingReactionContextNode {
@ -4215,9 +4300,9 @@ public final class StoryItemSetContainerComponent: Component {
return
}
let _ = component.context.engine.messages.setStoryLike(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, hasLike: !component.slice.item.storyItem.hasLike).start()
let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: component.slice.item.storyItem.myReaction == nil ? .builtin("") : nil).start()
if component.slice.item.storyItem.hasLike {
if component.slice.item.storyItem.myReaction != nil {
return
}
@ -4263,6 +4348,12 @@ public final class StoryItemSetContainerComponent: Component {
)
}
private func performLikeOptionsAction(sourceView: UIView) {
self.displayLikeReactions = true
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
self.updateIsProgressPaused()
}
func dismissAllTooltips() {
guard let component = self.component, let controller = component.controller() else {
return

View File

@ -67,6 +67,7 @@ final class StoryItemSetViewListComponent: Component {
let minimizedContentHeight: CGFloat
let outerExpansionFraction: CGFloat
let outerExpansionDirection: Bool
let availableReactions: StoryAvailableReactions?
let close: () -> Void
let expandViewStats: () -> Void
let deleteAction: () -> Void
@ -88,6 +89,7 @@ final class StoryItemSetViewListComponent: Component {
minimizedContentHeight: CGFloat,
outerExpansionFraction: CGFloat,
outerExpansionDirection: Bool,
availableReactions: StoryAvailableReactions?,
close: @escaping () -> Void,
expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void,
@ -108,6 +110,7 @@ final class StoryItemSetViewListComponent: Component {
self.minimizedContentHeight = minimizedContentHeight
self.outerExpansionFraction = outerExpansionFraction
self.outerExpansionDirection = outerExpansionDirection
self.availableReactions = availableReactions
self.close = close
self.expandViewStats = expandViewStats
self.deleteAction = deleteAction
@ -143,6 +146,9 @@ final class StoryItemSetViewListComponent: Component {
if lhs.outerExpansionDirection != rhs.outerExpansionDirection {
return false
}
if lhs.availableReactions !== rhs.availableReactions {
return false
}
return true
}
@ -517,7 +523,29 @@ final class StoryItemSetViewListComponent: Component {
subtitle: dateText,
subtitleAccessory: .checks,
presence: nil,
displayLike: item.isLike,
reaction: item.reaction.flatMap { reaction -> PeerListItemComponent.Reaction in
var animationFileId: Int64?
var animationFile: TelegramMediaFile?
switch reaction {
case .builtin:
if let availableReactions = component.availableReactions {
for availableReaction in availableReactions.reactionItems {
if availableReaction.reaction.rawValue == reaction {
animationFile = availableReaction.listAnimation
break
}
}
}
case let .custom(fileId):
animationFileId = fileId
animationFile = item.reactionFile
}
return PeerListItemComponent.Reaction(
reaction: reaction,
file: animationFile,
animationFileId: animationFileId
)
},
selectionState: .none,
hasNext: index != viewListState.totalCount - 1,
action: { [weak self] peer in
@ -670,7 +698,7 @@ final class StoryItemSetViewListComponent: Component {
applyState = true
let _ = synchronous
} else {
self.viewListState = EngineStoryViewListContext.State(totalCount: 0, items: [], loadMoreToken: nil)
self.viewListState = EngineStoryViewListContext.State(totalCount: 0, totalReactedCount: 0, items: [], loadMoreToken: nil)
}
}
@ -728,7 +756,7 @@ final class StoryItemSetViewListComponent: Component {
var externalViews: EngineStoryItem.Views? = component.storyItem.views
if let viewListState = self.viewListState, !viewListState.items.isEmpty {
externalViews = EngineStoryItem.Views(seenCount: viewListState.totalCount, seenPeers: viewListState.items.prefix(3).map(\.peer))
externalViews = EngineStoryItem.Views(seenCount: viewListState.totalCount, reactedCount: viewListState.totalReactedCount, seenPeers: viewListState.items.prefix(3).map(\.peer))
}
let navigationPanelSize = self.navigationPanel.update(

View File

@ -76,6 +76,9 @@ public final class StoryFooterPanelComponent: Component {
private let viewStatsExpandedText: AnimatedCountLabelView
private let deleteButton = ComponentView<Empty>()
private var reactionStatsIcon: UIImageView?
private var reactionStatsText: AnimatedCountLabelView?
private var statusButton: HighlightableButton?
private var statusNode: SemanticStatusNode?
private var uploadingText: ComponentView<Empty>?
@ -114,9 +117,13 @@ public final class StoryFooterPanelComponent: Component {
if highlighted {
self.avatarsView.alpha = 0.7
self.viewStatsText.alpha = 0.7
self.reactionStatsIcon?.alpha = 0.7
self.reactionStatsText?.alpha = 0.7
} else {
self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.viewStatsText.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.reactionStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.reactionStatsText?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
}
}
self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside)
@ -278,8 +285,10 @@ public final class StoryFooterPanelComponent: Component {
}
var viewCount = 0
var reactionCount = 0
if let views = component.externalViews ?? component.storyItem?.views, views.seenCount != 0 {
viewCount = views.seenCount
reactionCount = views.reactedCount
}
let viewsText: String
@ -353,7 +362,65 @@ public final class StoryFooterPanelComponent: Component {
transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width)
}
transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0)))
var statsButtonWidth = viewStatsTextFrame.maxY + 8.0
if reactionCount != 0 {
var reactionsTransition = transition
let reactionStatsIcon: UIImageView
if let current = self.reactionStatsIcon {
reactionStatsIcon = current
} else {
reactionsTransition = reactionsTransition.withAnimation(.none)
reactionStatsIcon = UIImageView()
reactionStatsIcon.image = UIImage(bundleImageName: "Stories/InputLikeOn")?.withRenderingMode(.alwaysTemplate)
reactionStatsIcon.tintColor = UIColor(rgb: 0xFF3B30)
self.reactionStatsIcon = reactionStatsIcon
self.externalContainerView.addSubview(reactionStatsIcon)
}
let reactionStatsText: AnimatedCountLabelView
if let current = self.reactionStatsText {
reactionStatsText = current
} else {
reactionStatsText = AnimatedCountLabelView(frame: CGRect())
reactionStatsText.isUserInteractionEnabled = false
self.reactionStatsText = reactionStatsText
self.externalContainerView.addSubview(reactionStatsText)
}
let reactionStatsLayout = reactionStatsText.update(
size: CGSize(width: availableSize.width, height: size.height),
segments: [
.number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.regular(15.0), textColor: .white))
],
transition: (isFirstTime || reactionsTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
)
let imageSize = CGSize(width: 23.0, height: 23.0)
reactionsTransition.setFrame(view: reactionStatsIcon, frame: CGRect(origin: CGPoint(x: viewStatsTextFrame.maxX + 7.0, y: viewStatsTextFrame.minY - 3.0), size: imageSize))
let reactionStatsFrame = CGRect(origin: CGPoint(x: viewStatsTextFrame.maxX + 7.0 + imageSize.width + 3.0, y: viewStatsTextFrame.minY), size: reactionStatsLayout.size)
reactionsTransition.setFrame(view: reactionStatsText, frame: reactionStatsFrame)
statsButtonWidth = reactionStatsFrame.maxX + 8.0
} else {
if let reactionStatsIcon = self.reactionStatsIcon {
self.reactionStatsIcon = nil
reactionStatsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsIcon] _ in
reactionStatsIcon?.removeFromSuperview()
})
}
if let reactionStatsText = self.reactionStatsText {
self.reactionStatsText = nil
reactionStatsText.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsText] _ in
reactionStatsText?.removeFromSuperview()
})
}
}
transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: statsButtonWidth, height: baseHeight)))
self.viewStatsButton.isUserInteractionEnabled = component.expandFraction == 0.0
var rightContentOffset: CGFloat = availableSize.width - 12.0