Unread-based animation playback

This commit is contained in:
Ali 2022-06-05 19:24:41 +04:00
parent 9a2394ce1a
commit 6b1a7d1aae
9 changed files with 167 additions and 105 deletions

View File

@ -375,6 +375,11 @@ public final class AnimatedStickerNode: ASDisplayNode {
} }
private var isSetUpForPlayback = false private var isSetUpForPlayback = false
public func playOnce() {
self.playbackMode = .once
self.play()
}
public func play(firstFrame: Bool = false, fromIndex: Int? = nil) { public func play(firstFrame: Bool = false, fromIndex: Int? = nil) {
if !firstFrame { if !firstFrame {

View File

@ -936,6 +936,7 @@ public final class MessageHistoryView {
public let entries: [MessageHistoryEntry] public let entries: [MessageHistoryEntry]
public let maxReadIndex: MessageIndex? public let maxReadIndex: MessageIndex?
public let fixedReadStates: MessageHistoryViewReadState? public let fixedReadStates: MessageHistoryViewReadState?
public let transientReadStates: MessageHistoryViewReadState?
public let topTaggedMessages: [Message] public let topTaggedMessages: [Message]
public let additionalData: [AdditionalMessageHistoryViewDataEntry] public let additionalData: [AdditionalMessageHistoryViewDataEntry]
public let isLoading: Bool public let isLoading: Bool
@ -952,6 +953,7 @@ public final class MessageHistoryView {
self.entries = entries self.entries = entries
self.maxReadIndex = nil self.maxReadIndex = nil
self.fixedReadStates = nil self.fixedReadStates = nil
self.transientReadStates = nil
self.topTaggedMessages = [] self.topTaggedMessages = []
self.additionalData = [] self.additionalData = []
self.isLoading = isLoading self.isLoading = isLoading
@ -1053,6 +1055,7 @@ public final class MessageHistoryView {
self.additionalData = mutableView.additionalDatas self.additionalData = mutableView.additionalDatas
self.fixedReadStates = mutableView.combinedReadStates self.fixedReadStates = mutableView.combinedReadStates
self.transientReadStates = mutableView.transientReadStates
switch mutableView.peerIds { switch mutableView.peerIds {
case .single, .associated: case .single, .associated:

View File

@ -52,6 +52,11 @@ public enum ChatControllerInteractionReaction {
case reaction(String) case reaction(String)
} }
struct UnreadMessageRangeKey: Hashable {
var peerId: PeerId
var namespace: MessageId.Namespace
}
public final class ChatControllerInteraction { public final class ChatControllerInteraction {
let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, MessageReference?, Peer?) -> Void let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, MessageReference?, Peer?) -> Void
@ -149,6 +154,7 @@ public final class ChatControllerInteraction {
var currentPsaMessageWithTooltip: MessageId? var currentPsaMessageWithTooltip: MessageId?
var stickerSettings: ChatInterfaceStickerSettings var stickerSettings: ChatInterfaceStickerSettings
var searchTextHighightState: (String, [MessageIndex])? var searchTextHighightState: (String, [MessageIndex])?
var unreadMessageRange: [UnreadMessageRangeKey: Range<MessageId.Id>] = [:]
var seenOneTimeAnimatedMedia = Set<MessageId>() var seenOneTimeAnimatedMedia = Set<MessageId>()
var currentMessageWithLoadingReplyThread: MessageId? var currentMessageWithLoadingReplyThread: MessageId?
var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?

View File

@ -2435,7 +2435,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
for text in breakChatInputText(trimChatInputText(inputText)) { for text in breakChatInputText(trimChatInputText(inputText)) {
if text.length != 0 { if text.length != 0 {
var attributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = []
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: Int(self.context.userLimits.maxAnimatedEmojisInText))) let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0/*Int(self.context.userLimits.maxAnimatedEmojisInText)*/))
if !entities.isEmpty { if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities)) attributes.append(TextEntitiesMessageAttribute(entities: entities))
} }

View File

@ -1309,31 +1309,35 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let previousMaxIncomingMessageIndexByNamespace = Atomic<[MessageId.Namespace: MessageIndex]>(value: [:]) let previousMaxIncomingMessageIndexByNamespace = Atomic<[MessageId.Namespace: MessageIndex]>(value: [:])
let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.canReadHistory.get()) let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.canReadHistory.get())
|> map { messageIndex, canRead in
if canRead { self.readHistoryDisposable.set((readHistory |> deliverOnMainQueue).start(next: { [weak self] messageIndex, canRead in
var apply = false guard let strongSelf = self else {
let _ = previousMaxIncomingMessageIndexByNamespace.modify { dict in return
let previousIndex = dict[messageIndex.id.namespace] }
if previousIndex == nil || previousIndex! < messageIndex { if !canRead {
apply = true return
var dict = dict }
dict[messageIndex.id.namespace] = messageIndex
return dict var apply = false
} let _ = previousMaxIncomingMessageIndexByNamespace.modify { dict in
let previousIndex = dict[messageIndex.id.namespace]
if previousIndex == nil || previousIndex! < messageIndex {
apply = true
var dict = dict
dict[messageIndex.id.namespace] = messageIndex
return dict return dict
} }
if apply { return dict
switch chatLocation { }
case .peer, .replyThread, .feed: if apply {
if !context.sharedContext.immediateExperimentalUISettings.skipReadHistory { switch chatLocation {
context.applyMaxReadIndex(for: chatLocation, contextHolder: chatLocationContextHolder, messageIndex: messageIndex) case .peer, .replyThread, .feed:
} if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
strongSelf.context.applyMaxReadIndex(for: chatLocation, contextHolder: chatLocationContextHolder, messageIndex: messageIndex)
} }
} }
} }
} }))
self.readHistoryDisposable.set(readHistory.start())
self.canReadHistoryDisposable = (self.canReadHistory.get() |> deliverOnMainQueue).start(next: { [weak self, weak context] value in self.canReadHistoryDisposable = (self.canReadHistory.get() |> deliverOnMainQueue).start(next: { [weak self, weak context] value in
if let strongSelf = self { if let strongSelf = self {
@ -2157,6 +2161,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
return result return result
} }
public func forEachVisibleMessageItemNode(_ f: (ChatMessageItemView) -> Void) {
self.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
f(itemNode)
}
}
}
public func latestMessageInCurrentHistoryView() -> Message? { public func latestMessageInCurrentHistoryView() -> Message? {
if let historyView = self.historyView { if let historyView = self.historyView {
if historyView.originalView.laterId == nil, let firstEntry = historyView.filteredEntries.last { if historyView.originalView.laterId == nil, let firstEntry = historyView.filteredEntries.last {
@ -2369,6 +2381,42 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
} }
var unreadMessageRangeUpdated = false
if case let .peer(peerId) = strongSelf.chatLocation, let previousReadStatesValue = strongSelf.historyView?.originalView.transientReadStates, case let .peer(previousReadStates) = previousReadStatesValue, case let .peer(updatedReadStates) = transition.historyView.originalView.transientReadStates {
if let previousPeerReadState = previousReadStates[peerId], let updatedPeerReadState = updatedReadStates[peerId] {
if previousPeerReadState != updatedPeerReadState {
for (namespace, state) in previousPeerReadState.states {
inner: for (updatedNamespace, updatedState) in updatedPeerReadState.states {
if namespace == updatedNamespace {
switch state {
case let .idBased(previousIncomingId, _, _, _, _):
if case let .idBased(updatedIncomingId, _, _, _, _) = updatedState, previousIncomingId <= updatedIncomingId {
let rangeKey = UnreadMessageRangeKey(peerId: peerId, namespace: namespace)
if let currentRange = strongSelf.controllerInteraction.unreadMessageRange[rangeKey] {
if currentRange.upperBound < (updatedIncomingId + 1) {
strongSelf.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: peerId, namespace: namespace)] = currentRange.lowerBound ..< (updatedIncomingId + 1)
unreadMessageRangeUpdated = true
}
} else {
strongSelf.controllerInteraction.unreadMessageRange[rangeKey] = (previousIncomingId + 1) ..< (updatedIncomingId + 1)
unreadMessageRangeUpdated = true
}
}
case .indexBased:
break
}
break inner
}
}
}
//print("Read from \(previousPeerReadState) up to \(updatedPeerReadState)")
}
}
}
strongSelf.historyView = transition.historyView strongSelf.historyView = transition.historyView
let loadState: ChatHistoryNodeLoadState let loadState: ChatHistoryNodeLoadState
@ -2541,6 +2589,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
} }
if unreadMessageRangeUpdated {
strongSelf.forEachVisibleMessageItemNode { itemNode in
itemNode.unreadMessageRangeUpdated()
}
}
strongSelf.hasActiveTransition = false strongSelf.hasActiveTransition = false
strongSelf.dequeueHistoryViewTransitions() strongSelf.dequeueHistoryViewTransitions()
} }

View File

@ -218,6 +218,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var currentSwipeAction: ChatControllerInteractionSwipeAction? private var currentSwipeAction: ChatControllerInteractionSwipeAction?
private var wasPending: Bool = false
private var didChangeFromPendingToSent: Bool = false
required init() { required init() {
self.contextSourceNode = ContextExtractedContentContainingNode() self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
@ -470,6 +473,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) {
super.setupItem(item, synchronousLoad: synchronousLoad) super.setupItem(item, synchronousLoad: synchronousLoad)
if item.message.id.namespace == Namespaces.Message.Local {
self.wasPending = true
}
if self.wasPending && item.message.id.namespace != Namespaces.Message.Local {
self.didChangeFromPendingToSent = true
}
for media in item.message.media { for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile { if let telegramFile = media as? TelegramMediaFile {
@ -526,7 +536,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)), fitzModifier: fitzModifier, thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)), fitzModifier: fitzModifier, thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad)
self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start()) self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start())
} }
self.updateVisibility()
let textEmoji = item.message.text.strippedEmoji let textEmoji = item.message.text.strippedEmoji
var additionalTextEmoji = textEmoji var additionalTextEmoji = textEmoji
@ -551,6 +560,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
} }
} }
self.updateVisibility()
} }
private func updateVisibility() { private func updateVisibility() {
@ -558,6 +569,30 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return return
} }
var file: TelegramMediaFile?
var playbackMode: AnimatedStickerPlaybackMode = .loop
var isEmoji = false
var fitzModifier: EmojiFitzModifier?
if let telegramFile = self.telegramFile {
file = telegramFile
if !item.controllerInteraction.stickerSettings.loopAnimatedStickers {
playbackMode = .once
}
} else if let emojiFile = self.emojiFile {
isEmoji = true
file = emojiFile
//if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
playbackMode = .still(.end)
//} else {
// playbackMode = .once
//}
let (_, fitz) = item.message.text.basicEmoji
if let fitz = fitz {
fitzModifier = EmojiFitzModifier(emoji: fitz)
}
}
let isPlaying = self.visibilityStatus && !self.forceStopAnimations let isPlaying = self.visibilityStatus && !self.forceStopAnimations
if let animationNode = self.animationNode as? AnimatedStickerNode { if let animationNode = self.animationNode as? AnimatedStickerNode {
if !isPlaying { if !isPlaying {
@ -583,51 +618,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if self.isPlaying != isPlaying { if self.isPlaying != isPlaying {
self.isPlaying = isPlaying self.isPlaying = isPlaying
var alreadySeen = false
if isPlaying, let _ = self.emojiFile {
if item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = true
}
}
if isPlaying && self.setupTimestamp == nil { if isPlaying && self.setupTimestamp == nil {
self.setupTimestamp = CACurrentMediaTime() self.setupTimestamp = CACurrentMediaTime()
} }
animationNode.visibility = isPlaying animationNode.visibility = isPlaying
if self.didSetUpAnimationNode && alreadySeen { /*if self.didSetUpAnimationNode && alreadySeen {
if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource { if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource {
} else { } else {
animationNode.seekTo(.start) animationNode.seekTo(.start)
} }
} }*/
if self.isPlaying && !self.didSetUpAnimationNode { if self.isPlaying && !self.didSetUpAnimationNode {
self.didSetUpAnimationNode = true self.didSetUpAnimationNode = true
var file: TelegramMediaFile?
var playbackMode: AnimatedStickerPlaybackMode = .loop
var isEmoji = false
var fitzModifier: EmojiFitzModifier?
if let telegramFile = self.telegramFile {
file = telegramFile
if !item.controllerInteraction.stickerSettings.loopAnimatedStickers {
playbackMode = .once
}
} else if let emojiFile = self.emojiFile {
isEmoji = true
file = emojiFile
if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
playbackMode = .still(.end)
} else {
playbackMode = .once
}
let (_, fitz) = item.message.text.basicEmoji
if let fitz = fitz {
fitzModifier = EmojiFitzModifier(emoji: fitz)
}
}
if let file = file { if let file = file {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
@ -637,15 +641,39 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let mode: AnimatedStickerMode = .direct(cachePathPrefix: pathPrefix) let mode: AnimatedStickerMode = .direct(cachePathPrefix: pathPrefix)
self.animationSize = fittedSize self.animationSize = fittedSize
animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier, isVideo: file.isVideoSticker), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode) animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier, isVideo: file.isVideoSticker), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
}
if file.isPremiumSticker && !alreadySeen { }
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) }
Queue.mainQueue().after(0.1) { }
self.playPremiumStickerAnimation()
} if isPlaying, let animationNode = self.animationNode as? AnimatedStickerNode {
var alreadySeen = true
if item.message.flags.contains(.Incoming) {
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
if unreadRange.contains(item.message.id.id) {
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = false
} }
} }
} }
} else {
if self.didChangeFromPendingToSent {
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = false
}
}
}
if !alreadySeen {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
if let file = file, file.isPremiumSticker {
Queue.mainQueue().after(0.1) {
self.playPremiumStickerAnimation()
}
} else if isEmoji {
animationNode.seekTo(.start)
animationNode.playOnce()
}
} }
} }
} }
@ -655,7 +683,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.updateVisibility() self.updateVisibility()
} }
private var absoluteRect: (CGRect, CGSize)? private var absoluteRect: (CGRect, CGSize)?
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize) self.absoluteRect = (rect, containerSize)
@ -2378,6 +2405,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
return nil return nil
} }
override func unreadMessageRangeUpdated() {
self.updateVisibility()
}
} }
struct AnimatedEmojiSoundsConfiguration { struct AnimatedEmojiSoundsConfiguration {

View File

@ -898,4 +898,7 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol
} }
} }
} }
func unreadMessageRangeUpdated() {
}
} }

