diff --git a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift index 2038bbed6c..93eb8c659b 100644 --- a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift +++ b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift @@ -272,8 +272,8 @@ private final class VideoStickerFrameSourceCache { private let useCache = true -public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?) -> AnimatedStickerFrameSource? { - return VideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: cachePathPrefix) +public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool) -> AnimatedStickerFrameSource? { + return VideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: cachePathPrefix, unpremultiplyAlpha: unpremultiplyAlpha) } final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource { @@ -293,7 +293,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource { return self.currentFrame % self.frameCount } - init?(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?) { + init?(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool = true) { self.queue = queue self.path = path self.width = width @@ -310,7 +310,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource { self.frameRate = Int(cache.frameRate) self.frameCount = Int(cache.frameCount) } else { - let source = SoftwareVideoSource(path: path, hintVP9: true) + let source = SoftwareVideoSource(path: path, hintVP9: true, unpremultiplyAlpha: unpremultiplyAlpha) self.source = source self.frameRate = min(30, source.getFramerate()) self.frameCount = 0 diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index bc247cb3f5..d79143102f 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -125,7 +125,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { } } - public func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?, forceARGB: Bool = false) -> MediaTrackFrame? { + public func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true) -> MediaTrackFrame? { if self.isError { return nil } @@ -145,7 +145,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { if let ptsOffset = ptsOffset { pts = CMTimeAdd(pts, ptsOffset) } - return convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: frame.duration, forceARGB: forceARGB) + return convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: frame.duration, forceARGB: forceARGB, unpremultiplyAlpha: unpremultiplyAlpha) } } @@ -268,7 +268,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { return UIImage(cgImage: image, scale: 1.0, orientation: .up) } - private func convertVideoFrame(_ frame: FFMpegAVFrame, pts: CMTime, dts: CMTime, duration: CMTime, forceARGB: Bool = false) -> MediaTrackFrame? { + private func convertVideoFrame(_ frame: FFMpegAVFrame, pts: CMTime, dts: CMTime, duration: CMTime, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true) -> MediaTrackFrame? { if frame.data[0] == nil { return nil } @@ -335,7 +335,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { var base: UnsafeMutableRawPointer if pixelFormat == kCVPixelFormatType_32ARGB { let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) - decodeYUVAPlanesToRGBA(frame.data[0], Int32(frame.lineSize[0]), frame.data[1], Int32(frame.lineSize[1]), frame.data[2], Int32(frame.lineSize[2]), hasAlpha, frame.data[3], CVPixelBufferGetBaseAddress(pixelBuffer)?.assumingMemoryBound(to: UInt8.self), Int32(frame.width), Int32(frame.height), Int32(bytesPerRow)) + decodeYUVAPlanesToRGBA(frame.data[0], Int32(frame.lineSize[0]), frame.data[1], Int32(frame.lineSize[1]), frame.data[2], Int32(frame.lineSize[2]), hasAlpha, frame.data[3], CVPixelBufferGetBaseAddress(pixelBuffer)?.assumingMemoryBound(to: UInt8.self), Int32(frame.width), Int32(frame.height), Int32(bytesPerRow), unpremultiplyAlpha) } else { let srcPlaneSize = Int(frame.lineSize[1]) * Int(frame.height / 2) let uvPlaneSize = srcPlaneSize * 2 diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 3a1233245a..4fc9cead04 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -59,14 +59,16 @@ public final class SoftwareVideoSource { fileprivate let size: Int32 private let hintVP9: Bool + private let unpremultiplyAlpha: Bool private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat, Bool)] = [] private var hasReadToEnd: Bool = false - public init(path: String, hintVP9: Bool) { + public init(path: String, hintVP9: Bool, unpremultiplyAlpha: Bool) { let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals self.hintVP9 = hintVP9 + self.unpremultiplyAlpha = unpremultiplyAlpha var s = stat() stat(path, &s) @@ -228,7 +230,7 @@ public final class SoftwareVideoSource { if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 { ptsOffset = maxPts } - result = (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset, forceARGB: self.hintVP9), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) + result = (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset, forceARGB: self.hintVP9, unpremultiplyAlpha: self.unpremultiplyAlpha), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) } else { result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) } diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 30f277299e..57689a4d46 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -2537,6 +2537,9 @@ final class MessageHistoryTable: Table { } for mediaId in attribute.associatedMediaIds { if associatedMedia[mediaId] == nil { + if mediaId.id == 5364107552168613887 { + assert(true) + } if let media = self.getMedia(mediaId) { associatedMedia[mediaId] = media } diff --git a/submodules/Postbox/Sources/MessageMediaTable.swift b/submodules/Postbox/Sources/MessageMediaTable.swift index 70bb530632..40dd4be676 100644 --- a/submodules/Postbox/Sources/MessageMediaTable.swift +++ b/submodules/Postbox/Sources/MessageMediaTable.swift @@ -31,6 +31,10 @@ final class MessageMediaTable: Table { return key } + func exists(id: MediaId) -> Bool { + return self.valueBox.exists(self.table, key: self.key(id)) + } + func get(_ id: MediaId, embedded: (MessageIndex, MediaId) -> Media?) -> (MessageIndex?, Media)? { if let value = self.valueBox.get(self.table, key: self.key(id)) { var type: Int8 = 0 diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 105cc1eab1..1af2ada131 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -740,6 +740,14 @@ public final class Transaction { return Set() } + public func filterStoredMediaIds(namespace: MediaId.Namespace, ids: Set) -> Set { + assert(!self.disposed) + if let postbox = self.postbox { + return postbox.filterStoredMediaIds(namespace: namespace, ids: ids) + } + return Set() + } + public func storedMessageId(peerId: PeerId, namespace: MessageId.Namespace, timestamp: Int32) -> MessageId? { assert(!self.disposed) return self.postbox?.storedMessageId(peerId: peerId, namespace: namespace, timestamp: timestamp) @@ -2352,6 +2360,18 @@ final class PostboxImpl { return filteredIds } + fileprivate func filterStoredMediaIds(namespace: MediaId.Namespace, ids: Set) -> Set { + var filteredIds = Set() + + for id in ids { + if !self.mediaTable.exists(id: MediaId(namespace: namespace, id: id)) { + filteredIds.insert(id) + } + } + + return filteredIds + } + fileprivate func storedMessageId(peerId: PeerId, namespace: MessageId.Namespace, timestamp: Int32) -> MessageId? { if let id = self.messageHistoryTable.findMessageId(peerId: peerId, namespace: namespace, timestamp: timestamp), id.namespace == namespace { return id diff --git a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift index fd3ab89d89..0e7d528b9f 100644 --- a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift @@ -116,7 +116,7 @@ public final class SoftwareVideoLayerFrameManager { let size = fileSize(path) Logger.shared.log("SoftwareVideo", "loaded video from \(stringForResource(resource)) (file size: \(String(describing: size))") - let _ = strongSelf.source.swap(SoftwareVideoSource(path: path, hintVP9: strongSelf.hintVP9)) + let _ = strongSelf.source.swap(SoftwareVideoSource(path: path, hintVP9: strongSelf.hintVP9, unpremultiplyAlpha: true)) } })) } diff --git a/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift b/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift index 11a62ea8da..176236668a 100644 --- a/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift +++ b/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift @@ -263,7 +263,7 @@ public func cacheVideoStickerFrames(path: String, size: CGSize, cacheKey: String return Signal { subscriber in let cancelled = Atomic(value: false) - let source = SoftwareVideoSource(path: path, hintVP9: true) + let source = SoftwareVideoSource(path: path, hintVP9: true, unpremultiplyAlpha: true) let queue = ThreadPoolQueue(threadPool: softwareVideoWorkers) queue.addTask(ThreadPoolTask({ _ in diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index b6412810b3..f8f3b3b666 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1556,7 +1556,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } } if !channelPeers.isEmpty { - let resetSignal = resetChannels(network: network, peers: channelPeers, state: updatedState) + let resetSignal = resetChannels(postbox: postbox, network: network, peers: channelPeers, state: updatedState) |> map { resultState -> (AccountMutableState, Bool, Int32?) in return (resultState, true, nil) } @@ -1589,7 +1589,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } } } - return resolveAssociatedMessages(network: network, state: finalState) + return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) |> mapToSignal { resultingState -> Signal in return resolveMissingPeerChatInfos(network: network, state: resultingState) |> map { resultingState, resolveError -> AccountFinalState in @@ -1599,11 +1599,40 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } } +func extractEmojiFileIds(message: StoreMessage, fileIds: inout Set) { + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + for entity in attribute.entities { + switch entity.type { + case let .CustomEmoji(_, fileId): + fileIds.insert(fileId) + default: + break + } + } + } + } +} -private func resolveAssociatedMessages(network: Network, state: AccountMutableState) -> Signal { +private func messagesFromOperations(state: AccountMutableState) -> [StoreMessage] { + var messages: [StoreMessage] = [] + for operation in state.operations { + switch operation { + case let .AddMessages(messagesValue, _): + messages.append(contentsOf: messagesValue) + case let .EditMessage(_, message): + messages.append(message) + default: + break + } + } + return messages +} + +private func resolveAssociatedMessages(postbox: Postbox, network: Network, state: AccountMutableState) -> Signal { let missingMessageIds = state.referencedMessageIds.subtracting(state.storedMessages) if missingMessageIds.isEmpty { - return .single(state) + return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: state), result: state) } else { var missingPeers = false let _ = missingPeers @@ -1642,7 +1671,8 @@ private func resolveAssociatedMessages(network: Network, state: AccountMutableSt let fetchMessages = combineLatest(signals) - return fetchMessages |> map { results in + return fetchMessages + |> map { results in var updatedState = state for (messages, chats, users) in results { if !messages.isEmpty { @@ -1663,6 +1693,9 @@ private func resolveAssociatedMessages(network: Network, state: AccountMutableSt } return updatedState } + |> mapToSignal { updatedState -> Signal in + return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: updatedState), result: updatedState) + } } } @@ -1825,7 +1858,7 @@ func pollChannelOnce(postbox: Postbox, network: Network, peerId: PeerId, stateMa let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedMessageIds: Set(), initialStoredMessages: Set(), initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) return pollChannel(network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in - return resolveAssociatedMessages(network: network, state: finalState) + return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) |> mapToSignal { resultingState -> Signal in return resolveMissingPeerChatInfos(network: network, state: resultingState) |> map { resultingState, _ -> AccountFinalState in @@ -1879,7 +1912,7 @@ public func standalonePollChannelOnce(postbox: Postbox, network: Network, peerId let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedMessageIds: Set(), initialStoredMessages: Set(), initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) return pollChannel(network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in - return resolveAssociatedMessages(network: network, state: finalState) + return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) |> mapToSignal { resultingState -> Signal in return resolveMissingPeerChatInfos(network: network, state: resultingState) |> map { resultingState, _ -> AccountFinalState in @@ -1900,7 +1933,7 @@ func keepPollingChannel(postbox: Postbox, network: Network, peerId: PeerId, stat |> delay(1.0, queue: .concurrentDefaultQueue()) } -private func resetChannels(network: Network, peers: [Peer], state: AccountMutableState) -> Signal { +private func resetChannels(postbox: Postbox, network: Network, peers: [Peer], state: AccountMutableState) -> Signal { var inputPeers: [Api.InputDialogPeer] = [] for peer in peers { if let inputPeer = apiInputPeer(peer) { @@ -2052,7 +2085,7 @@ private func resetChannels(network: Network, peers: [Peer], state: AccountMutabl // TODO: delete messages later than top - return resolveAssociatedMessages(network: network, state: updatedState) + return resolveAssociatedMessages(postbox: postbox, network: network, state: updatedState) |> mapToSignal { resultingState -> Signal in return .single(resultingState) } diff --git a/submodules/TelegramCore/Sources/State/FetchChatList.swift b/submodules/TelegramCore/Sources/State/FetchChatList.swift index bcf6be922b..bc0d24b245 100644 --- a/submodules/TelegramCore/Sources/State/FetchChatList.swift +++ b/submodules/TelegramCore/Sources/State/FetchChatList.swift @@ -295,7 +295,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo } return combineLatest(folderSignals) - |> map { folders -> FetchedChatList? in + |> mapToSignal { folders -> Signal in var peers: [Peer] = [] var peerPresences: [PeerId: PeerPresence] = [:] var notificationSettings: [PeerId: PeerNotificationSettings] = [:] @@ -372,7 +372,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo } } - return FetchedChatList( + let result: FetchedChatList? = FetchedChatList( chatPeerIds: parsedRemoteChats.itemIds + (pinnedItemIds ?? []), peers: peers, peerPresences: peerPresences, @@ -390,6 +390,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo folderSummaries: folderSummaries, peerGroupIds: peerGroupIds ) + return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: storeMessages, result: result) } } } diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 8ca1cbe565..04ca364162 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -43,7 +43,58 @@ enum FetchMessageHistoryHoleSource { } } -func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHistoryHoleSource, peers: [PeerId: Peer], storeMessages: [StoreMessage], _ f: @escaping (Transaction, [Peer], [StoreMessage]) -> T) -> Signal { +func resolveUnknownEmojiFiles(postbox: Postbox, source: FetchMessageHistoryHoleSource, messages: [StoreMessage], result: T) -> Signal { + var fileIds = Set() + + for message in messages { + extractEmojiFileIds(message: message, fileIds: &fileIds) + } + + if fileIds.isEmpty { + return .single(result) + } else { + return postbox.transaction { transaction -> Set in + return transaction.filterStoredMediaIds(namespace: Namespaces.Media.CloudFile, ids: fileIds) + } + |> mapToSignal { unknownIds -> Signal in + if unknownIds.isEmpty { + return .single(result) + } else { + var signals: [Signal<[Api.Document]?, NoError>] = [] + var remainingIds = Array(unknownIds) + while !remainingIds.isEmpty { + let partIdCount = min(100, remainingIds.count) + let partIds = remainingIds.prefix(partIdCount) + remainingIds.removeFirst(partIdCount) + signals.append(source.request(Api.functions.messages.getCustomEmojiDocuments(documentId: Array(partIds))) + |> map(Optional.init) + |> `catch` { _ -> Signal<[Api.Document]?, NoError> in + return .single(nil) + }) + } + + return combineLatest(signals) + |> mapToSignal { documentSets -> Signal in + return postbox.transaction { transaction -> T in + for documentSet in documentSets { + if let documentSet = documentSet { + for document in documentSet { + if let file = telegramMediaFileFromApiDocument(document) { + transaction.storeMediaIfNotPresent(media: file) + } + } + } + } + + return result + } + } + } + } + } +} + +private func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHistoryHoleSource, peers: [PeerId: Peer], storeMessages: [StoreMessage], _ f: @escaping (Transaction, [Peer], [StoreMessage]) -> T) -> Signal { return postbox.transaction { transaction -> Signal in var storedIds = Set() var referencedIds = Set() @@ -60,7 +111,12 @@ func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHis referencedIds.subtract(transaction.filterStoredMessageIds(referencedIds)) if referencedIds.isEmpty { - return .single(f(transaction, [], [])) + return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages, result: Void()) + |> mapToSignal { _ -> Signal in + return postbox.transaction { transaction -> T in + return f(transaction, [], []) + } + } } else { var signals: [Signal<([Api.Message], [Api.Chat], [Api.User]), NoError>] = [] for (peerId, messageIds) in messagesIdsGroupedByPeerId(referencedIds) { @@ -117,8 +173,12 @@ func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHis additionalPeers.append(TelegramUser(user: user)) } } - return postbox.transaction { transaction -> T in - return f(transaction, additionalPeers, additionalMessages) + + return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages + additionalMessages, result: Void()) + |> mapToSignal { _ -> Signal in + return postbox.transaction { transaction -> T in + return f(transaction, additionalPeers, additionalMessages) + } } } } @@ -528,7 +588,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH } } - return withResolvedAssociatedMessages(postbox: postbox, source: source, peers: Dictionary(peers.map({ ($0.id, $0) }), uniquingKeysWith: { lhs, _ in lhs }), storeMessages: storeMessages, { transaction, additionalPeers, additionalMessages -> FetchMessageHistoryHoleResult in + return withResolvedAssociatedMessages(postbox: postbox, source: source, peers: Dictionary(peers.map({ ($0.id, $0) }), uniquingKeysWith: { lhs, _ in lhs }), storeMessages: storeMessages, { transaction, additionalPeers, additionalMessages -> FetchMessageHistoryHoleResult? in let _ = transaction.addMessages(storeMessages, location: .Random) let _ = transaction.addMessages(additionalMessages, location: .Random) var filledRange: ClosedRange @@ -623,13 +683,14 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH print("fetchMessageHistoryHole for \(peerInput) space \(space) done") - return FetchMessageHistoryHoleResult( + let result = FetchMessageHistoryHoleResult( removedIndices: IndexSet(integersIn: Int(filledRange.lowerBound) ... Int(filledRange.upperBound)), strictRemovedIndices: strictFilledIndices, actualPeerId: storeMessages.first?.id.peerId, actualThreadId: storeMessages.first?.threadId, ids: fullIds ) + return result }) } } @@ -665,7 +726,7 @@ func fetchChatListHole(postbox: Postbox, network: Network, accountPeerId: PeerId } |> ignoreValues } - return withResolvedAssociatedMessages(postbox: postbox, source: .network(network), peers: Dictionary(fetchedChats.peers.map({ ($0.id, $0) }), uniquingKeysWith: { lhs, _ in lhs }), storeMessages: fetchedChats.storeMessages, { transaction, additionalPeers, additionalMessages in + return withResolvedAssociatedMessages(postbox: postbox, source: .network(network), peers: Dictionary(fetchedChats.peers.map({ ($0.id, $0) }), uniquingKeysWith: { lhs, _ in lhs }), storeMessages: fetchedChats.storeMessages, { transaction, additionalPeers, additionalMessages -> Void in updatePeers(transaction: transaction, peers: fetchedChats.peers + additionalPeers, update: { _, updated -> Peer in return updated }) @@ -716,8 +777,6 @@ func fetchChatListHole(postbox: Postbox, network: Network, accountPeerId: PeerId for (groupId, summary) in fetchedChats.folderSummaries { transaction.resetPeerGroupSummary(groupId: groupId, namespace: Namespaces.Message.Cloud, summary: summary) } - - return }) |> ignoreValues } diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 8093e31732..081ab6a77e 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -235,10 +235,12 @@ private final class AnimationCacheItemWriterInternal { private var frames: [FrameMetadata] = [] - private let dctQuality: Int + private let dctQualityLuma: Int + private let dctQualityChroma: Int init?(allocateTempFile: @escaping () -> String) { - self.dctQuality = 70 + self.dctQualityLuma = 70 + self.dctQualityChroma = 75 self.compressedPath = allocateTempFile() @@ -297,7 +299,7 @@ private final class AnimationCacheItemWriterInternal { if let current = self.currentDctData { dctData = current } else { - dctData = DctData(generatingTablesAtQuality: self.dctQuality) + dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma) self.currentDctData = dctData } @@ -433,12 +435,14 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { private var frames: [FrameMetadata] = [] - private let dctQuality: Int + private let dctQualityLuma: Int + private let dctQualityChroma: Int private let lock = Lock() init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { - self.dctQuality = 70 + self.dctQualityLuma = 70 + self.dctQualityChroma = 75 self.queue = queue self.compressedPath = allocateTempFile() @@ -511,7 +515,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { if let current = self.currentDctData { dctData = current } else { - dctData = DctData(generatingTablesAtQuality: self.dctQuality) + dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma) self.currentDctData = dctData } diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift index 1c833fc60e..a0caff07c1 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift @@ -173,11 +173,11 @@ final class DctData { self.chromaDct = ImageDCT(table: chromaTableData) } - init(generatingTablesAtQuality quality: Int) { - self.lumaTable = ImageDCTTable(quality: quality, isChroma: false) + init(generatingTablesAtQualityLuma lumaQuality: Int, chroma chromaQuality: Int) { + self.lumaTable = ImageDCTTable(quality: lumaQuality, isChroma: false) self.lumaDct = ImageDCT(table: self.lumaTable) - self.chromaTable = ImageDCTTable(quality: quality, isChroma: true) + self.chromaTable = ImageDCTTable(quality: chromaQuality, isChroma: true) self.chromaDct = ImageDCT(table: self.chromaTable) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 75ee7ec788..97ad8548ab 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -599,7 +599,8 @@ private final class GroupHeaderLayer: UIView { color = theme.chat.inputPanel.primaryTextColor needsTintText = false } else { - color = theme.chat.inputMediaPanel.stickersSectionTextColor.withMultipliedAlpha(0.1) + //color = theme.chat.inputMediaPanel.stickersSectionTextColor.withMultipliedAlpha(0.1) + color = UIColor(white: 1.0, alpha: 0.0) needsTintText = true } let subtitleColor = theme.chat.inputMediaPanel.stickersSectionTextColor.withMultipliedAlpha(0.1) @@ -1520,6 +1521,7 @@ public final class EmojiPagerContentComponent: Component { var itemFeaturedHeaderHeight: CGFloat var nativeItemSize: CGFloat let visibleItemSize: CGFloat + let playbackItemSize: CGFloat var horizontalSpacing: CGFloat var verticalSpacing: CGFloat var verticalGroupDefaultSpacing: CGFloat @@ -1543,6 +1545,7 @@ public final class EmojiPagerContentComponent: Component { case .compact: minItemsPerRow = 8 self.nativeItemSize = 36.0 + self.playbackItemSize = 48.0 self.verticalSpacing = 9.0 minSpacing = 9.0 self.itemDefaultHeaderHeight = 24.0 @@ -1552,6 +1555,7 @@ public final class EmojiPagerContentComponent: Component { case .detailed: minItemsPerRow = 5 self.nativeItemSize = 71.0 + self.playbackItemSize = 96.0 self.verticalSpacing = 2.0 minSpacing = 12.0 self.itemDefaultHeaderHeight = 24.0 @@ -1820,79 +1824,59 @@ public final class EmojiPagerContentComponent: Component { super.init() if let file = file { - if file.isAnimatedSticker || file.isVideoEmoji { - let loadAnimation: () -> Void = { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in - let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - - let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in - guard let result = result else { - return - } - - if file.isVideoEmoji { - cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) - } else if file.isAnimatedSticker { - guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { - writer.finish() - return - } - cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) - } else { - cacheStillSticker(path: result, width: Int(size.width), height: Int(size.height), writer: writer) - } - }) - - let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() - - return ActionDisposable { - dataDisposable.dispose() - fetchDisposable.dispose() - } - }) + let loadAnimation: () -> Void = { [weak self] in + guard let strongSelf = self else { + return } - if attemptSynchronousLoad { - if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { - self.updateDisplayPlaceholder(displayPlaceholder: true) - } + strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - loadAnimation() - } else { - let _ = renderer.loadFirstFrame(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in - loadAnimation() + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { + return + } - if !success { - guard let strongSelf = self else { + if file.isVideoEmoji { + cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) + } else if file.isAnimatedSticker { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() return } - - strongSelf.updateDisplayPlaceholder(displayPlaceholder: true) + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + } else { + cacheStillSticker(path: result, width: Int(size.width), height: Int(size.height), writer: writer) } }) - } - } else if let _ = file.dimensions { - let isSmall: Bool = false - self.disposable = (chatMessageSticker(account: context.account, file: file, small: isSmall, synchronousLoad: attemptSynchronousLoad)).start(next: { [weak self] resultTransform in - let boundingSize = CGSize(width: 93.0, height: 93.0) - let imageSize = boundingSize//dimensions.cgSize.aspectFitted(boundingSize) - if let image = resultTransform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - - strongSelf.contents = image.cgImage - } + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() } }) + } + + if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + self.updateDisplayPlaceholder(displayPlaceholder: true) + } - self.fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: isSmall)).start() + loadAnimation() + } else { + let _ = renderer.loadFirstFrame(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in + loadAnimation() + + if !success { + guard let strongSelf = self else { + return + } + + strongSelf.updateDisplayPlaceholder(displayPlaceholder: true) + } + }) } } else if let staticEmoji = staticEmoji { let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in @@ -1907,7 +1891,7 @@ public final class EmojiPagerContentComponent: Component { let string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(32.0 * scaleFactor)), textColor: .black) let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil) UIGraphicsPushContext(context) - string.draw(at: CGPoint(x: (scaledSize.width - boundingRect.width) / 2.0 + boundingRect.minX, y: (scaledSize.height - boundingRect.height) / 2.0 + boundingRect.minY)) + string.draw(at: CGPoint(x: floor((scaledSize.width - boundingRect.width) / 2.0 + boundingRect.minX), y: floor((scaledSize.height - boundingRect.height) / 2.0 + boundingRect.minY))) UIGraphicsPopContext() }) self.contents = image?.cgImage @@ -2120,8 +2104,8 @@ public final class EmojiPagerContentComponent: Component { } } - private let shimmerHostView: PortalSourceView - private let standaloneShimmerEffect: StandaloneShimmerEffect + private let shimmerHostView: PortalSourceView? + private let standaloneShimmerEffect: StandaloneShimmerEffect? private let backgroundView: BlurredBackgroundView private var vibrancyEffectView: UIVisualEffectView? @@ -2155,8 +2139,13 @@ public final class EmojiPagerContentComponent: Component { override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: nil) - self.shimmerHostView = PortalSourceView() - self.standaloneShimmerEffect = StandaloneShimmerEffect() + if ProcessInfo.processInfo.activeProcessorCount > 2 { + self.shimmerHostView = PortalSourceView() + self.standaloneShimmerEffect = StandaloneShimmerEffect() + } else { + self.shimmerHostView = nil + self.standaloneShimmerEffect = nil + } self.mirrorContentScrollView = UIView() self.mirrorContentScrollView.layer.anchorPoint = CGPoint() @@ -2170,13 +2159,15 @@ public final class EmojiPagerContentComponent: Component { self.addSubview(self.backgroundView) - self.shimmerHostView.alpha = 0.0 - self.addSubview(self.shimmerHostView) + if let shimmerHostView = self.shimmerHostView { + shimmerHostView.alpha = 0.0 + self.addSubview(shimmerHostView) + } self.boundsChangeTrackerLayer.opacity = 0.0 self.layer.addSublayer(self.boundsChangeTrackerLayer) self.boundsChangeTrackerLayer.didEnterHierarchy = { [weak self] in - self?.standaloneShimmerEffect.updateLayer() + self?.standaloneShimmerEffect?.updateLayer() } self.scrollView.delaysContentTouches = false @@ -3304,6 +3295,7 @@ public final class EmojiPagerContentComponent: Component { } let itemNativeFitSize = itemDimensions.fitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize)) let itemVisibleFitSize = itemDimensions.fitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize)) + let itemPlaybackSize = itemDimensions.fitted(CGSize(width: itemLayout.playbackItemSize, height: itemLayout.playbackItemSize)) var animateItemIn = false var updateItemLayerPlaceholder = false @@ -3326,7 +3318,7 @@ public final class EmojiPagerContentComponent: Component { renderer: component.animationRenderer, placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1), blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5), - pointSize: itemVisibleFitSize, + pointSize: item.staticEmoji == nil ? itemPlaybackSize : itemVisibleFitSize, onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in guard let strongSelf = self else { return @@ -3525,10 +3517,12 @@ public final class EmojiPagerContentComponent: Component { } private func updateShimmerIfNeeded() { - if self.placeholdersContainerView.subviews.isEmpty { - self.standaloneShimmerEffect.layer = nil - } else { - self.standaloneShimmerEffect.layer = self.shimmerHostView.layer + if let standaloneShimmerEffect = self.standaloneShimmerEffect, let shimmerHostView = self.shimmerHostView { + if self.placeholdersContainerView.subviews.isEmpty { + standaloneShimmerEffect.layer = nil + } else { + standaloneShimmerEffect.layer = shimmerHostView.layer + } } } @@ -3544,11 +3538,16 @@ public final class EmojiPagerContentComponent: Component { } if self.vibrancyEffectView == nil { - let blurEffect = UIBlurEffect(style: .extraLight) + let style: UIBlurEffect.Style + style = .extraLight + let blurEffect = UIBlurEffect(style: style) let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect) let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect) self.vibrancyEffectView = vibrancyEffectView self.backgroundView.addSubview(vibrancyEffectView) + for subview in vibrancyEffectView.subviews { + let _ = subview + } vibrancyEffectView.contentView.addSubview(self.mirrorContentScrollView) } @@ -3577,11 +3576,15 @@ public final class EmojiPagerContentComponent: Component { self.pagerEnvironment = pagerEnvironment - transition.setFrame(view: self.shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize)) + if let shimmerHostView = self.shimmerHostView { + transition.setFrame(view: shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } - let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08) - let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15) - self.standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor) + if let standaloneShimmerEffect = self.standaloneShimmerEffect { + let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08) + let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15) + standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor) + } var previousItemPositions: [ItemLayer.Key: CGPoint]? diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 3bc5e874cd..440c3a4787 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -449,7 +449,7 @@ public final class EntityKeyboardComponent: Component { if let file = itemGroup.items[0].file { topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, - isReorderable: true, + isReorderable: !itemGroup.isFeatured, content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: component.emojiContent.context, file: file, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 1921cbb023..5d6506e888 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -1449,6 +1449,8 @@ final class EntityKeyboardTopPanelComponent: Component { visibleBounds.origin.x -= 200.0 visibleBounds.size.width += 400.0 + let scale = max(0.01, self.visibilityFraction) + var validIds = Set() let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds) if !self.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex { @@ -1477,10 +1479,10 @@ final class EntityKeyboardTopPanelComponent: Component { containerSize: itemOuterFrame.size ) let itemFrame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) - /*if index == visibleItemRange.minIndex, !itemTransition.animation.isImmediate { - print("\(index): \(itemView.frame) -> \(itemFrame)") - }*/ itemTransition.setFrame(view: itemView, frame: itemFrame) + + transition.setSublayerTransform(view: itemView, transform: CATransform3DMakeScale(scale, scale, 1.0)) + transition.setAlpha(view: itemView, alpha: self.visibilityFraction) } } var removedIds: [AnyHashable] = [] @@ -1804,6 +1806,13 @@ final class EntityKeyboardTopPanelComponent: Component { self.highlightedIconBackgroundView.isHidden = true }*/ } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.visibilityFraction < 0.5 { + return nil + } + return super.hitTest(point, with: event) + } } func makeView() -> View { diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index aed0d0e445..8ffe7cf143 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -127,14 +127,14 @@ public final class TextNodeWithEntities { var found = false string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, stop in if let value = value as? ChatTextInputTextCustomEmojiAttribute, let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont { - let updatedSubstring = NSMutableAttributedString(string: "😀") + let updatedSubstring = NSMutableAttributedString(string: "&") let replacementRange = NSRange(location: 0, length: updatedSubstring.length) updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange) updatedSubstring.addAttribute(originalTextAttributeKey, value: string.attributedSubstring(from: range).string, range: replacementRange) - let itemSize = (font.pointSize * 24.0 / 17.0) * 0.5 + let itemSize = (font.pointSize * 24.0 / 17.0) let runDelegateData = RunDelegateData( ascent: font.ascender, diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift index c59db57746..80190fc36f 100644 --- a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift +++ b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift @@ -20,7 +20,7 @@ private func roundUp(_ numToRound: Int, multiple: Int) -> Int { public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) { writer.queue.async { - guard let frameSource = makeVideoStickerDirectFrameSource(queue: writer.queue, path: path, width: roundUp(width, multiple: 16), height: roundUp(height, multiple: 16), cachePathPrefix: nil) else { + guard let frameSource = makeVideoStickerDirectFrameSource(queue: writer.queue, path: path, width: roundUp(width, multiple: 16), height: roundUp(height, multiple: 16), cachePathPrefix: nil, unpremultiplyAlpha: false) else { return } let frameDuration = 1.0 / Double(frameSource.frameRate) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 5503ea1c4b..775821121c 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -560,7 +560,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - self.textInputPanelNode = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in + self.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in self?.interfaceInteraction?.presentController(controller, nil) }) self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index bc20d3b0d1..6decb7096e 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -86,7 +86,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> Signal { + static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool) -> Signal { let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in guard case let .user(user) = peer else { @@ -218,29 +218,31 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - for featuredEmojiPack in featuredEmojiPacks { - if installedCollectionIds.contains(featuredEmojiPack.info.id) { - continue - } - - for item in featuredEmojiPack.topItems { - let resultItem = EmojiPagerContentComponent.Item( - file: item.file, - staticEmoji: nil, - subgroupId: nil - ) - - let supergroupId = featuredEmojiPack.info.id - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium - if isPremiumLocked && isPremiumDisabled { + if !isStandalone { + for featuredEmojiPack in featuredEmojiPacks { + if installedCollectionIds.contains(featuredEmojiPack.info.id) { continue } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, items: [resultItem])) + + for item in featuredEmojiPack.topItems { + let resultItem = EmojiPagerContentComponent.Item( + file: item.file, + staticEmoji: nil, + subgroupId: nil + ) + + let supergroupId = featuredEmojiPack.info.id + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, items: [resultItem])) + } } } } @@ -301,7 +303,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationRenderer = MultiAnimationRendererImpl() //} - let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer) + let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers] @@ -1484,6 +1486,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { stickerContent?.inputInteractionHolder.inputInteraction = self.stickerInputInteraction self.currentInputData.emoji.inputInteractionHolder.inputInteraction = self.emojiInputInteraction + let startTime = CFAbsoluteTimeGetCurrent() + let entityKeyboardSize = self.entityKeyboardView.update( transition: mappedTransition, component: AnyComponent(EntityKeyboardComponent( @@ -1559,6 +1563,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { ) transition.updateFrame(view: self.entityKeyboardView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: entityKeyboardSize)) + let layoutTime = CFAbsoluteTimeGetCurrent() - startTime + if layoutTime > 0.1 { + #if DEBUG + print("EntityKeyboard layout in \(layoutTime * 1000.0) ms") + #endif + } + return (expandedHeight, 0.0) } @@ -1849,7 +1860,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer).start(next: { value in + let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true).start(next: { value in emojiComponent = value semaphore.signal() }) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index f56092af6e..a0a8432548 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -39,7 +39,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return editPanelNode } else { - let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat) + let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 033774677c..90a7d71ad8 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -297,7 +297,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState textInputPanelNode.context = context return (textInputPanelNode, nil) } else { - let panel = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentationContext: nil, presentController: { [weak interfaceInteraction] controller in + let panel = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: nil, presentController: { [weak interfaceInteraction] controller in interfaceInteraction?.presentController(controller, nil) }) diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index ed841436f1..a2ff3fdca7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -15,6 +15,9 @@ import PhotoResources import WebsiteType import ChatMessageInteractiveMediaBadge import GalleryData +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer private let buttonFont = Font.semibold(13.0) @@ -217,7 +220,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { final class ChatMessageAttachedContentNode: ASDisplayNode { private let lineNode: ASImageNode - private let textNode: TextNode + private let textNode: TextNodeWithEntities private let inlineImageNode: TransformImageNode private var contentImageNode: ChatMessageInteractiveMediaNode? private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode? @@ -239,8 +242,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var visibility: ListViewItemNodeVisibility = .none { didSet { - self.contentImageNode?.visibility = self.visibility != .none - self.contentInstantVideoNode?.visibility = self.visibility != .none + if oldValue != self.visibility { + self.contentImageNode?.visibility = self.visibility != .none + self.contentInstantVideoNode?.visibility = self.visibility != .none + + switch self.visibility { + case .none: + self.textNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.textNode.visibilityRect = subRect + } + } } } @@ -250,11 +265,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { self.lineNode.displaysAsynchronously = false self.lineNode.displayWithoutProcessing = true - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - self.textNode.contentsScale = UIScreenScale - self.textNode.contentMode = .topLeft + self.textNode = TextNodeWithEntities() + self.textNode.textNode.isUserInteractionEnabled = false + self.textNode.textNode.displaysAsynchronously = false + self.textNode.textNode.contentsScale = UIScreenScale + self.textNode.textNode.contentMode = .topLeft self.inlineImageNode = TransformImageNode() self.inlineImageNode.contentAnimations = [.subsequentUpdates] @@ -266,13 +281,13 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { super.init() self.addSubnode(self.lineNode) - self.addSubnode(self.textNode) + self.addSubnode(self.textNode.textNode) self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { - let textAsyncLayout = TextNode.asyncLayout(self.textNode) + func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let textAsyncLayout = TextNodeWithEntities.asyncLayout(self.textNode) let currentImage = self.media as? TelegramMediaImage let imageLayout = self.inlineImageNode.asyncLayout() let statusLayout = self.statusNode.asyncLayout() @@ -284,7 +299,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode - return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize in + return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in let isPreview = presentationData.isPreview let fontSize: CGFloat if message.adAttribute != nil { @@ -847,9 +862,24 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { animation.animator.updateFrame(layer: strongSelf.lineNode.layer, frame: CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)), completion: nil) strongSelf.lineNode.isHidden = !displayLine - strongSelf.textNode.displaysAsynchronously = !isPreview + strongSelf.textNode.textNode.displaysAsynchronously = !isPreview - let _ = textApply() + let _ = textApply(TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: messageTheme.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads + )) + switch strongSelf.visibility { + case .none: + strongSelf.textNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + strongSelf.textNode.visibilityRect = subRect + } if let imageFrame = imageFrame { if let updateImageSignal = updateInlineImageSignal { @@ -976,9 +1006,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { textVerticalOffset = contentMediaHeight + 7.0 } - strongSelf.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) + strongSelf.textNode.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) if let statusSizeAndApply = statusSizeAndApply { - var statusFrame = CGRect(origin: CGPoint(x: strongSelf.textNode.frame.minX, y: strongSelf.textNode.frame.maxY), size: statusSizeAndApply.0) + var statusFrame = CGRect(origin: CGPoint(x: strongSelf.textNode.textNode.frame.minX, y: strongSelf.textNode.textNode.frame.maxY), size: statusSizeAndApply.0) if let imageFrame = imageFrame { if statusFrame.maxY < imageFrame.maxY + 10.0 { statusFrame.origin.y = max(statusFrame.minY, imageFrame.maxY + 2.0) @@ -1067,11 +1097,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - let textNodeFrame = self.textNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let textNodeFrame = self.textNode.textNode.frame + if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true - if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } return .url(url: url, concealed: concealed) @@ -1095,8 +1125,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let context = self.context, let message = self.message, let theme = self.theme { var rects: [CGRect]? if let point = point { - let textNodeFrame = self.textNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let textNodeFrame = self.textNode.textNode.frame + if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ TelegramTextAttributes.URL, TelegramTextAttributes.PeerMention, @@ -1107,7 +1137,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { - rects = self.textNode.attributeRects(name: name, at: index) + rects = self.textNode.textNode.attributeRects(name: name, at: index) break } } @@ -1121,9 +1151,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else { linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor) self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } - linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.frame = self.textNode.textNode.frame linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = self.linkHighlightingNode { self.linkHighlightingNode = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift index 5d52b3fc0b..9f4ef55e34 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -9,6 +9,12 @@ import TelegramCore final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode + override var visibility: ListViewItemNodeVisibility { + didSet { + self.contentNode.visibility = visibility + } + } + required init() { self.contentNode = ChatMessageAttachedContentNode() @@ -43,7 +49,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble } let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift index 1118997e9c..facb0a018e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift @@ -9,6 +9,12 @@ import TelegramCore final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode + override var visibility: ListViewItemNodeVisibility { + didSet { + self.contentNode.visibility = visibility + } + } + required init() { self.contentNode = ChatMessageAttachedContentNode() @@ -38,7 +44,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent let text: String = item.message.text let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift index cbfb47a4ff..364766ccbe 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift @@ -9,6 +9,12 @@ import TelegramCore final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode + override var visibility: ListViewItemNodeVisibility { + didSet { + self.contentNode.visibility = visibility + } + } + required init() { self.contentNode = ChatMessageAttachedContentNode() @@ -43,7 +49,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont } let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift index b9630d689f..35f51b625f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift @@ -13,7 +13,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { override var visibility: ListViewItemNodeVisibility { didSet { - self.contentNode.visibility = self.visibility + self.contentNode.visibility = visibility } } @@ -77,7 +77,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(id: item.message.id.peerId), title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(id: item.message.id.peerId), title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift index 4f900a526b..2dd754cc31 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -73,7 +73,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, preparePosition, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift index 06cecfc2fd..d6e97e397e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift @@ -15,6 +15,9 @@ import PhotoResources import TelegramStringFormatting import TextFormat import InvisibleInkDustNode +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer public final class ChatMessageNotificationItem: NotificationItem { let context: AccountContext @@ -69,7 +72,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { private let avatarNode: AvatarNode private let titleIconNode: ASImageNode private let titleNode: TextNode - private let textNode: TextNode + private let textNode: TextNodeWithEntities private var dustNode: InvisibleInkDustNode? private let imageNode: TransformImageNode @@ -79,6 +82,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { private var compact: Bool? private var validLayout: CGFloat? + private var animationCache: AnimationCache? + private var multiAnimationRenderer: MultiAnimationRenderer? + override init() { self.avatarNode = AvatarNode(font: avatarFont) @@ -90,8 +96,8 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { self.titleIconNode.displayWithoutProcessing = true self.titleIconNode.displaysAsynchronously = false - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false + self.textNode = TextNodeWithEntities() + self.textNode.textNode.isUserInteractionEnabled = false self.imageNode = TransformImageNode() @@ -100,12 +106,20 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { self.addSubnode(self.avatarNode) self.addSubnode(self.titleIconNode) self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) + self.addSubnode(self.textNode.textNode) self.addSubnode(self.imageNode) } func setupItem(_ item: ChatMessageNotificationItem, compact: Bool) { self.item = item + + if self.animationCache == nil { + self.animationCache = AnimationCacheImpl(basePath: item.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.multiAnimationRenderer = MultiAnimationRendererImpl() + } + self.compact = compact if compact { self.avatarNode.font = compactAvatarFont @@ -190,6 +204,8 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { messageEntities = message.textEntitiesAttribute?.entities.filter { entity in if case .Spoiler = entity.type { return true + } else if case .CustomEmoji = entity.type { + return true } else { return false } @@ -384,10 +400,24 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: self.titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - titleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let _ = titleApply() - let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: self.textAttributedText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let _ = titleApply() - let _ = textApply() + + if let item = self.item, let cache = self.animationCache, let renderer = self.multiAnimationRenderer { + let theme = item.context.sharedContext.currentPresentationData.with({ $0 }).theme + let _ = textApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: cache, + renderer: renderer, + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: false + )) + } else { + let _ = textApply(nil) + } + + self.textNode.visibilityRect = CGRect.infinite let textSpacing: CGFloat = 1.0 @@ -399,7 +429,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size) - transition.updateFrame(node: self.textNode, frame: textFrame) + transition.updateFrame(node: self.textNode.textNode, frame: textFrame) transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 10.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize)) @@ -411,7 +441,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { dustNode = InvisibleInkDustNode(textNode: nil) dustNode.isUserInteractionEnabled = false self.dustNode = dustNode - self.insertSubnode(dustNode, aboveSubnode: self.textNode) + self.insertSubnode(dustNode, aboveSubnode: self.textNode.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) dustNode.update(size: dustNode.frame.size, color: presentationData.theme.inAppNotification.primaryTextColor, textColor: presentationData.theme.inAppNotification.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index f7e60862b9..3a38fa9ecb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -373,7 +373,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { displayLine = false } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index f021309a7f..d0c9788f9c 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -588,7 +588,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { + init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState var hasSpoilers = true @@ -652,6 +652,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { super.init() + self.context = context + self.addSubnode(self.clippingNode) self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in @@ -1109,9 +1111,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) if let textInputNode = self.textInputNode { - if let text = textInputNode.attributedText?.string { + if let text = textInputNode.attributedText { let range = textInputNode.selectedRange - textInputNode.attributedText = NSAttributedString(string: text, font: Font.regular(baseFontSize), textColor: textColor) + let updatedText = NSMutableAttributedString(attributedString: text) + updatedText.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(location: 0, length: updatedText.length)) + textInputNode.attributedText = updatedText textInputNode.selectedRange = range } textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index 70af8117f2..9c53911751 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -13,6 +13,10 @@ import RadialStatusNode import PhotoResources import TelegramStringFormatting import ChatPresentationInterfaceState +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import TextFormat final class EditAccessoryPanelNode: AccessoryPanelNode { let dateTimeFormat: PresentationDateTimeFormat @@ -22,7 +26,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { let lineNode: ASImageNode let iconNode: ASImageNode let titleNode: ImmediateTextNode - let textNode: ImmediateTextNode + let textNode: ImmediateTextNodeWithEntities let imageNode: TransformImageNode let dimNode: ASDisplayNode let drawIconNode: ASImageNode @@ -71,7 +75,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? - init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) { + init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { self.context = context self.messageId = messageId self.theme = theme @@ -99,10 +103,21 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.titleNode.maximumNumberOfLines = 1 self.titleNode.displaysAsynchronously = false - self.textNode = ImmediateTextNode() + self.textNode = ImmediateTextNodeWithEntities() self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false self.textNode.isUserInteractionEnabled = true + self.textNode.visibility = true + + if let animationCache = animationCache, let animationRenderer = animationRenderer { + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: false + ) + } self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] @@ -238,6 +253,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.isPhoto = isPhoto let isMedia: Bool + let isText: Bool if let message = message { var effectiveMessage = message if let currentEditMediaReference = self.currentEditMediaReference { @@ -247,11 +263,14 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId) { case .text: isMedia = false + isText = true default: isMedia = true + isText = false } } else { isMedia = false + isText = true } let canEditMedia: Bool @@ -268,7 +287,29 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { titleString = self.strings.Conversation_EditingMessagePanelTitle } self.titleNode.attributedText = NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor) + + let textFont = Font.regular(14.0) + let messageText: NSAttributedString + if isText, let message = message { + let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + switch entity.type { + case .Spoiler, .CustomEmoji: + return true + default: + return false + } + } + let textColor = self.theme.chat.inputPanel.primaryTextColor + if entities.count > 0 { + messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + } else { + messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor) + } + } else { + messageText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor) + } + + self.textNode.attributedText = messageText let headerString: String = titleString self.actionArea.accessibilityLabel = "\(headerString).\n\(text)" @@ -325,8 +366,10 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) } - if let text = self.textNode.attributedText?.string { - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + if let text = self.textNode.attributedText { + let mutableText = NSMutableAttributedString(attributedString: text) + mutableText.addAttribute(NSAttributedString.Key.foregroundColor, value: self.theme.chat.inputPanel.primaryTextColor, range: NSRange(location: 0, length: mutableText.length)) + self.textNode.attributedText = mutableText } if let (size, inset, interfaceState) = self.validLayout { diff --git a/submodules/YuvConversion/PublicHeaders/YuvConversion/YUV.h b/submodules/YuvConversion/PublicHeaders/YuvConversion/YUV.h index d47d16e3c5..60e3e5a2fd 100644 --- a/submodules/YuvConversion/PublicHeaders/YuvConversion/YUV.h +++ b/submodules/YuvConversion/PublicHeaders/YuvConversion/YUV.h @@ -4,4 +4,4 @@ void encodeRGBAToYUVA(uint8_t *yuva, uint8_t const *argb, int width, int height, void resizeAndEncodeRGBAToYUVA(uint8_t *yuva, uint8_t const *argb, int width, int height, int bytesPerRow, int originalWidth, int originalHeight, int originalBytesPerRow, bool unpremultiply); void decodeYUVAToRGBA(uint8_t const *yuva, uint8_t *argb, int width, int height, int bytesPerRow); -void decodeYUVAPlanesToRGBA(uint8_t const *srcYpData, int srcYpBytesPerRow, uint8_t const *srcCbData, int srcCbBytesPerRow, uint8_t const *srcCrData, int srcCrBytesPerRow, bool hasAlpha, uint8_t const *alphaData, uint8_t *argb, int width, int height, int bytesPerRow); +void decodeYUVAPlanesToRGBA(uint8_t const *srcYpData, int srcYpBytesPerRow, uint8_t const *srcCbData, int srcCbBytesPerRow, uint8_t const *srcCrData, int srcCrBytesPerRow, bool hasAlpha, uint8_t const *alphaData, uint8_t *argb, int width, int height, int bytesPerRow, bool unpremultiply); diff --git a/submodules/YuvConversion/Sources/YUV.m b/submodules/YuvConversion/Sources/YUV.m index e99f101d1c..e8d4609878 100644 --- a/submodules/YuvConversion/Sources/YUV.m +++ b/submodules/YuvConversion/Sources/YUV.m @@ -173,7 +173,7 @@ void decodeYUVAToRGBA(uint8_t const *yuva, uint8_t *argb, int width, int height, } } -void decodeYUVAPlanesToRGBA(uint8_t const *srcYpData, int srcYpBytesPerRow, uint8_t const *srcCbData, int srcCbBytesPerRow, uint8_t const *srcCrData, int srcCrBytesPerRow, bool hasAlpha, uint8_t const *alphaData, uint8_t *argb, int width, int height, int bytesPerRow) { +void decodeYUVAPlanesToRGBA(uint8_t const *srcYpData, int srcYpBytesPerRow, uint8_t const *srcCbData, int srcCbBytesPerRow, uint8_t const *srcCrData, int srcCrBytesPerRow, bool hasAlpha, uint8_t const *alphaData, uint8_t *argb, int width, int height, int bytesPerRow, bool unpremultiply) { static vImage_YpCbCrToARGB info; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -220,7 +220,11 @@ void decodeYUVAPlanesToRGBA(uint8_t const *srcYpData, int srcYpBytesPerRow, uint } uint8_t permuteMap[4] = {3, 2, 1, 0}; - error = vImageUnpremultiplyData_ARGB8888(&dest, &dest, kvImageDoNotTile); + if (unpremultiply) { + error = vImageUnpremultiplyData_ARGB8888(&dest, &dest, kvImageDoNotTile); + } else { + error = vImagePremultiplyData_ARGB8888(&dest, &dest, kvImageDoNotTile); + } error = vImagePermuteChannels_ARGB8888(&dest, &dest, permuteMap, kvImageDoNotTile); if (error != kvImageNoError) {