diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index 564f1b1852..b6c431821b 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -63,10 +63,11 @@ public protocol UniversalVideoDecoration: AnyObject { } public enum UniversalVideoPriority: Int32, Comparable { - case secondaryOverlay = 0 - case embedded = 1 - case gallery = 2 - case overlay = 3 + case minimal = 0 + case secondaryOverlay = 1 + case embedded = 2 + case gallery = 3 + case overlay = 4 public static func <(lhs: UniversalVideoPriority, rhs: UniversalVideoPriority) -> Bool { return lhs.rawValue < rhs.rawValue diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 7c56109f2e..b5d9a7f70a 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -52,6 +52,7 @@ swift_library( "//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge", "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/GalleryData:GalleryData", + "//submodules/GalleryUI:GalleryUI", "//submodules/InstantPageUI:InstantPageUI", "//submodules/ListSectionHeaderNode:ListSectionHeaderNode", "//submodules/ChatInterfaceState:ChatInterfaceState", @@ -68,6 +69,8 @@ swift_library( "//submodules/Components/ProgressIndicatorComponent:ProgressIndicatorComponent", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/PremiumUI:PremiumUI", + "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", + "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 58b00e48d3..0794f311b0 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit +import Postbox import TelegramCore import TelegramPresentationData import ItemListUI @@ -19,6 +20,10 @@ import ContextUI import ChatInterfaceState import TextFormat import InvisibleInkDustNode +import TelegramUniversalVideoContent +import UniversalMediaPlayer +import GalleryUI +import HierarchyTrackingLayer public enum ChatListItemContent { public final class DraftState: Equatable { @@ -415,6 +420,8 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { } } +private let maxVideoLoopCount = 3 + class ChatListItemNode: ItemListRevealOptionsItemNode { var item: ChatListItem? @@ -424,6 +431,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let contextContainer: ContextControllerSourceNode let avatarNode: AvatarNode + var videoNode: UniversalVideoNode? + private var videoContent: NativeVideoContent? + private let playbackStartDisposable = MetaDisposable() + private var videoLoopCount = 0 + let titleNode: TextNode let authorNode: TextNode let measureNode: TextNode @@ -442,6 +454,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var credibilityIconNode: ASImageNode? let mutedIconNode: ASImageNode + private var hierarchyTrackingLayer: HierarchyTrackingLayer? + private var cachedDataDisposable = MetaDisposable() + private var currentTextLeftCutout: CGFloat = 0.0 private var currentMediaPreviewSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = [] private var mediaPreviewNodes: [EngineMedia.Id: ChatListMediaPreviewNode] = [:] @@ -586,6 +601,46 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + override var visibility: ListViewItemNodeVisibility { + didSet { + let wasVisible = self.visibilityStatus + let isVisible: Bool + switch self.visibility { + case let .visible(fraction): + isVisible = fraction > 0.2 + case .none: + isVisible = false + } + if wasVisible != isVisible { + self.visibilityStatus = isVisible + } + } + } + + private var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + if self.visibilityStatus { + self.videoLoopCount = 0 + } + self.updateVideoVisibility() + } + } + } + + private var trackingIsInHierarchy: Bool = false { + didSet { + if self.trackingIsInHierarchy != oldValue { + Queue.mainQueue().justDispatch { + if self.trackingIsInHierarchy { + self.videoLoopCount = 0 + } + self.updateVideoVisibility() + } + } + } + } + required init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -675,6 +730,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + deinit { + self.cachedDataDisposable.dispose() + self.playbackStartDisposable.dispose() + } + func setupItem(item: ChatListItem, synchronousLoads: Bool) { let previousItem = self.item self.item = item @@ -711,6 +771,65 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { overrideImage = .deletedIcon } self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0)) + + if peer.isPremium { + let context = item.context + self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + guard let strongSelf = self else { + return + } + let cachedPeerData = peerView.cachedData + if let cachedPeerData = cachedPeerData as? CachedUserData { + if let photo = cachedPeerData.photo, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) { + let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) + if videoContent.id != strongSelf.videoContent?.id { + strongSelf.videoNode?.removeFromSupernode() + strongSelf.videoContent = videoContent + } + + if strongSelf.hierarchyTrackingLayer == nil { + let hierarchyTrackingLayer = HierarchyTrackingLayer() + hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.trackingIsInHierarchy = true + } + + hierarchyTrackingLayer.didExitHierarchy = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.trackingIsInHierarchy = false + } + strongSelf.hierarchyTrackingLayer = hierarchyTrackingLayer + strongSelf.layer.addSublayer(hierarchyTrackingLayer) + } + } else { + strongSelf.videoContent = nil + + strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() + strongSelf.hierarchyTrackingLayer = nil + } + + strongSelf.updateVideoVisibility() + } else { + let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() + } + })) + } else { + self.cachedDataDisposable.set(nil) + self.videoContent = nil + + self.videoNode?.removeFromSupernode() + self.videoNode = nil + + self.hierarchyTrackingLayer?.removeFromSuperlayer() + self.hierarchyTrackingLayer = nil + } } self.contextContainer.isGestureEnabled = enablePreview && !item.editing @@ -1364,7 +1483,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if isSecret { currentSecretIconImage = PresentationResourcesChatList.secretIcon(item.presentationData.theme) } - var credibilityIconOffset: CGFloat = 0.0 if !isPeerGroup && item.index.messageIndex.id.peerId != item.context.account.peerId { if displayAsMessage { @@ -1373,16 +1491,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let peer = messages.last?.author { if peer.isScam { currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) - credibilityIconOffset = 2.0 } else if peer.isFake { currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) - credibilityIconOffset = 2.0 } else if peer.isVerified { currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) - credibilityIconOffset = 3.0 } else if peer.isPremium { currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) - credibilityIconOffset = 2.0 } } default: @@ -1391,16 +1505,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { if peer.isScam { currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) - credibilityIconOffset = 2.0 } else if peer.isFake { currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) - credibilityIconOffset = 2.0 } else if peer.isVerified { currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) - credibilityIconOffset = 3.0 } else if peer.isPremium { currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) - credibilityIconOffset = 2.0 } } } @@ -1654,6 +1764,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame) + strongSelf.updateVideoVisibility() let onlineFrame: CGRect if onlineIsVoiceChat { @@ -1743,41 +1854,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.secretIconNode = nil secretIconNode.removeFromSupernode() } - - var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.size.width + 3.0 + titleOffset - - if let currentCredibilityIconImage = currentCredibilityIconImage { - let iconNode: ASImageNode - if let current = strongSelf.credibilityIconNode { - iconNode = current - } else { - iconNode = ASImageNode() - iconNode.isLayerBacked = true - iconNode.displaysAsynchronously = false - iconNode.displayWithoutProcessing = true - strongSelf.contextContainer.addSubnode(iconNode) - strongSelf.credibilityIconNode = iconNode - } - iconNode.image = currentCredibilityIconImage - transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: contentRect.origin.y + credibilityIconOffset), size: currentCredibilityIconImage.size)) - nextTitleIconOrigin += currentCredibilityIconImage.size.width + 5.0 - } else if let credibilityIconNode = strongSelf.credibilityIconNode { - strongSelf.credibilityIconNode = nil - credibilityIconNode.removeFromSupernode() - } - - if let currentMutedIconImage = currentMutedIconImage { - strongSelf.mutedIconNode.image = currentMutedIconImage - strongSelf.mutedIconNode.isHidden = false - transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: contentRect.origin.y - 3.0 + UIScreenPixel), size: currentMutedIconImage.size)) - nextTitleIconOrigin += currentMutedIconImage.size.width + 1.0 - } else { - strongSelf.mutedIconNode.image = nil - strongSelf.mutedIconNode.isHidden = true - } - + let contentDelta = CGPoint(x: contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset), y: contentRect.origin.y - (strongSelf.titleNode.frame.minY - UIScreenPixel)) - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout.size) strongSelf.authorNode.frame = authorNodeFrame let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) @@ -1917,6 +1997,37 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.textNode.recursivelyEnsureDisplaySynchronously(true) } + var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.size.width + 3.0 + titleOffset + if let currentCredibilityIconImage = currentCredibilityIconImage { + let iconNode: ASImageNode + if let current = strongSelf.credibilityIconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displaysAsynchronously = false + iconNode.displayWithoutProcessing = true + strongSelf.contextContainer.addSubnode(iconNode) + strongSelf.credibilityIconNode = iconNode + } + iconNode.image = currentCredibilityIconImage + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: floorToScreenPixels(titleFrame.midY - currentCredibilityIconImage.size.height / 2.0) - UIScreenPixel), size: currentCredibilityIconImage.size)) + nextTitleIconOrigin += currentCredibilityIconImage.size.width + 4.0 + } else if let credibilityIconNode = strongSelf.credibilityIconNode { + strongSelf.credibilityIconNode = nil + credibilityIconNode.removeFromSupernode() + } + + if let currentMutedIconImage = currentMutedIconImage { + strongSelf.mutedIconNode.image = currentMutedIconImage + strongSelf.mutedIconNode.isHidden = false + transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: floorToScreenPixels(titleFrame.midY - currentMutedIconImage.size.height / 2.0) - UIScreenPixel), size: currentMutedIconImage.size)) + nextTitleIconOrigin += currentMutedIconImage.size.width + 1.0 + } else { + strongSelf.mutedIconNode.image = nil + strongSelf.mutedIconNode.isHidden = true + } + let separatorInset: CGFloat if case let .groupReference(_, _, _, _, hiddenByDefault) = item.content, hiddenByDefault { separatorInset = 0.0 @@ -1998,6 +2109,80 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + private func updateVideoVisibility() { + guard let item = self.item else { + return + } + + let isVisible = self.visibilityStatus && self.trackingIsInHierarchy + if isVisible, let videoContent = self.videoContent, self.videoLoopCount != maxVideoLoopCount { + if self.videoNode == nil { + let context = item.context + let mediaManager = context.sharedContext.mediaManager + let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) + videoNode.clipsToBounds = true + videoNode.isUserInteractionEnabled = false + videoNode.isHidden = true + videoNode.playbackCompleted = { [weak self] in + if let strongSelf = self { + strongSelf.videoLoopCount += 1 + if strongSelf.videoLoopCount == maxVideoLoopCount { + if let videoNode = strongSelf.videoNode { + strongSelf.videoNode = nil + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.removeFromSupernode() + }) + } + } + } + } + + if let _ = videoContent.startTimestamp { + self.playbackStartDisposable.set((videoNode.status + |> map { status -> Bool in + if let status = status, case .playing = status.status { + return true + } else { + return false + } + } + |> filter { playing in + return playing + } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + if let strongSelf = self { + Queue.mainQueue().after(0.15) { + strongSelf.videoNode?.isHidden = false + } + } + })) + } else { + self.playbackStartDisposable.set(nil) + videoNode.isHidden = false + } + videoNode.layer.cornerRadius = self.avatarNode.frame.size.width / 2.0 + if #available(iOS 13.0, *) { + videoNode.layer.cornerCurve = .circular + } + + videoNode.canAttachContent = true + videoNode.play() + + self.contextContainer.insertSubnode(videoNode, aboveSubnode: self.avatarNode) + self.videoNode = videoNode + } + } else if let videoNode = self.videoNode { + self.videoNode = nil + videoNode.removeFromSupernode() + } + + if let videoNode = self.videoNode { + videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) + videoNode.frame = self.avatarNode.frame + } + } + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) @@ -2032,6 +2217,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var avatarFrame = self.avatarNode.frame avatarFrame.origin.x = leftInset - avatarLeftInset + editingOffset + 10.0 + offset transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + if let videoNode = self.videoNode { + transition.updateFrame(node: videoNode, frame: avatarFrame) + } var onlineFrame = self.onlineNode.frame if self.onlineIsVoiceChat { diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 7f3e7d7660..e89624f2ce 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -333,6 +333,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let avatarNode: AvatarNode private let titleNode: TextNode + private var credibilityIconNode: ASImageNode? private var verificationIconNode: ASImageNode? private let statusNode: TextNode private var badgeBackgroundNode: ASImageNode? @@ -562,11 +563,19 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - var verificationIconImage: UIImage? + var currentCredibilityIconImage: UIImage? switch item.peer { case let .peer(peer, _): - if let peer = peer, peer.isVerified { - verificationIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + if let peer = peer { + if peer.isScam { + currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + } else if peer.isFake { + currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + } else if peer.isVerified { + currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + } else if peer.isPremium { + currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) + } } case .deviceContact: break @@ -726,8 +735,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } var additionalTitleInset: CGFloat = 0.0 - if let verificationIconImage = verificationIconImage { - additionalTitleInset += 3.0 + verificationIconImage.size.width + if let currentCredibilityIconImage = currentCredibilityIconImage { + additionalTitleInset += 3.0 + currentCredibilityIconImage.size.width } if let actionButtons = actionButtons { additionalTitleInset += 3.0 @@ -900,7 +909,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))) let _ = titleApply() - transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0)) + let titleFrame = titleFrame.offsetBy(dx: revealOffset, dy: 0.0) + transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.4 strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0 @@ -912,23 +922,23 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.frame = statusFrame transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) - if let verificationIconImage = verificationIconImage { - if strongSelf.verificationIconNode == nil { - let verificationIconNode = ASImageNode() - verificationIconNode.isLayerBacked = true - verificationIconNode.displayWithoutProcessing = true - verificationIconNode.displaysAsynchronously = false - strongSelf.verificationIconNode = verificationIconNode - strongSelf.offsetContainerNode.addSubnode(verificationIconNode) + if let currentCredibilityIconImage = currentCredibilityIconImage { + let iconNode: ASImageNode + if let current = strongSelf.credibilityIconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displaysAsynchronously = false + iconNode.displayWithoutProcessing = true + strongSelf.containerNode.addSubnode(iconNode) + strongSelf.credibilityIconNode = iconNode } - if let verificationIconNode = strongSelf.verificationIconNode { - verificationIconNode.image = verificationIconImage - - transition.updateFrame(node: verificationIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + titleFrame.maxX + 3.0, y: titleFrame.minY + 3.0 + UIScreenPixel), size: verificationIconImage.size)) - } - } else if let verificationIconNode = strongSelf.verificationIconNode { - strongSelf.verificationIconNode = nil - verificationIconNode.removeFromSupernode() + iconNode.image = currentCredibilityIconImage + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - currentCredibilityIconImage.size.height / 2.0) - UIScreenPixel), size: currentCredibilityIconImage.size)) + } else if let credibilityIconNode = strongSelf.credibilityIconNode { + strongSelf.credibilityIconNode = nil + credibilityIconNode.removeFromSupernode() } if let actionButtons = actionButtons { @@ -1128,10 +1138,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.statusNode.frame = statusFrame transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) - if let verificationIconNode = self.verificationIconNode { - var iconFrame = verificationIconNode.frame - iconFrame.origin.x = titleFrame.maxX + 3.0 - transition.updateFrame(node: verificationIconNode, frame: iconFrame) + if let credibilityIconNode = self.credibilityIconNode { + var iconFrame = credibilityIconNode.frame + iconFrame.origin.x = titleFrame.maxX + 4.0 + transition.updateFrame(node: credibilityIconNode, frame: iconFrame) } if let badgeBackgroundNode = self.badgeBackgroundNode, let badgeTextNode = self.badgeTextNode { diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 5e964ba5d7..66a4f920fd 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -3676,7 +3676,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } private func updateItemNodesVisibilities(onlyPositive: Bool) { - let visibilityRect = CGRect(origin: CGPoint(x: 0.0, y: self.insets.top), size: CGSize(width: self.visibleSize.width, height: self.visibleSize.height - self.insets.top - self.insets.bottom)) + let insets: UIEdgeInsets = self.visualInsets ?? self.insets + let visibilityRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: self.visibleSize.width, height: self.visibleSize.height - insets.top - insets.bottom)) for itemNode in self.itemNodes { let itemFrame = itemNode.apparentFrame var visibility: ListViewItemNodeVisibility = .none diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index ccb824c3d3..7e34f7d15f 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -460,6 +460,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo private let labelBadgeNode: ASImageNode private var labelArrowNode: ASImageNode? private let statusNode: TextNode + private var credibilityIconNode: ASImageNode? private var switchNode: SwitchNode? private var checkNode: ASImageNode? @@ -600,6 +601,22 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let labelDisclosureFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) var updatedLabelBadgeImage: UIImage? + var currentCredibilityIconImage: UIImage? + + if item.peer.isScam { + currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + } else if item.peer.isFake { + currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + } else if item.peer.isVerified { + currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + } else if item.peer.isPremium { + currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) + } + + var titleIconsWidth: CGFloat = 0.0 + if let currentCredibilityIconImage = currentCredibilityIconImage { + titleIconsWidth += 4.0 + currentCredibilityIconImage.size.width + } var badgeColor: UIColor? if case .badge = item.label { @@ -842,7 +859,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -921,7 +938,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } else { transition = .immediate } - + if let currentDisabledOverlayNode = currentDisabledOverlayNode { if currentDisabledOverlayNode != strongSelf.disabledOverlayNode { strongSelf.disabledOverlayNode = currentDisabledOverlayNode @@ -1036,9 +1053,29 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)) + let titleFrame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size) + transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) + if let currentCredibilityIconImage = currentCredibilityIconImage { + let iconNode: ASImageNode + if let current = strongSelf.credibilityIconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displaysAsynchronously = false + iconNode.displayWithoutProcessing = true + strongSelf.containerNode.addSubnode(iconNode) + strongSelf.credibilityIconNode = iconNode + } + iconNode.image = currentCredibilityIconImage + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - currentCredibilityIconImage.size.height / 2.0) - UIScreenPixel), size: currentCredibilityIconImage.size)) + } else if let credibilityIconNode = strongSelf.credibilityIconNode { + strongSelf.credibilityIconNode = nil + credibilityIconNode.removeFromSupernode() + } + if let currentSwitchNode = currentSwitchNode { if currentSwitchNode !== strongSelf.switchNode { strongSelf.switchNode = currentSwitchNode @@ -1278,6 +1315,10 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) + if let credibilityIconNode = self.credibilityIconNode { + transition.updateFrame(node: credibilityIconNode, frame: CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 4.0, y: credibilityIconNode.frame.minY), size: credibilityIconNode.bounds.size)) + } + var rightLabelInset: CGFloat = 15.0 + params.rightInset if let labelArrowNode = self.labelArrowNode { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index ce3fb4e275..4813cb00b1 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -63,6 +63,7 @@ public final class CachedUserData: CachedPeerData { public let hasScheduledMessages: Bool public let autoremoveTimeout: CachedPeerAutoremoveTimeout public let themeEmoticon: String? + public let photo: TelegramMediaImage? public let peerIds: Set public let messageIds: Set @@ -82,11 +83,12 @@ public final class CachedUserData: CachedPeerData { self.hasScheduledMessages = false self.autoremoveTimeout = .unknown self.themeEmoticon = nil + self.photo = nil self.peerIds = Set() self.messageIds = Set() } - public init(about: String?, botInfo: BotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?) { + public init(about: String?, botInfo: BotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: TelegramMediaImage?) { self.about = about self.botInfo = botInfo self.peerStatusSettings = peerStatusSettings @@ -100,6 +102,7 @@ public final class CachedUserData: CachedPeerData { self.hasScheduledMessages = hasScheduledMessages self.autoremoveTimeout = autoremoveTimeout self.themeEmoticon = themeEmoticon + self.photo = photo self.peerIds = Set() @@ -135,6 +138,12 @@ public final class CachedUserData: CachedPeerData { self.autoremoveTimeout = decoder.decodeObjectForKey("artv", decoder: CachedPeerAutoremoveTimeout.init(decoder:)) as? CachedPeerAutoremoveTimeout ?? .unknown self.themeEmoticon = decoder.decodeOptionalStringForKey("te") + if let photo = decoder.decodeObjectForKey("ph", decoder: { TelegramMediaImage(decoder: $0) }) as? TelegramMediaImage { + self.photo = photo + } else { + self.photo = nil + } + self.peerIds = Set() var messageIds = Set() @@ -182,6 +191,12 @@ public final class CachedUserData: CachedPeerData { } else { encoder.encodeNil(forKey: "te") } + + if let photo = self.photo { + encoder.encodeObject(photo, forKey: "ph") + } else { + encoder.encodeNil(forKey: "ph") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -196,58 +211,62 @@ public final class CachedUserData: CachedPeerData { return false } - return other.about == self.about && other.botInfo == self.botInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon + return other.about == self.about && other.botInfo == self.botInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon && self.photo == other.photo } public func withUpdatedAbout(_ about: String?) -> CachedUserData { - return CachedUserData(about: about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedBotInfo(_ botInfo: BotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedIsBlocked(_ isBlocked: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedCommonGroupCount(_ commonGroupCount: Int32) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedVoiceCallsAvailable(_ voiceCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedVideoCallsAvailable(_ videoCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedCallsPrivate(_ callsPrivate: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedCanPinMessages(_ canPinMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo) + } + + public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index fed43d2ada..9cafdf198b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -234,7 +234,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee previous = CachedUserData() } switch fullUser { - case let .userFull(userFullFlags, _, userFullAbout, userFullSettings, _, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _): + case let .userFull(userFullFlags, _, userFullAbout, userFullSettings, profilePhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _): let botInfo = userFullBotInfo.flatMap(BotInfo.init(apiBotInfo:)) let isBlocked = (userFullFlags & (1 << 0)) != 0 let voiceCallsAvailable = (userFullFlags & (1 << 4)) != 0 @@ -250,9 +250,12 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let autoremoveTimeout: CachedPeerAutoremoveTimeout = .known(CachedPeerAutoremoveTimeout.Value(userFullTtlPeriod)) + let photo = profilePhoto.flatMap { telegramMediaImageFromApiPhoto($0) } + return previous.withUpdatedAbout(userFullAbout).withUpdatedBotInfo(botInfo).withUpdatedCommonGroupCount(userFullCommonChatsCount).withUpdatedIsBlocked(isBlocked).withUpdatedVoiceCallsAvailable(voiceCallsAvailable).withUpdatedVideoCallsAvailable(videoCallsAvailable).withUpdatedCallsPrivate(callsPrivate).withUpdatedCanPinMessages(canPinMessages).withUpdatedPeerStatusSettings(peerStatusSettings).withUpdatedPinnedMessageId(pinnedMessageId).withUpdatedHasScheduledMessages(hasScheduledMessages) .withUpdatedAutoremoveTimeout(autoremoveTimeout) .withUpdatedThemeEmoticon(userFullThemeEmoticon) + .withUpdatedPhoto(photo) } }) } diff --git a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift index 3c9d3817ce..798396befc 100644 --- a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift @@ -2,15 +2,45 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import SwiftSignalKit +import Postbox +import TelegramCore import AvatarNode import ContextUI +import TelegramPresentationData +import TelegramUniversalVideoContent +import UniversalMediaPlayer +import GalleryUI +import HierarchyTrackingLayer +import AccountContext private let normalFont = avatarPlaceholderFont(size: 16.0) private let smallFont = avatarPlaceholderFont(size: 12.0) +private let maxVideoLoopCount = 3 + final class ChatAvatarNavigationNode: ASDisplayNode { + private var context: AccountContext? + private let containerNode: ContextControllerSourceNode let avatarNode: AvatarNode + private var videoNode: UniversalVideoNode? + + private var videoContent: NativeVideoContent? + private let playbackStartDisposable = MetaDisposable() + private var cachedDataDisposable = MetaDisposable() + private var hierarchyTrackingLayer: HierarchyTrackingLayer? + private var videoLoopCount = 0 + + private var trackingIsInHierarchy: Bool = false { + didSet { + if self.trackingIsInHierarchy != oldValue { + Queue.mainQueue().justDispatch { + self.updateVideoVisibility() + } + } + } + } var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? var contextActionIsEnabled: Bool = true { @@ -41,11 +71,79 @@ final class ChatAvatarNavigationNode: ASDisplayNode { self.avatarNode.frame = self.containerNode.bounds } + deinit { + self.cachedDataDisposable.dispose() + self.playbackStartDisposable.dispose() + } + override func didLoad() { super.didLoad() self.view.isOpaque = false } + public func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), storeUnrounded: Bool = false) { + self.context = context + self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: displayDimensions, storeUnrounded: storeUnrounded) + + if let peer = peer, peer.isPremium { + self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + guard let strongSelf = self else { + return + } + let cachedPeerData = peerView.cachedData + if let cachedPeerData = cachedPeerData as? CachedUserData { + if let photo = cachedPeerData.photo, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) { + let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) + if videoContent.id != strongSelf.videoContent?.id { + strongSelf.videoNode?.removeFromSupernode() + strongSelf.videoContent = videoContent + } + + if strongSelf.hierarchyTrackingLayer == nil { + let hierarchyTrackingLayer = HierarchyTrackingLayer() + hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.trackingIsInHierarchy = true + } + + hierarchyTrackingLayer.didExitHierarchy = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.trackingIsInHierarchy = false + } + strongSelf.hierarchyTrackingLayer = hierarchyTrackingLayer + strongSelf.layer.addSublayer(hierarchyTrackingLayer) + } + } else { + strongSelf.videoContent = nil + + strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() + strongSelf.hierarchyTrackingLayer = nil + } + + strongSelf.updateVideoVisibility() + } else { + let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() + } + })) + } else { + self.cachedDataDisposable.set(nil) + self.videoContent = nil + + self.videoNode?.removeFromSupernode() + self.videoNode = nil + + self.hierarchyTrackingLayer?.removeFromSuperlayer() + self.hierarchyTrackingLayer = nil + } + } + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: 37.0, height: 37.0) } @@ -82,4 +180,77 @@ final class ChatAvatarNavigationNode: ASDisplayNode { snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } } + + private func updateVideoVisibility() { + guard let context = self.context else { + return + } + + let isVisible = self.trackingIsInHierarchy + if isVisible, let videoContent = self.videoContent, self.videoLoopCount != maxVideoLoopCount { + if self.videoNode == nil { + let mediaManager = context.sharedContext.mediaManager + let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .minimal) + videoNode.clipsToBounds = true + videoNode.isUserInteractionEnabled = false + videoNode.isHidden = true + videoNode.playbackCompleted = { [weak self] in + if let strongSelf = self { + strongSelf.videoLoopCount += 1 + if strongSelf.videoLoopCount == maxVideoLoopCount { + if let videoNode = strongSelf.videoNode { + strongSelf.videoNode = nil + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.removeFromSupernode() + }) + } + } + } + } + + if let _ = videoContent.startTimestamp { + self.playbackStartDisposable.set((videoNode.status + |> map { status -> Bool in + if let status = status, case .playing = status.status { + return true + } else { + return false + } + } + |> filter { playing in + return playing + } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + if let strongSelf = self { + Queue.mainQueue().after(0.15) { + strongSelf.videoNode?.isHidden = false + } + } + })) + } else { + self.playbackStartDisposable.set(nil) + videoNode.isHidden = false + } + videoNode.layer.cornerRadius = self.avatarNode.frame.size.width / 2.0 + if #available(iOS 13.0, *) { + videoNode.layer.cornerCurve = .circular + } + + videoNode.canAttachContent = true + videoNode.play() + + self.containerNode.insertSubnode(videoNode, aboveSubnode: self.avatarNode) + self.videoNode = videoNode + } + } else if let videoNode = self.videoNode { + self.videoNode = nil + videoNode.removeFromSupernode() + } + + if let videoNode = self.videoNode { + videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) + videoNode.frame = self.avatarNode.frame + } + } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 31d3baff12..0ed33b837c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3895,7 +3895,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { imageOverride = nil } - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = presentationInterfaceState.strings.Conversation_ContextMenuOpenProfile } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 95423a59db..2b3fcdf449 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -461,7 +461,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState loadStickerSaveStatus = file.fileId } } - loadCopyMediaResource = file.resource + if loadStickerSaveStatus == nil { + loadCopyMediaResource = file.resource + } } else if media is TelegramMediaAction || media is TelegramMediaExpiredContent { isAction = true } else if let image = media as? TelegramMediaImage { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 7a2482645c..3048d9661b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1673,44 +1673,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private func gestureRecognized(gesture: TapLongTapOrDoubleTapGesture, location: CGPoint, recognizer: TapLongTapOrDoubleTapGestureRecognizer?) -> InternalBubbleTapAction? { switch gesture { - case .tap: - if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { - if let item = self.item, let author = item.content.firstMessage.author { - return .optionalAction({ - var openPeerId = item.effectiveAuthorId ?? author.id - var navigate: ChatControllerInteractionNavigateToPeer - - if item.content.firstMessage.id.peerId == item.context.account.peerId { - navigate = .chat(textInputState: nil, subject: nil, peekData: nil) - } else { - navigate = .info - } - - for attribute in item.content.firstMessage.attributes { - if let attribute = attribute as? SourceReferenceMessageAttribute { - openPeerId = attribute.messageId.peerId - navigate = .chat(textInputState: nil, subject: .message(id: .id(attribute.messageId), highlight: true, timecode: nil), peekData: nil) - } - } - - if item.effectiveAuthorId?.namespace == Namespaces.Peer.Empty { - item.controllerInteraction.displayMessageTooltip(item.content.firstMessage.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, avatarNode.frame) - } else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), forwardInfo.author == nil { - item.controllerInteraction.displayImportedMessageTooltip(avatarNode) - } else { - if !item.message.id.peerId.isReplies, let channel = item.content.firstMessage.forwardInfo?.author as? TelegramChannel, channel.username == nil { - if case .member = channel.participationStatus { - } else { - item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, self, avatarNode.frame) - } - } - item.controllerInteraction.openPeer(openPeerId, navigate, MessageReference(item.message), item.message.peers[openPeerId]) - } - }) - } - return nil - } - + case .tap: if let viaBotNode = self.viaBotNode, viaBotNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { diff --git a/submodules/TelegramUI/Sources/ChatMessageAvatarAccessoryItem.swift b/submodules/TelegramUI/Sources/ChatMessageAvatarAccessoryItem.swift deleted file mode 100644 index eb86e97f8f..0000000000 --- a/submodules/TelegramUI/Sources/ChatMessageAvatarAccessoryItem.swift +++ /dev/null @@ -1,183 +0,0 @@ -import Foundation -import UIKit -import Postbox -import AsyncDisplayKit -import Display -import TelegramCore -import TelegramPresentationData -import AvatarNode -import AccountContext - -private let avatarFont = avatarPlaceholderFont(size: 16.0) - -final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem { - private let context: AccountContext - private let peerId: PeerId - private let peer: Peer? - private let messageReference: MessageReference? - private let messageTimestamp: Int32 - private let forwardInfo: MessageForwardInfo? - private let emptyColor: UIColor - private let controllerInteraction: ChatControllerInteraction - - private let day: Int32 - - init(context: AccountContext, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, messageTimestamp: Int32, forwardInfo: MessageForwardInfo?, emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) { - self.context = context - self.peerId = peerId - self.peer = peer - self.messageReference = messageReference - self.messageTimestamp = messageTimestamp - self.forwardInfo = forwardInfo - self.emptyColor = emptyColor - self.controllerInteraction = controllerInteraction - - var t: time_t = time_t(messageTimestamp) - var timeinfo: tm = tm() - gmtime_r(&t, &timeinfo) - - self.day = timeinfo.tm_mday - } - - func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool { - if case let other as ChatMessageAvatarAccessoryItem = other { - if other.peerId != self.peerId { - return false - } - if self.day != other.day { - return false - } - - var effectiveTimestamp = self.messageTimestamp - if let forwardInfo = self.forwardInfo, forwardInfo.flags.contains(.isImported) { - effectiveTimestamp = forwardInfo.date - } - - var effectiveOtherTimestamp = other.messageTimestamp - if let otherForwardInfo = other.forwardInfo, otherForwardInfo.flags.contains(.isImported) { - effectiveOtherTimestamp = otherForwardInfo.date - } - - if abs(effectiveTimestamp - effectiveOtherTimestamp) >= 10 * 60 { - return false - } - if let forwardInfo = self.forwardInfo, let otherForwardInfo = other.forwardInfo, forwardInfo.flags.contains(.isImported), otherForwardInfo.flags.contains(.isImported) { - if (forwardInfo.authorSignature != nil) == (otherForwardInfo.authorSignature != nil) && (forwardInfo.author != nil) == (otherForwardInfo.author != nil) { - if let authorSignature = forwardInfo.authorSignature, let otherAuthorSignature = otherForwardInfo.authorSignature { - if authorSignature != otherAuthorSignature { - return false - } - } else if let authorId = forwardInfo.author?.id, let otherAuthorId = otherForwardInfo.author?.id { - if authorId != otherAuthorId { - return false - } - } - } else { - return false - } - } else if let forwardInfo = self.forwardInfo, forwardInfo.flags.contains(.isImported) { - return false - } else if let otherForwardInfo = other.forwardInfo, otherForwardInfo.flags.contains(.isImported) { - return false - } - return true - } else { - return false - } - } - - func node(synchronous: Bool) -> ListViewAccessoryItemNode { - let node = ChatMessageAvatarAccessoryItemNode() - node.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) - if let forwardInfo = self.forwardInfo, forwardInfo.flags.contains(.isImported) { - if let author = forwardInfo.author { - node.setPeer(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, peer: author, authorOfMessage: self.messageReference, emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction) - } else if let authorSignature = forwardInfo.authorSignature, !authorSignature.isEmpty { - let components = authorSignature.components(separatedBy: " ") - if !components.isEmpty, !components[0].hasPrefix("+") { - var letters: [String] = [] - - letters.append(String(components[0][components[0].startIndex])) - if components.count > 1 { - letters.append(String(components[1][components[1].startIndex])) - } - - node.setCustomLetters(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, letters: letters, emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction) - } else { - node.setCustomLetters(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, letters: [], emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction) - } - } else { - node.setCustomLetters(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, letters: [], emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction) - } - } else if let peer = self.peer { - node.setPeer(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, peer: peer, authorOfMessage: self.messageReference, emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction) - } - return node - } -} - -final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode { - var controllerInteraction: ChatControllerInteraction? - var peer: Peer? - var messageId: MessageId? - - let containerNode: ContextControllerSourceNode - let avatarNode: AvatarNode - - var contextActionIsEnabled: Bool = true { - didSet { - if self.contextActionIsEnabled != oldValue { - self.containerNode.isGestureEnabled = self.contextActionIsEnabled - } - } - } - - override init() { - self.containerNode = ContextControllerSourceNode() - self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) - - self.avatarNode = AvatarNode(font: avatarFont) - self.avatarNode.isLayerBacked = !smartInvertColorsEnabled() - self.avatarNode.frame = self.containerNode.bounds - self.avatarNode.isUserInteractionEnabled = false - - super.init() - - self.isLayerBacked = false - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.avatarNode) - - self.containerNode.activated = { [weak self] gesture, _ in - guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction, let peer = strongSelf.peer else { - return - } - controllerInteraction.openPeerContextMenu(peer, strongSelf.messageId, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) - } - } - - func setCustomLetters(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, letters: [String], emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) { - self.controllerInteraction = controllerInteraction - self.peer = nil - - self.contextActionIsEnabled = false - - self.avatarNode.setCustomLetters(letters, icon: !letters.isEmpty ? nil : .phone) - } - - func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) { - self.controllerInteraction = controllerInteraction - self.peer = peer - if let messageReference = authorOfMessage, case let .message(_, id, _, _, _) = messageReference.content { - self.messageId = id - } - - self.contextActionIsEnabled = peer.smallProfileImage != nil - - var overrideImage: AvatarNodeImageOverride? - if peer.isDeleted { - overrideImage = .deletedIcon - } - self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: 38.0, height: 38.0)) - } -} diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 9f7f6ac185..eb180b0697 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -832,11 +832,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return .fail } } - - if let avatarNode = strongSelf.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(point) { - return .waitForSingleTap - } - + if let nameNode = strongSelf.nameNode, nameNode.frame.contains(point) { if let item = strongSelf.item { for attribute in item.message.attributes { @@ -3115,46 +3111,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode switch gesture { case .tap: - if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { - return .action({ - if let item = self.item, let author = item.content.firstMessage.author { - var openPeerId = item.effectiveAuthorId ?? author.id - var navigate: ChatControllerInteractionNavigateToPeer - - if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { - navigate = .chat(textInputState: nil, subject: nil, peekData: nil) - } else if openPeerId.namespace == Namespaces.Peer.CloudUser { - navigate = .info - } else { - navigate = .chat(textInputState: nil, subject: nil, peekData: nil) - } - - for attribute in item.content.firstMessage.attributes { - if let attribute = attribute as? SourceReferenceMessageAttribute { - openPeerId = attribute.messageId.peerId - navigate = .chat(textInputState: nil, subject: .message(id: .id(attribute.messageId), highlight: true, timecode: nil), peekData: nil) - } - } - - if item.effectiveAuthorId?.namespace == Namespaces.Peer.Empty { - item.controllerInteraction.displayMessageTooltip(item.content.firstMessage.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, avatarNode.frame) - } else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), forwardInfo.author == nil { - item.controllerInteraction.displayImportedMessageTooltip(avatarNode) - } else { - if item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId), let channel = item.content.firstMessage.forwardInfo?.author as? TelegramChannel, channel.username == nil { - if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { - } else if case .member = channel.participationStatus { - } else if !item.message.id.peerId.isReplies { - item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, self, avatarNode.frame) - return - } - } - item.controllerInteraction.openPeer(openPeerId, navigate, MessageReference(item.message), item.message.peers[openPeerId]) - } - } - }) - } - if let nameNode = self.nameNode, nameNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { @@ -3484,10 +3440,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(point) { - return avatarNode.containerNode.view - } - if !self.backgroundNode.frame.contains(point) { if let actionButtonsNode = self.actionButtonsNode, let result = actionButtonsNode.hitTest(self.view.convert(point, to: actionButtonsNode.view), with: event) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index 23fc82effe..b848d6ace2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -2,11 +2,16 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import TelegramPresentationData +import SwiftSignalKit import Postbox +import TelegramPresentationData import AccountContext import AvatarNode import TelegramCore +import TelegramUniversalVideoContent +import UniversalMediaPlayer +import GalleryUI +import HierarchyTrackingLayer private let timezoneOffset: Int32 = { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -345,7 +350,7 @@ final class ChatMessageAvatarHeader: ListViewItemHeader { self.controllerInteraction = controllerInteraction self.id = ListViewItemNode.HeaderId(space: 1, id: Id(peerId: peerId, timestampId: dateHeaderTimestampId(timestamp: timestamp))) } - + let stickDirection: ListViewItemHeaderStickDirection = .top let stickOverInsets: Bool = false @@ -376,17 +381,40 @@ final class ChatMessageAvatarHeader: ListViewItemHeader { private let avatarFont = avatarPlaceholderFont(size: 16.0) +private let maxVideoLoopCount = 3 + final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { + private let context: AccountContext + private var presentationData: ChatPresentationData + private let controllerInteraction: ChatControllerInteraction + private let peerId: PeerId private let messageReference: MessageReference? private let peer: Peer? private let containerNode: ContextControllerSourceNode private let avatarNode: AvatarNode - private var presentationData: ChatPresentationData - private let context: AccountContext - private let controllerInteraction: ChatControllerInteraction - + private var videoNode: UniversalVideoNode? + + private var videoContent: NativeVideoContent? + private let playbackStartDisposable = MetaDisposable() + private var cachedDataDisposable = MetaDisposable() + private var hierarchyTrackingLayer: HierarchyTrackingLayer? + private var videoLoopCount = 0 + + private var trackingIsInHierarchy: Bool = false { + didSet { + if self.trackingIsInHierarchy != oldValue { + Queue.mainQueue().justDispatch { + if self.trackingIsInHierarchy { + self.videoLoopCount = 0 + } + self.updateVideoVisibility() + } + } + } + } + init(peerId: PeerId, peer: Peer?, messageReference: MessageReference?, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction, synchronousLoad: Bool) { self.peerId = peerId self.peer = peer @@ -423,6 +451,11 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { self.updateSelectionState(animated: false) } + + deinit { + self.cachedDataDisposable.dispose() + self.playbackStartDisposable.dispose() + } func setCustomLetters(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, letters: [String], emptyColor: UIColor) { self.containerNode.isGestureEnabled = false @@ -438,6 +471,64 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { overrideImage = .deletedIcon } self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: 38.0, height: 38.0)) + + if peer.isPremium { + self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + guard let strongSelf = self else { + return + } + let cachedPeerData = peerView.cachedData + if let cachedPeerData = cachedPeerData as? CachedUserData { + if let photo = cachedPeerData.photo, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer) { + let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) + if videoContent.id != strongSelf.videoContent?.id { + strongSelf.videoNode?.removeFromSupernode() + strongSelf.videoContent = videoContent + } + + if strongSelf.hierarchyTrackingLayer == nil { + let hierarchyTrackingLayer = HierarchyTrackingLayer() + hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.trackingIsInHierarchy = true + } + + hierarchyTrackingLayer.didExitHierarchy = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.trackingIsInHierarchy = false + } + strongSelf.hierarchyTrackingLayer = hierarchyTrackingLayer + strongSelf.layer.addSublayer(hierarchyTrackingLayer) + } + } else { + strongSelf.videoContent = nil + + strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() + strongSelf.hierarchyTrackingLayer = nil + } + + strongSelf.updateVideoVisibility() + } else { + let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() + } + })) + } else { + self.cachedDataDisposable.set(nil) + self.videoContent = nil + + self.videoNode?.removeFromSupernode() + self.videoNode = nil + + self.hierarchyTrackingLayer?.removeFromSuperlayer() + self.hierarchyTrackingLayer = nil + } } override func didLoad() { @@ -509,4 +600,74 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { } } } + + private func updateVideoVisibility() { + let context = self.context + let isVisible = self.trackingIsInHierarchy + if isVisible, let videoContent = self.videoContent, self.videoLoopCount != maxVideoLoopCount { + if self.videoNode == nil { + let mediaManager = context.sharedContext.mediaManager + let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) + videoNode.clipsToBounds = true + videoNode.isUserInteractionEnabled = false + videoNode.isHidden = true + videoNode.playbackCompleted = { [weak self] in + if let strongSelf = self { + strongSelf.videoLoopCount += 1 + if strongSelf.videoLoopCount == maxVideoLoopCount { + if let videoNode = strongSelf.videoNode { + strongSelf.videoNode = nil + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.removeFromSupernode() + }) + } + } + } + } + + if let _ = videoContent.startTimestamp { + self.playbackStartDisposable.set((videoNode.status + |> map { status -> Bool in + if let status = status, case .playing = status.status { + return true + } else { + return false + } + } + |> filter { playing in + return playing + } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + if let strongSelf = self { + Queue.mainQueue().after(0.15) { + strongSelf.videoNode?.isHidden = false + } + } + })) + } else { + self.playbackStartDisposable.set(nil) + videoNode.isHidden = false + } + videoNode.layer.cornerRadius = self.avatarNode.frame.size.width / 2.0 + if #available(iOS 13.0, *) { + videoNode.layer.cornerCurve = .circular + } + + videoNode.canAttachContent = true + videoNode.play() + + self.containerNode.insertSubnode(videoNode, aboveSubnode: self.avatarNode) + self.videoNode = videoNode + } + } else if let videoNode = self.videoNode { + self.videoNode = nil + videoNode.removeFromSupernode() + } + + if let videoNode = self.videoNode { + videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) + videoNode.frame = self.avatarNode.frame + } + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 8b4a54854d..4291fba002 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -880,42 +880,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD private func gestureRecognized(gesture: TapLongTapOrDoubleTapGesture, location: CGPoint, recognizer: TapLongTapOrDoubleTapGestureRecognizer?) -> InternalBubbleTapAction? { switch gesture { - case .tap: - if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { - if let item = self.item, let author = item.content.firstMessage.author { - var openPeerId = item.effectiveAuthorId ?? author.id - var navigate: ChatControllerInteractionNavigateToPeer - - if item.content.firstMessage.id.peerId == item.context.account.peerId { - navigate = .chat(textInputState: nil, subject: nil, peekData: nil) - } else { - navigate = .info - } - - for attribute in item.content.firstMessage.attributes { - if let attribute = attribute as? SourceReferenceMessageAttribute { - openPeerId = attribute.messageId.peerId - navigate = .chat(textInputState: nil, subject: .message(id: .id(attribute.messageId), highlight: true, timecode: nil), peekData: nil) - } - } - - return .optionalAction({ - if item.effectiveAuthorId?.namespace == Namespaces.Peer.Empty { - item.controllerInteraction.displayMessageTooltip(item.content.firstMessage.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, avatarNode.frame) - } else { - if !item.message.id.peerId.isReplies, let channel = item.content.firstMessage.forwardInfo?.author as? TelegramChannel, channel.username == nil { - if case .member = channel.participationStatus { - } else { - item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, self, avatarNode.frame) - return - } - } - item.controllerInteraction.openPeer(openPeerId, navigate, MessageReference(item.message), item.message.peers[openPeerId]) - } - }) - } - } - + case .tap: if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Sources/ChatMessageItem.swift index 4bdc1d3eef..35795344b5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItem.swift @@ -358,7 +358,6 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } if !hasActionMedia && !isBroadcastChannel { if let effectiveAuthor = effectiveAuthor { - //accessoryItem = ChatMessageAvatarAccessoryItem(context: context, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), messageTimestamp: content.index.timestamp, forwardInfo: message.forwardInfo, emptyColor: presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill, controllerInteraction: controllerInteraction) avatarHeader = ChatMessageAvatarHeader(timestamp: content.index.timestamp, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), message: message, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 42b9a6dece..ec9b4cea42 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -723,12 +723,6 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - override public func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode, leftInset: CGFloat, rightInset: CGFloat) { - if let avatarNode = accessoryItemNode as? ChatMessageAvatarAccessoryItemNode { - avatarNode.frame = CGRect(origin: CGPoint(x: leftInset + 3.0, y: self.apparentFrame.height - 38.0 - self.insets.top - 2.0 - UIScreenPixel), size: CGSize(width: 38.0, height: 38.0)) - } - } - func cancelInsertionAnimations() { } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 77937e07f6..d7657cb0fd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -1090,43 +1090,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private func gestureRecognized(gesture: TapLongTapOrDoubleTapGesture, location: CGPoint, recognizer: TapLongTapOrDoubleTapGestureRecognizer?) -> InternalBubbleTapAction? { switch gesture { - case .tap: - if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { - if let item = self.item, let author = item.content.firstMessage.author { - return .optionalAction({ - var openPeerId = item.effectiveAuthorId ?? author.id - var navigate: ChatControllerInteractionNavigateToPeer - - if item.content.firstMessage.id.peerId == item.context.account.peerId { - navigate = .chat(textInputState: nil, subject: nil, peekData: nil) - } else { - navigate = .info - } - - for attribute in item.content.firstMessage.attributes { - if let attribute = attribute as? SourceReferenceMessageAttribute { - openPeerId = attribute.messageId.peerId - navigate = .chat(textInputState: nil, subject: .message(id: .id(attribute.messageId), highlight: true, timecode: nil), peekData: nil) - } - } - - if item.effectiveAuthorId?.namespace == Namespaces.Peer.Empty { - item.controllerInteraction.displayMessageTooltip(item.content.firstMessage.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, avatarNode.frame) - } else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), forwardInfo.author == nil { - item.controllerInteraction.displayImportedMessageTooltip(avatarNode) - } else { - if !item.message.id.peerId.isReplies, let channel = item.content.firstMessage.forwardInfo?.author as? TelegramChannel, channel.username == nil { - if case .member = channel.participationStatus { - } else { - item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, self, avatarNode.frame) - } - } - item.controllerInteraction.openPeer(openPeerId, navigate, MessageReference(item.message), item.message.peers[openPeerId]) - } - }) - } - } - + case .tap: if let viaBotNode = self.viaBotNode, viaBotNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 7759e02e7f..74775f6111 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -413,29 +413,29 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let representations: [ImageRepresentationWithReference] let videoRepresentations: [VideoRepresentationWithReference] let immediateThumbnailData: Data? - var id: Int64 + var videoId: Int64 switch item { case .custom: representations = [] videoRepresentations = [] immediateThumbnailData = nil - id = 0 + videoId = 0 case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): representations = topRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail - id = peer.id.id._internalGetInt64Value() + videoId = peer.id.id._internalGetInt64Value() if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { - id = id &+ resource.photoId + videoId = videoId &+ resource.photoId } case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail if case let .cloud(imageId, _, _) = reference { - id = imageId + videoId = imageId } else { - id = peer.id.id._internalGetInt64Value() + videoId = peer.id.id._internalGetInt64Value() } } @@ -443,7 +443,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { if let video = videoRepresentations.last, let peerReference = PeerReference(peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(id, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index e2ae3368ff..7a2bf1550e 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -33,7 +33,7 @@ public final class NativeVideoContent: UniversalVideoContent { let onlyFullSizeThumbnail: Bool let useLargeThumbnail: Bool let autoFetchFullSizeThumbnail: Bool - let startTimestamp: Double? + public let startTimestamp: Double? let endTimestamp: Double? let continuePlayingWithoutSoundOnLostAudioSession: Bool let placeholderColor: UIColor