View File

@ -341,7 +341,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor) attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor)
} }
if let entities = entities { /*if let entities = entities {
let updatedString = NSMutableAttributedString(attributedString: attributedText) let updatedString = NSMutableAttributedString(attributedString: attributedText)
for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) {
@ -368,48 +368,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile) updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}]", attributes: updatedAttributes) let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}]", attributes: updatedAttributes)
//updatedString.insert(insertString, at: NSRange(substringRange, in: updatedString.string).upperBound)
updatedString.replaceCharacters(in: range, with: insertString) updatedString.replaceCharacters(in: range, with: insertString)
} }
/*var currentCount = 0
let updatedString = NSMutableAttributedString(attributedString: attributedText)
var startIndex = updatedString.string.startIndex
while true {
var hadUpdates = false
updatedString.string.enumerateSubstrings(in: startIndex ..< updatedString.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in
if let substring = substring {
let emoji = substring.basicEmoji.0
var emojiFile: TelegramMediaFile?
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
if emojiFile == nil {
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
}
if let emojiFile = emojiFile {
let currentDict = updatedString.attributes(at: NSRange(substringRange, in: updatedString.string).lowerBound, effectiveRange: nil)
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes)
//updatedString.insert(insertString, at: NSRange(substringRange, in: updatedString.string).upperBound)
updatedString.replaceCharacters(in: NSRange(substringRange, in: updatedString.string), with: insertString)
startIndex = substringRange.lowerBound
currentCount += 1
hadUpdates = true
stop = true
}
}
}
if !hadUpdates || currentCount >= 10 {
break
}
}*/
} }
attributedText = updatedString attributedText = updatedString
} }*/
let cutout: TextNodeCutout? = nil let cutout: TextNodeCutout? = nil

View File

@ -146,7 +146,7 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType,
public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil) -> [MessageTextEntity] { public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil) -> [MessageTextEntity] {
var entities: [MessageTextEntity] = [] var entities: [MessageTextEntity] = []
if let maxAnimatedEmojisInText = maxAnimatedEmojisInText { if let maxAnimatedEmojisInText = maxAnimatedEmojisInText, maxAnimatedEmojisInText != 0 {
var count = 0 var count = 0
text.string.enumerateSubstrings(in: text.string.startIndex ..< text.string.endIndex, options: [.byComposedCharacterSequences], { substring, substringRange, _, stop in text.string.enumerateSubstrings(in: text.string.startIndex ..< text.string.endIndex, options: [.byComposedCharacterSequences], { substring, substringRange, _, stop in
if let substring = substring { if let substring = substring {
@ -159,10 +159,7 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
count += 1 count += 1
if count >= maxAnimatedEmojisInText { if count >= maxAnimatedEmojisInText {
#if DEBUG
#else
stop = true stop = true
#endif
} }
} }
} }