mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-24 09:06:30 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
eb66f51388
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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: {},
|
||||
|
||||
@ -32,6 +32,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/StickerPeekUI",
|
||||
"//submodules/Components/ReactionButtonListComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user