diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 2321b4c35d..6a2e0c8a76 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -142,6 +142,15 @@ public enum AvatarGalleryEntry: Equatable { } } + public var emojiMarkup: TelegramMediaImage.EmojiMarkup? { + switch self { + case .topImage: + return nil + case let .image(_, _, _, _, _, _, _, _, _, _, _, markup): + return markup + } + } + public var indexData: GalleryItemIndexData? { switch self { case let .topImage(_, _, _, indexData, _, _): @@ -343,11 +352,16 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { var videoRepresentations: [VideoRepresentationWithReference] = first.videoRepresentations + var emojiMarkup: TelegramMediaImage.EmojiMarkup? = first.emojiMarkup let isPersonal = first.representations.first?.representation.isPersonal == true if videoRepresentations.isEmpty, !isPersonal { videoRepresentations = photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }) } - result.append(.image(photo.image.imageId, photo.image.reference, first.representations, videoRepresentations, peer, secondEntry != nil ? 0 : photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false, photo.image.emojiMarkup)) + if emojiMarkup == nil, !isPersonal { + emojiMarkup = photo.image.emojiMarkup + } + + result.append(.image(photo.image.imageId, photo.image.reference, first.representations, videoRepresentations, peer, secondEntry != nil ? 0 : photo.date, indexData, photo.messageId, first.immediateThumbnailData ?? photo.image.immediateThumbnailData, nil, false, emojiMarkup)) } else { result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, photo.image.id == lastEntry?.id, photo.image.emojiMarkup)) } diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 606ca4466b..a7b2b34a3e 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -4676,6 +4676,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getEmojiProfilePhotoGroups(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(564480243) + serializeInt32(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getEmojiProfilePhotoGroups", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.EmojiGroups? in + let reader = BufferReader(buffer) + var result: Api.messages.EmojiGroups? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.EmojiGroups + } + return result + }) + } +} public extension Api.functions.messages { static func getEmojiStatusGroups(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7764,15 +7779,16 @@ public extension Api.functions.photos { } } public extension Api.functions.photos { - static func uploadContactProfilePhoto(flags: Int32, userId: Api.InputUser, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func uploadContactProfilePhoto(flags: Int32, userId: Api.InputUser, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?, videoEmojiMarkup: Api.VideoSize?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1189444673) + buffer.appendInt32(-515093903) serializeInt32(flags, buffer: buffer, boxed: false) userId.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {file!.serialize(buffer, true)} if Int(flags) & Int(1 << 1) != 0 {video!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "photos.uploadContactProfilePhoto", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("file", String(describing: file)), ("video", String(describing: video)), ("videoStartTs", String(describing: videoStartTs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in + if Int(flags) & Int(1 << 5) != 0 {videoEmojiMarkup!.serialize(buffer, true)} + return (FunctionDescription(name: "photos.uploadContactProfilePhoto", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("file", String(describing: file)), ("video", String(describing: video)), ("videoStartTs", String(describing: videoStartTs)), ("videoEmojiMarkup", String(describing: videoEmojiMarkup))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in let reader = BufferReader(buffer) var result: Api.photos.Photo? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift index 714aa37462..01bb54a501 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift @@ -26,8 +26,8 @@ public extension TelegramEngine { return _internal_updateContactName(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName) } - public func updateContactPhoto(peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updateContactPhoto(account: self.account, peerId: peerId, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + public func updateContactPhoto(peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateContactPhoto(account: self.account, peerId: peerId, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes) } public func deviceContactsImportedByCount(contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift index fa52c750b2..77e74723b6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift @@ -19,7 +19,13 @@ public enum UploadPeerPhotoMarkup { } func _internal_updateAccountPhoto(account: Account, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, fallback: Bool, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: account.peerId, photo: resource.flatMap({ _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }), video: videoResource.flatMap({ _internal_uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, markup: markup, fallback: fallback, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + let photo: Signal? + if videoResource == nil && markup != nil, let resource = resource { + photo = .single(UploadedPeerPhotoData.withResource(resource)) + } else { + photo = resource.flatMap({ _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }) + } + return _internal_updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: account.peerId, photo: photo, video: videoResource.flatMap({ _internal_uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, markup: markup, fallback: fallback, mapResourceToAvatarSizes: mapResourceToAvatarSizes) } public enum SetCustomPeerPhotoMode { @@ -28,13 +34,14 @@ public enum SetCustomPeerPhotoMode { case customAndSuggest } -func _internal_updateContactPhoto(account: Account, peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: resource.flatMap({ _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }), video: videoResource.flatMap({ _internal_uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, customPeerPhotoMode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes) +func _internal_updateContactPhoto(account: Account, peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: resource.flatMap({ _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }), video: videoResource.flatMap({ _internal_uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, markup: markup, customPeerPhotoMode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes) } public struct UploadedPeerPhotoData { fileprivate let resource: MediaResource fileprivate let content: UploadedPeerPhotoDataContent + fileprivate let local: Bool public var isCompleted: Bool { if case let .result(result) = content, case .inputFile = result { @@ -43,6 +50,10 @@ public struct UploadedPeerPhotoData { return false } } + + static func withResource(_ resource: MediaResource) -> UploadedPeerPhotoData { + return UploadedPeerPhotoData(resource: resource, content: .result(.inputFile(.inputFile(id: 0, parts: 0, name: "", md5Checksum: ""))), local: true) + } } enum UploadedPeerPhotoDataContent { @@ -53,10 +64,10 @@ enum UploadedPeerPhotoDataContent { func _internal_uploadedPeerPhoto(postbox: Postbox, network: Network, resource: MediaResource) -> Signal { return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedPeerPhotoData in - return UploadedPeerPhotoData(resource: resource, content: .result(result)) + return UploadedPeerPhotoData(resource: resource, content: .result(result), local: false) } |> `catch` { _ -> Signal in - return .single(UploadedPeerPhotoData(resource: resource, content: .error)) + return .single(UploadedPeerPhotoData(resource: resource, content: .error, local: false)) } } @@ -64,18 +75,18 @@ func _internal_uploadedPeerVideo(postbox: Postbox, network: Network, messageMedi if let messageMediaPreuploadManager = messageMediaPreuploadManager { return messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), hintFileSize: nil, hintFileIsLarge: false) |> map { result -> UploadedPeerPhotoData in - return UploadedPeerPhotoData(resource: resource, content: .result(result)) + return UploadedPeerPhotoData(resource: resource, content: .result(result), local: false) } |> `catch` { _ -> Signal in - return .single(UploadedPeerPhotoData(resource: resource, content: .error)) + return .single(UploadedPeerPhotoData(resource: resource, content: .error, local: false)) } } else { return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedPeerPhotoData in - return UploadedPeerPhotoData(resource: resource, content: .result(result)) + return UploadedPeerPhotoData(resource: resource, content: .result(result), local: false) } |> `catch` { _ -> Signal in - return .single(UploadedPeerPhotoData(resource: resource, content: .error)) + return .single(UploadedPeerPhotoData(resource: resource, content: .error, local: false)) } } } @@ -88,6 +99,16 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state return peer |> mapError { _ -> UploadPeerPhotoError in } |> mapToSignal { peer -> Signal in + var videoEmojiMarkup: Api.VideoSize? + if let markup = markup { + switch markup { + case let .emoji(fileId, backgroundColors): + videoEmojiMarkup = .videoSizeEmojiMarkup(emojiId: fileId, backgroundColors: backgroundColors) + case let .sticker(packReference, fileId, backgroundColors): + videoEmojiMarkup = .videoSizeStickerMarkup(stickerset: packReference.apiInputStickerSet, stickerId: fileId, backgroundColors: backgroundColors) + } + } + if let photo = photo { let mappedPhoto = photo |> take(until: { value in @@ -111,7 +132,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state } else { mappedVideo = .single(nil) } - + return combineLatest(mappedPhoto, mappedVideo) |> mapError { _ -> UploadPeerPhotoError in } |> mapToSignal { photoResult, videoResult -> Signal<(UpdatePeerPhotoStatus, MediaResource?, MediaResource?), UploadPeerPhotoError> in @@ -145,33 +166,33 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state } } } + + var photoFile: Api.InputFile? + if !photoResult.local { + photoFile = file + } if peer is TelegramUser { - var flags: Int32 = (1 << 0) + var flags: Int32 = 0 + if let _ = photoFile { + flags = (1 << 0) + } if let _ = videoFile { flags |= (1 << 1) if let _ = videoStartTimestamp { flags |= (1 << 2) } } - - var videoEmojiMarkup: Api.VideoSize? - if let markup = markup{ - switch markup { - case let .emoji(fileId, backgroundColors): - videoEmojiMarkup = .videoSizeEmojiMarkup(emojiId: fileId, backgroundColors: backgroundColors) - case let .sticker(packReference, fileId, backgroundColors): - videoEmojiMarkup = .videoSizeStickerMarkup(stickerset: packReference.apiInputStickerSet, stickerId: fileId, backgroundColors: backgroundColors) - } - flags |= (1 << 4) - } - + let request: Signal if peer.id == accountPeerId { if fallback { flags |= (1 << 3) } - request = network.request(Api.functions.photos.uploadProfilePhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp, videoEmojiMarkup: videoEmojiMarkup)) + if let _ = videoEmojiMarkup { + flags |= (1 << 4) + } + request = network.request(Api.functions.photos.uploadProfilePhoto(flags: flags, file: photoFile, video: videoFile, videoStartTs: videoStartTimestamp, videoEmojiMarkup: videoEmojiMarkup)) } else if let inputUser = apiInputUser(peer) { if let customPeerPhotoMode = customPeerPhotoMode { switch customPeerPhotoMode { @@ -184,8 +205,10 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state flags |= (1 << 4) } } - - request = network.request(Api.functions.photos.uploadContactProfilePhoto(flags: flags, userId: inputUser, file: file, video: videoFile, videoStartTs: videoStartTimestamp)) + if let _ = videoEmojiMarkup { + flags |= (1 << 5) + } + request = network.request(Api.functions.photos.uploadContactProfilePhoto(flags: flags, userId: inputUser, file: file, video: videoFile, videoStartTs: videoStartTimestamp, videoEmojiMarkup: videoEmojiMarkup)) } else { request = .complete() } @@ -273,6 +296,14 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state return nil } } + } else if peer.id == accountPeerId && customPeerPhotoMode == nil { + transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in + if let cachedPeerData = cachedPeerData as? CachedUserData { + return cachedPeerData.withUpdatedPhoto(.known(image)) + } else { + return nil + } + } } } return (.complete(representations), photoResult.resource, videoResult?.resource) @@ -287,17 +318,10 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state } } - var videoEmojiMarkup: Api.VideoSize? - if let markup = markup { - switch markup { - case let .emoji(fileId, backgroundColors): - videoEmojiMarkup = .videoSizeEmojiMarkup(emojiId: fileId, backgroundColors: backgroundColors) - case let .sticker(packReference, fileId, backgroundColors): - videoEmojiMarkup = .videoSizeStickerMarkup(stickerset: packReference.apiInputStickerSet, stickerId: fileId, backgroundColors: backgroundColors) - } + if let _ = videoEmojiMarkup { flags |= (1 << 3) } - + let request: Signal if let peer = peer as? TelegramGroup { request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id._internalGetInt64Value(), photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp, videoEmojiMarkup: videoEmojiMarkup))) @@ -388,7 +412,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state request = network.request(Api.functions.photos.updateProfilePhoto(flags: flags, id: Api.InputPhoto.inputPhotoEmpty)) } else if let inputUser = apiInputUser(peer) { let flags: Int32 = (1 << 4) - request = network.request(Api.functions.photos.uploadContactProfilePhoto(flags: flags, userId: inputUser, file: nil, video: nil, videoStartTs: nil)) + request = network.request(Api.functions.photos.uploadContactProfilePhoto(flags: flags, userId: inputUser, file: nil, video: nil, videoStartTs: nil, videoEmojiMarkup: nil)) } else { request = .complete() } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index dbbc963c37..a6b412936b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -14,6 +14,7 @@ import GalleryUI import HierarchyTrackingLayer import WallpaperBackgroundNode import ChatControllerInteraction +import AvatarVideoNode private let timezoneOffset: Int32 = { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -446,20 +447,17 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { private let containerNode: ContextControllerSourceNode private let avatarNode: AvatarNode - private var videoNode: UniversalVideoNode? - - private var videoContent: NativeVideoContent? - private let playbackStartDisposable = MetaDisposable() + private var avatarVideoNode: AvatarVideoNode? + 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.avatarVideoNode?.resetPlayback() } self.updateVideoVisibility() } @@ -507,7 +505,6 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { deinit { self.cachedDataDisposable.dispose() - self.playbackStartDisposable.dispose() } func setCustomLetters(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, letters: [String], emptyColor: UIColor) { @@ -531,16 +528,35 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { guard let strongSelf = self else { return } - let cachedPeerData = peerView.cachedData - if let cachedPeerData = cachedPeerData as? CachedUserData, case let .known(maybePhoto) = cachedPeerData.photo { - if let photo = maybePhoto, 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, "\(Int32.random(in: 0 ..< Int32.max))"), userLocation: .other, 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, storeAfterDownload: nil) - if videoContent.id != strongSelf.videoContent?.id { - strongSelf.videoNode?.removeFromSupernode() - strongSelf.videoContent = videoContent + + let cachedPeerData = peerView.cachedData as? CachedUserData + var personalPhoto: TelegramMediaImage? + var profilePhoto: TelegramMediaImage? + var isKnown = false + + if let cachedPeerData = cachedPeerData { + if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto { + personalPhoto = maybePersonalPhoto + isKnown = true + } + if case let .known(maybePhoto) = cachedPeerData.photo { + profilePhoto = maybePhoto + isKnown = true + } + } + + if isKnown { + let photo = personalPhoto ?? profilePhoto + if let photo = photo, !photo.videoRepresentations.isEmpty || photo.emojiMarkup != nil { + let videoNode: AvatarVideoNode + if let current = strongSelf.avatarVideoNode { + videoNode = current + } else { + videoNode = AvatarVideoNode(context: context) + strongSelf.avatarNode.addSubnode(videoNode) + strongSelf.avatarVideoNode = videoNode } + videoNode.update(peer: EnginePeer(peer), photo: photo, size: CGSize(width: 38.0, height: 38.0)) if strongSelf.hierarchyTrackingLayer == nil { let hierarchyTrackingLayer = HierarchyTrackingLayer() @@ -561,23 +577,65 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { strongSelf.layer.addSublayer(hierarchyTrackingLayer) } } else { - strongSelf.videoContent = nil - + if let avatarVideoNode = strongSelf.avatarVideoNode { + avatarVideoNode.removeFromSupernode() + strongSelf.avatarVideoNode = nil + } strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() strongSelf.hierarchyTrackingLayer = nil } - strongSelf.updateVideoVisibility() } else { let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() } + + + + +// let cachedPeerData = peerView.cachedData +// if let cachedPeerData = cachedPeerData as? CachedUserData, case let .known(maybePhoto) = cachedPeerData.photo { +// if let photo = maybePhoto, 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, "\(Int32.random(in: 0 ..< Int32.max))"), userLocation: .other, 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, storeAfterDownload: nil) +// 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 { })) } else { self.cachedDataDisposable.set(nil) - self.videoContent = nil - self.videoNode?.removeFromSupernode() - self.videoNode = nil + self.avatarVideoNode?.removeFromSupernode() + self.avatarVideoNode = nil self.hierarchyTrackingLayer?.removeFromSuperlayer() self.hierarchyTrackingLayer = nil @@ -659,74 +717,12 @@ 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 - Queue.mainQueue().async { - 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 + self.avatarVideoNode?.updateVisibility(isVisible) + + if let videoNode = self.avatarVideoNode { + videoNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: self.avatarNode.frame.size.width / 2.0, transition: .immediate) + videoNode.frame = self.avatarNode.bounds } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index ce567f5da3..f8484c2f9b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -6919,11 +6919,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }) } } else if case .custom = mode { - signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) }) } else if case .suggest = mode { - signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in + signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) }) } else { @@ -7030,85 +7030,109 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue } + var markup: UploadPeerPhotoMarkup? = nil + if let fileId = adjustments?.documentId, let backgroundColors = adjustments?.colors as? [Int32], fileId != 0 { + if let packId = adjustments?.stickerPackId, let accessHash = adjustments?.stickerPackAccessHash, packId != 0 { + markup = .sticker(packReference: .id(id: packId, accessHash: accessHash), fileId: fileId, backgroundColors: backgroundColors) + } else { + markup = .emoji(fileId: fileId, backgroundColors: backgroundColors) + } + } + + var uploadVideo = true + if let _ = markup { + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue { + uploadVideo = true + } else { + uploadVideo = false + } + } + let account = self.context.account let context = self.context - let signal = Signal { [weak self] subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(account: account, adjustments: adjustments) - } else { - return nil - } - } - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, watcher: nil, entityRenderer: entityRenderer)! + + let videoResource: Signal + if uploadVideo { + videoResource = Signal { [weak self] subscriber in + let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in + if let paintingData = adjustments.paintingData, paintingData.hasAnimation { + return LegacyPaintEntityRenderer(account: account, adjustments: adjustments) } else { - return SSignal.single(nil) - } - }) - - } else if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else { - signal = SSignal.complete() - } - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - } - - if let timestamp = videoStartTimestamp { - videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - } - account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - subscriber.putNext(resource) - } - } - subscriber.putCompletion() - } else if let strongSelf = self, let progress = next as? NSNumber { - Queue.mainQueue().async { - strongSelf.state = strongSelf.state.withAvatarUploadProgress(CGFloat(progress.floatValue * 0.45)) - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) - } + return nil } } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() + let uploadInterface = LegacyLiveUploadInterface(context: context) + let signal: SSignal + if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + let durationSignal: SSignal = SSignal(generator: { subscriber in + let disposable = (entityRenderer.duration()).start(next: { duration in + subscriber.putNext(duration) + subscriber.putCompletion() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) + signal = durationSignal.map(toSignal: { duration -> SSignal in + if let duration = duration as? Double { + return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, watcher: nil, entityRenderer: entityRenderer)! + } else { + return SSignal.single(nil) + } + }) + + } else if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! + } else { + signal = SSignal.complete() + } + + let signalDisposable = signal.start(next: { next in + if let result = next as? TGMediaVideoConversionResult { + if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { + account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + } + + if let timestamp = videoStartTimestamp { + videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) + } + + var value = stat() + if stat(result.fileURL.path, &value) == 0 { + if let data = try? Data(contentsOf: result.fileURL) { + let resource: TelegramMediaResource + if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { + resource = LocalFileMediaResource(fileId: liveUploadData.id) + } else { + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + } + account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + subscriber.putNext(resource) + } + } + subscriber.putCompletion() + } else if let strongSelf = self, let progress = next as? NSNumber { + Queue.mainQueue().async { + strongSelf.state = strongSelf.state.withAvatarUploadProgress(CGFloat(progress.floatValue * 0.45)) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + } + } + }, error: { _ in + }, completed: nil) + + let disposable = ActionDisposable { + signalDisposable?.dispose() + } + + return ActionDisposable { + disposable.dispose() + } } + } else { + videoResource = .single(nil) } var dismissStatus: (() -> Void)? @@ -7131,17 +7155,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let peerId = self.peerId let isSettings = self.isSettings - self.updateAvatarDisposable.set((signal + self.updateAvatarDisposable.set((videoResource |> mapToSignal { videoResource -> Signal in - var markup: UploadPeerPhotoMarkup? = nil if isSettings { - if let fileId = adjustments?.documentId, let backgroundColors = adjustments?.colors as? [Int32], fileId != 0 { - if let packId = adjustments?.stickerPackId, let accessHash = adjustments?.stickerPackAccessHash, packId != 0 { - markup = .sticker(packReference: .id(id: packId, accessHash: accessHash), fileId: fileId, backgroundColors: backgroundColors) - } else { - markup = .emoji(fileId: fileId, backgroundColors: backgroundColors) - } - } if case .fallback = mode { return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) @@ -7152,15 +7168,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }) } } else if case .custom = mode { - return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } else if case .suggest = mode { - return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } else { - return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } @@ -7418,7 +7434,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let postbox = strongSelf.context.account.postbox let signal: Signal if case .custom = mode { - signal = strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + signal = strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) }) } else if case .fallback = mode { @@ -8507,7 +8523,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate guard let strongSelf = self else { return } - strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in mapResourceToAvatarSizes(postbox: strongSelf.context.account.postbox, resource: resource, representations: representations) }) |> deliverOnMainQueue).start(next: { [weak self] _ in