Emoji improvements

This commit is contained in:
Ali 2022-07-22 23:30:46 +02:00
parent 7b663a3445
commit 61b47ada27
34 changed files with 484 additions and 206 deletions

View File

@ -272,8 +272,8 @@ private final class VideoStickerFrameSourceCache {
private let useCache = true private let useCache = true
public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?) -> AnimatedStickerFrameSource? { 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) return VideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: cachePathPrefix, unpremultiplyAlpha: unpremultiplyAlpha)
} }
final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource { final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
@ -293,7 +293,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
return self.currentFrame % self.frameCount 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.queue = queue
self.path = path self.path = path
self.width = width self.width = width
@ -310,7 +310,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
self.frameRate = Int(cache.frameRate) self.frameRate = Int(cache.frameRate)
self.frameCount = Int(cache.frameCount) self.frameCount = Int(cache.frameCount)
} else { } else {
let source = SoftwareVideoSource(path: path, hintVP9: true) let source = SoftwareVideoSource(path: path, hintVP9: true, unpremultiplyAlpha: unpremultiplyAlpha)
self.source = source self.source = source
self.frameRate = min(30, source.getFramerate()) self.frameRate = min(30, source.getFramerate())
self.frameCount = 0 self.frameCount = 0

View File

@ -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 { if self.isError {
return nil return nil
} }
@ -145,7 +145,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
if let ptsOffset = ptsOffset { if let ptsOffset = ptsOffset {
pts = CMTimeAdd(pts, 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) 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 { if frame.data[0] == nil {
return nil return nil
} }
@ -335,7 +335,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
var base: UnsafeMutableRawPointer var base: UnsafeMutableRawPointer
if pixelFormat == kCVPixelFormatType_32ARGB { if pixelFormat == kCVPixelFormatType_32ARGB {
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) 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 { } else {
let srcPlaneSize = Int(frame.lineSize[1]) * Int(frame.height / 2) let srcPlaneSize = Int(frame.lineSize[1]) * Int(frame.height / 2)
let uvPlaneSize = srcPlaneSize * 2 let uvPlaneSize = srcPlaneSize * 2

View File

@ -59,14 +59,16 @@ public final class SoftwareVideoSource {
fileprivate let size: Int32 fileprivate let size: Int32
private let hintVP9: Bool private let hintVP9: Bool
private let unpremultiplyAlpha: Bool
private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat, Bool)] = [] private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat, Bool)] = []
private var hasReadToEnd: Bool = false private var hasReadToEnd: Bool = false
public init(path: String, hintVP9: Bool) { public init(path: String, hintVP9: Bool, unpremultiplyAlpha: Bool) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
self.hintVP9 = hintVP9 self.hintVP9 = hintVP9
self.unpremultiplyAlpha = unpremultiplyAlpha
var s = stat() var s = stat()
stat(path, &s) stat(path, &s)
@ -228,7 +230,7 @@ public final class SoftwareVideoSource {
if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 { if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 {
ptsOffset = maxPts 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 { } else {
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
} }

View File

@ -2537,6 +2537,9 @@ final class MessageHistoryTable: Table {
} }
for mediaId in attribute.associatedMediaIds { for mediaId in attribute.associatedMediaIds {
if associatedMedia[mediaId] == nil { if associatedMedia[mediaId] == nil {
if mediaId.id == 5364107552168613887 {
assert(true)
}
if let media = self.getMedia(mediaId) { if let media = self.getMedia(mediaId) {
associatedMedia[mediaId] = media associatedMedia[mediaId] = media
} }

View File

@ -31,6 +31,10 @@ final class MessageMediaTable: Table {
return key 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)? { func get(_ id: MediaId, embedded: (MessageIndex, MediaId) -> Media?) -> (MessageIndex?, Media)? {
if let value = self.valueBox.get(self.table, key: self.key(id)) { if let value = self.valueBox.get(self.table, key: self.key(id)) {
var type: Int8 = 0 var type: Int8 = 0

View File

@ -740,6 +740,14 @@ public final class Transaction {
return Set() return Set()
} }
public func filterStoredMediaIds(namespace: MediaId.Namespace, ids: Set<Int64>) -> Set<Int64> {
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? { public func storedMessageId(peerId: PeerId, namespace: MessageId.Namespace, timestamp: Int32) -> MessageId? {
assert(!self.disposed) assert(!self.disposed)
return self.postbox?.storedMessageId(peerId: peerId, namespace: namespace, timestamp: timestamp) return self.postbox?.storedMessageId(peerId: peerId, namespace: namespace, timestamp: timestamp)
@ -2352,6 +2360,18 @@ final class PostboxImpl {
return filteredIds return filteredIds
} }
fileprivate func filterStoredMediaIds(namespace: MediaId.Namespace, ids: Set<Int64>) -> Set<Int64> {
var filteredIds = Set<Int64>()
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? { 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 { if let id = self.messageHistoryTable.findMessageId(peerId: peerId, namespace: namespace, timestamp: timestamp), id.namespace == namespace {
return id return id

View File

@ -116,7 +116,7 @@ public final class SoftwareVideoLayerFrameManager {
let size = fileSize(path) let size = fileSize(path)
Logger.shared.log("SoftwareVideo", "loaded video from \(stringForResource(resource)) (file size: \(String(describing: size))") 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))
} }
})) }))
} }

View File

@ -263,7 +263,7 @@ public func cacheVideoStickerFrames(path: String, size: CGSize, cacheKey: String
return Signal { subscriber in return Signal { subscriber in
let cancelled = Atomic<Bool>(value: false) let cancelled = Atomic<Bool>(value: false)
let source = SoftwareVideoSource(path: path, hintVP9: true) let source = SoftwareVideoSource(path: path, hintVP9: true, unpremultiplyAlpha: true)
let queue = ThreadPoolQueue(threadPool: softwareVideoWorkers) let queue = ThreadPoolQueue(threadPool: softwareVideoWorkers)
queue.addTask(ThreadPoolTask({ _ in queue.addTask(ThreadPoolTask({ _ in

View File

@ -1556,7 +1556,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
} }
} }
if !channelPeers.isEmpty { 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 |> map { resultState -> (AccountMutableState, Bool, Int32?) in
return (resultState, true, nil) 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<AccountFinalState, NoError> in |> mapToSignal { resultingState -> Signal<AccountFinalState, NoError> in
return resolveMissingPeerChatInfos(network: network, state: resultingState) return resolveMissingPeerChatInfos(network: network, state: resultingState)
|> map { resultingState, resolveError -> AccountFinalState in |> map { resultingState, resolveError -> AccountFinalState in
@ -1599,11 +1599,40 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
} }
} }
func extractEmojiFileIds(message: StoreMessage, fileIds: inout Set<Int64>) {
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<AccountMutableState, NoError> { 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<AccountMutableState, NoError> {
let missingMessageIds = state.referencedMessageIds.subtracting(state.storedMessages) let missingMessageIds = state.referencedMessageIds.subtracting(state.storedMessages)
if missingMessageIds.isEmpty { if missingMessageIds.isEmpty {
return .single(state) return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: state), result: state)
} else { } else {
var missingPeers = false var missingPeers = false
let _ = missingPeers let _ = missingPeers
@ -1642,7 +1671,8 @@ private func resolveAssociatedMessages(network: Network, state: AccountMutableSt
let fetchMessages = combineLatest(signals) let fetchMessages = combineLatest(signals)
return fetchMessages |> map { results in return fetchMessages
|> map { results in
var updatedState = state var updatedState = state
for (messages, chats, users) in results { for (messages, chats, users) in results {
if !messages.isEmpty { if !messages.isEmpty {
@ -1663,6 +1693,9 @@ private func resolveAssociatedMessages(network: Network, state: AccountMutableSt
} }
return updatedState return updatedState
} }
|> mapToSignal { updatedState -> Signal<AccountMutableState, NoError> 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: [:]) 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) return pollChannel(network: network, peer: peer, state: initialState)
|> mapToSignal { (finalState, _, timeout) -> Signal<Int32, NoError> in |> mapToSignal { (finalState, _, timeout) -> Signal<Int32, NoError> in
return resolveAssociatedMessages(network: network, state: finalState) return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState)
|> mapToSignal { resultingState -> Signal<AccountFinalState, NoError> in |> mapToSignal { resultingState -> Signal<AccountFinalState, NoError> in
return resolveMissingPeerChatInfos(network: network, state: resultingState) return resolveMissingPeerChatInfos(network: network, state: resultingState)
|> map { resultingState, _ -> AccountFinalState in |> 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: [:]) 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) return pollChannel(network: network, peer: peer, state: initialState)
|> mapToSignal { (finalState, _, timeout) -> Signal<Never, NoError> in |> mapToSignal { (finalState, _, timeout) -> Signal<Never, NoError> in
return resolveAssociatedMessages(network: network, state: finalState) return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState)
|> mapToSignal { resultingState -> Signal<AccountFinalState, NoError> in |> mapToSignal { resultingState -> Signal<AccountFinalState, NoError> in
return resolveMissingPeerChatInfos(network: network, state: resultingState) return resolveMissingPeerChatInfos(network: network, state: resultingState)
|> map { resultingState, _ -> AccountFinalState in |> map { resultingState, _ -> AccountFinalState in
@ -1900,7 +1933,7 @@ func keepPollingChannel(postbox: Postbox, network: Network, peerId: PeerId, stat
|> delay(1.0, queue: .concurrentDefaultQueue()) |> delay(1.0, queue: .concurrentDefaultQueue())
} }
private func resetChannels(network: Network, peers: [Peer], state: AccountMutableState) -> Signal<AccountMutableState, NoError> { private func resetChannels(postbox: Postbox, network: Network, peers: [Peer], state: AccountMutableState) -> Signal<AccountMutableState, NoError> {
var inputPeers: [Api.InputDialogPeer] = [] var inputPeers: [Api.InputDialogPeer] = []
for peer in peers { for peer in peers {
if let inputPeer = apiInputPeer(peer) { if let inputPeer = apiInputPeer(peer) {
@ -2052,7 +2085,7 @@ private func resetChannels(network: Network, peers: [Peer], state: AccountMutabl
// TODO: delete messages later than top // TODO: delete messages later than top
return resolveAssociatedMessages(network: network, state: updatedState) return resolveAssociatedMessages(postbox: postbox, network: network, state: updatedState)
|> mapToSignal { resultingState -> Signal<AccountMutableState, NoError> in |> mapToSignal { resultingState -> Signal<AccountMutableState, NoError> in
return .single(resultingState) return .single(resultingState)
} }

View File

@ -295,7 +295,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo
} }
return combineLatest(folderSignals) return combineLatest(folderSignals)
|> map { folders -> FetchedChatList? in |> mapToSignal { folders -> Signal<FetchedChatList?, NoError> in
var peers: [Peer] = [] var peers: [Peer] = []
var peerPresences: [PeerId: PeerPresence] = [:] var peerPresences: [PeerId: PeerPresence] = [:]
var notificationSettings: [PeerId: PeerNotificationSettings] = [:] 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 ?? []), chatPeerIds: parsedRemoteChats.itemIds + (pinnedItemIds ?? []),
peers: peers, peers: peers,
peerPresences: peerPresences, peerPresences: peerPresences,
@ -390,6 +390,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo
folderSummaries: folderSummaries, folderSummaries: folderSummaries,
peerGroupIds: peerGroupIds peerGroupIds: peerGroupIds
) )
return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: storeMessages, result: result)
} }
} }
} }

View File

@ -43,7 +43,58 @@ enum FetchMessageHistoryHoleSource {
} }
} }
func withResolvedAssociatedMessages<T>(postbox: Postbox, source: FetchMessageHistoryHoleSource, peers: [PeerId: Peer], storeMessages: [StoreMessage], _ f: @escaping (Transaction, [Peer], [StoreMessage]) -> T) -> Signal<T, NoError> { func resolveUnknownEmojiFiles<T>(postbox: Postbox, source: FetchMessageHistoryHoleSource, messages: [StoreMessage], result: T) -> Signal<T, NoError> {
var fileIds = Set<Int64>()
for message in messages {
extractEmojiFileIds(message: message, fileIds: &fileIds)
}
if fileIds.isEmpty {
return .single(result)
} else {
return postbox.transaction { transaction -> Set<Int64> in
return transaction.filterStoredMediaIds(namespace: Namespaces.Media.CloudFile, ids: fileIds)
}
|> mapToSignal { unknownIds -> Signal<T, NoError> 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<T, NoError> 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<T>(postbox: Postbox, source: FetchMessageHistoryHoleSource, peers: [PeerId: Peer], storeMessages: [StoreMessage], _ f: @escaping (Transaction, [Peer], [StoreMessage]) -> T) -> Signal<T, NoError> {
return postbox.transaction { transaction -> Signal<T, NoError> in return postbox.transaction { transaction -> Signal<T, NoError> in
var storedIds = Set<MessageId>() var storedIds = Set<MessageId>()
var referencedIds = Set<MessageId>() var referencedIds = Set<MessageId>()
@ -60,7 +111,12 @@ func withResolvedAssociatedMessages<T>(postbox: Postbox, source: FetchMessageHis
referencedIds.subtract(transaction.filterStoredMessageIds(referencedIds)) referencedIds.subtract(transaction.filterStoredMessageIds(referencedIds))
if referencedIds.isEmpty { if referencedIds.isEmpty {
return .single(f(transaction, [], [])) return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages, result: Void())
|> mapToSignal { _ -> Signal<T, NoError> in
return postbox.transaction { transaction -> T in
return f(transaction, [], [])
}
}
} else { } else {
var signals: [Signal<([Api.Message], [Api.Chat], [Api.User]), NoError>] = [] var signals: [Signal<([Api.Message], [Api.Chat], [Api.User]), NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(referencedIds) { for (peerId, messageIds) in messagesIdsGroupedByPeerId(referencedIds) {
@ -117,8 +173,12 @@ func withResolvedAssociatedMessages<T>(postbox: Postbox, source: FetchMessageHis
additionalPeers.append(TelegramUser(user: user)) 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<T, NoError> 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(storeMessages, location: .Random)
let _ = transaction.addMessages(additionalMessages, location: .Random) let _ = transaction.addMessages(additionalMessages, location: .Random)
var filledRange: ClosedRange<MessageId.Id> var filledRange: ClosedRange<MessageId.Id>
@ -623,13 +683,14 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH
print("fetchMessageHistoryHole for \(peerInput) space \(space) done") print("fetchMessageHistoryHole for \(peerInput) space \(space) done")
return FetchMessageHistoryHoleResult( let result = FetchMessageHistoryHoleResult(
removedIndices: IndexSet(integersIn: Int(filledRange.lowerBound) ... Int(filledRange.upperBound)), removedIndices: IndexSet(integersIn: Int(filledRange.lowerBound) ... Int(filledRange.upperBound)),
strictRemovedIndices: strictFilledIndices, strictRemovedIndices: strictFilledIndices,
actualPeerId: storeMessages.first?.id.peerId, actualPeerId: storeMessages.first?.id.peerId,
actualThreadId: storeMessages.first?.threadId, actualThreadId: storeMessages.first?.threadId,
ids: fullIds ids: fullIds
) )
return result
}) })
} }
} }
@ -665,7 +726,7 @@ func fetchChatListHole(postbox: Postbox, network: Network, accountPeerId: PeerId
} }
|> ignoreValues |> 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 updatePeers(transaction: transaction, peers: fetchedChats.peers + additionalPeers, update: { _, updated -> Peer in
return updated return updated
}) })
@ -716,8 +777,6 @@ func fetchChatListHole(postbox: Postbox, network: Network, accountPeerId: PeerId
for (groupId, summary) in fetchedChats.folderSummaries { for (groupId, summary) in fetchedChats.folderSummaries {
transaction.resetPeerGroupSummary(groupId: groupId, namespace: Namespaces.Message.Cloud, summary: summary) transaction.resetPeerGroupSummary(groupId: groupId, namespace: Namespaces.Message.Cloud, summary: summary)
} }
return
}) })
|> ignoreValues |> ignoreValues
} }

View File

@ -235,10 +235,12 @@ private final class AnimationCacheItemWriterInternal {
private var frames: [FrameMetadata] = [] private var frames: [FrameMetadata] = []
private let dctQuality: Int private let dctQualityLuma: Int
private let dctQualityChroma: Int
init?(allocateTempFile: @escaping () -> String) { init?(allocateTempFile: @escaping () -> String) {
self.dctQuality = 70 self.dctQualityLuma = 70
self.dctQualityChroma = 75
self.compressedPath = allocateTempFile() self.compressedPath = allocateTempFile()
@ -297,7 +299,7 @@ private final class AnimationCacheItemWriterInternal {
if let current = self.currentDctData { if let current = self.currentDctData {
dctData = current dctData = current
} else { } else {
dctData = DctData(generatingTablesAtQuality: self.dctQuality) dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma)
self.currentDctData = dctData self.currentDctData = dctData
} }
@ -433,12 +435,14 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
private var frames: [FrameMetadata] = [] private var frames: [FrameMetadata] = []
private let dctQuality: Int private let dctQualityLuma: Int
private let dctQualityChroma: Int
private let lock = Lock() private let lock = Lock()
init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) {
self.dctQuality = 70 self.dctQualityLuma = 70
self.dctQualityChroma = 75
self.queue = queue self.queue = queue
self.compressedPath = allocateTempFile() self.compressedPath = allocateTempFile()
@ -511,7 +515,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
if let current = self.currentDctData { if let current = self.currentDctData {
dctData = current dctData = current
} else { } else {
dctData = DctData(generatingTablesAtQuality: self.dctQuality) dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma)
self.currentDctData = dctData self.currentDctData = dctData
} }

View File

@ -173,11 +173,11 @@ final class DctData {
self.chromaDct = ImageDCT(table: chromaTableData) self.chromaDct = ImageDCT(table: chromaTableData)
} }
init(generatingTablesAtQuality quality: Int) { init(generatingTablesAtQualityLuma lumaQuality: Int, chroma chromaQuality: Int) {
self.lumaTable = ImageDCTTable(quality: quality, isChroma: false) self.lumaTable = ImageDCTTable(quality: lumaQuality, isChroma: false)
self.lumaDct = ImageDCT(table: self.lumaTable) 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) self.chromaDct = ImageDCT(table: self.chromaTable)
} }
} }

View File

@ -599,7 +599,8 @@ private final class GroupHeaderLayer: UIView {
color = theme.chat.inputPanel.primaryTextColor color = theme.chat.inputPanel.primaryTextColor
needsTintText = false needsTintText = false
} else { } 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 needsTintText = true
} }
let subtitleColor = theme.chat.inputMediaPanel.stickersSectionTextColor.withMultipliedAlpha(0.1) let subtitleColor = theme.chat.inputMediaPanel.stickersSectionTextColor.withMultipliedAlpha(0.1)
@ -1520,6 +1521,7 @@ public final class EmojiPagerContentComponent: Component {
var itemFeaturedHeaderHeight: CGFloat var itemFeaturedHeaderHeight: CGFloat
var nativeItemSize: CGFloat var nativeItemSize: CGFloat
let visibleItemSize: CGFloat let visibleItemSize: CGFloat
let playbackItemSize: CGFloat
var horizontalSpacing: CGFloat var horizontalSpacing: CGFloat
var verticalSpacing: CGFloat var verticalSpacing: CGFloat
var verticalGroupDefaultSpacing: CGFloat var verticalGroupDefaultSpacing: CGFloat
@ -1543,6 +1545,7 @@ public final class EmojiPagerContentComponent: Component {
case .compact: case .compact:
minItemsPerRow = 8 minItemsPerRow = 8
self.nativeItemSize = 36.0 self.nativeItemSize = 36.0
self.playbackItemSize = 48.0
self.verticalSpacing = 9.0 self.verticalSpacing = 9.0
minSpacing = 9.0 minSpacing = 9.0
self.itemDefaultHeaderHeight = 24.0 self.itemDefaultHeaderHeight = 24.0
@ -1552,6 +1555,7 @@ public final class EmojiPagerContentComponent: Component {
case .detailed: case .detailed:
minItemsPerRow = 5 minItemsPerRow = 5
self.nativeItemSize = 71.0 self.nativeItemSize = 71.0
self.playbackItemSize = 96.0
self.verticalSpacing = 2.0 self.verticalSpacing = 2.0
minSpacing = 12.0 minSpacing = 12.0
self.itemDefaultHeaderHeight = 24.0 self.itemDefaultHeaderHeight = 24.0
@ -1820,79 +1824,59 @@ public final class EmojiPagerContentComponent: Component {
super.init() super.init()
if let file = file { if let file = file {
if file.isAnimatedSticker || file.isVideoEmoji { let loadAnimation: () -> Void = { [weak self] in
let loadAnimation: () -> Void = { [weak self] in guard let strongSelf = self else {
guard let strongSelf = self else { return
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()
}
})
} }
if attemptSynchronousLoad { strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in
if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
self.updateDisplayPlaceholder(displayPlaceholder: true)
}
loadAnimation() let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
} else { guard let result = result else {
let _ = renderer.loadFirstFrame(target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in return
loadAnimation() }
if !success { if file.isVideoEmoji {
guard let strongSelf = self else { 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 return
} }
cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer)
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true) } 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() { let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start()
Queue.mainQueue().async {
guard let strongSelf = self else { return ActionDisposable {
return dataDisposable.dispose()
} fetchDisposable.dispose()
strongSelf.contents = image.cgImage
}
} }
}) })
}
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 { } else if let staticEmoji = staticEmoji {
let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in 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 string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(32.0 * scaleFactor)), textColor: .black)
let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil) let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil)
UIGraphicsPushContext(context) 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() UIGraphicsPopContext()
}) })
self.contents = image?.cgImage self.contents = image?.cgImage
@ -2120,8 +2104,8 @@ public final class EmojiPagerContentComponent: Component {
} }
} }
private let shimmerHostView: PortalSourceView private let shimmerHostView: PortalSourceView?
private let standaloneShimmerEffect: StandaloneShimmerEffect private let standaloneShimmerEffect: StandaloneShimmerEffect?
private let backgroundView: BlurredBackgroundView private let backgroundView: BlurredBackgroundView
private var vibrancyEffectView: UIVisualEffectView? private var vibrancyEffectView: UIVisualEffectView?
@ -2155,8 +2139,13 @@ public final class EmojiPagerContentComponent: Component {
override init(frame: CGRect) { override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil) self.backgroundView = BlurredBackgroundView(color: nil)
self.shimmerHostView = PortalSourceView() if ProcessInfo.processInfo.activeProcessorCount > 2 {
self.standaloneShimmerEffect = StandaloneShimmerEffect() self.shimmerHostView = PortalSourceView()
self.standaloneShimmerEffect = StandaloneShimmerEffect()
} else {
self.shimmerHostView = nil
self.standaloneShimmerEffect = nil
}
self.mirrorContentScrollView = UIView() self.mirrorContentScrollView = UIView()
self.mirrorContentScrollView.layer.anchorPoint = CGPoint() self.mirrorContentScrollView.layer.anchorPoint = CGPoint()
@ -2170,13 +2159,15 @@ public final class EmojiPagerContentComponent: Component {
self.addSubview(self.backgroundView) self.addSubview(self.backgroundView)
self.shimmerHostView.alpha = 0.0 if let shimmerHostView = self.shimmerHostView {
self.addSubview(self.shimmerHostView) shimmerHostView.alpha = 0.0
self.addSubview(shimmerHostView)
}
self.boundsChangeTrackerLayer.opacity = 0.0 self.boundsChangeTrackerLayer.opacity = 0.0
self.layer.addSublayer(self.boundsChangeTrackerLayer) self.layer.addSublayer(self.boundsChangeTrackerLayer)
self.boundsChangeTrackerLayer.didEnterHierarchy = { [weak self] in self.boundsChangeTrackerLayer.didEnterHierarchy = { [weak self] in
self?.standaloneShimmerEffect.updateLayer() self?.standaloneShimmerEffect?.updateLayer()
} }
self.scrollView.delaysContentTouches = false 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 itemNativeFitSize = itemDimensions.fitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize))
let itemVisibleFitSize = itemDimensions.fitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize)) 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 animateItemIn = false
var updateItemLayerPlaceholder = false var updateItemLayerPlaceholder = false
@ -3326,7 +3318,7 @@ public final class EmojiPagerContentComponent: Component {
renderer: component.animationRenderer, renderer: component.animationRenderer,
placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1), placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1),
blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5), blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5),
pointSize: itemVisibleFitSize, pointSize: item.staticEmoji == nil ? itemPlaybackSize : itemVisibleFitSize,
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -3525,10 +3517,12 @@ public final class EmojiPagerContentComponent: Component {
} }
private func updateShimmerIfNeeded() { private func updateShimmerIfNeeded() {
if self.placeholdersContainerView.subviews.isEmpty { if let standaloneShimmerEffect = self.standaloneShimmerEffect, let shimmerHostView = self.shimmerHostView {
self.standaloneShimmerEffect.layer = nil if self.placeholdersContainerView.subviews.isEmpty {
} else { standaloneShimmerEffect.layer = nil
self.standaloneShimmerEffect.layer = self.shimmerHostView.layer } else {
standaloneShimmerEffect.layer = shimmerHostView.layer
}
} }
} }
@ -3544,11 +3538,16 @@ public final class EmojiPagerContentComponent: Component {
} }
if self.vibrancyEffectView == nil { 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 vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect) let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
self.vibrancyEffectView = vibrancyEffectView self.vibrancyEffectView = vibrancyEffectView
self.backgroundView.addSubview(vibrancyEffectView) self.backgroundView.addSubview(vibrancyEffectView)
for subview in vibrancyEffectView.subviews {
let _ = subview
}
vibrancyEffectView.contentView.addSubview(self.mirrorContentScrollView) vibrancyEffectView.contentView.addSubview(self.mirrorContentScrollView)
} }
@ -3577,11 +3576,15 @@ public final class EmojiPagerContentComponent: Component {
self.pagerEnvironment = pagerEnvironment 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) if let standaloneShimmerEffect = self.standaloneShimmerEffect {
let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15) let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08)
self.standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor) let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15)
standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor)
}
var previousItemPositions: [ItemLayer.Key: CGPoint]? var previousItemPositions: [ItemLayer.Key: CGPoint]?

View File

@ -449,7 +449,7 @@ public final class EntityKeyboardComponent: Component {
if let file = itemGroup.items[0].file { if let file = itemGroup.items[0].file {
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId, id: itemGroup.supergroupId,
isReorderable: true, isReorderable: !itemGroup.isFeatured,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: component.emojiContent.context, context: component.emojiContent.context,
file: file, file: file,

View File

@ -1449,6 +1449,8 @@ final class EntityKeyboardTopPanelComponent: Component {
visibleBounds.origin.x -= 200.0 visibleBounds.origin.x -= 200.0
visibleBounds.size.width += 400.0 visibleBounds.size.width += 400.0
let scale = max(0.01, self.visibilityFraction)
var validIds = Set<AnyHashable>() var validIds = Set<AnyHashable>()
let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds) let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds)
if !self.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex { if !self.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex {
@ -1477,10 +1479,10 @@ final class EntityKeyboardTopPanelComponent: Component {
containerSize: itemOuterFrame.size 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) 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) 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] = [] var removedIds: [AnyHashable] = []
@ -1804,6 +1806,13 @@ final class EntityKeyboardTopPanelComponent: Component {
self.highlightedIconBackgroundView.isHidden = true 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 { func makeView() -> View {

View File

@ -127,14 +127,14 @@ public final class TextNodeWithEntities {
var found = false var found = false
string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, stop in 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 { 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) let replacementRange = NSRange(location: 0, length: updatedSubstring.length)
updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) 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(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) 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( let runDelegateData = RunDelegateData(
ascent: font.ascender, ascent: font.ascender,

View File

@ -20,7 +20,7 @@ private func roundUp(_ numToRound: Int, multiple: Int) -> Int {
public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) { public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) {
writer.queue.async { 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 return
} }
let frameDuration = 1.0 / Double(frameSource.frameRate) let frameDuration = 1.0 / Double(frameSource.frameRate)

View File

@ -560,7 +560,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) 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?.interfaceInteraction?.presentController(controller, nil)
}) })
self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage

View File

@ -86,7 +86,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} }
} }
static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError> { static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool) -> Signal<EmojiPagerContentComponent, NoError> {
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in |> map { peer -> Bool in
guard case let .user(user) = peer else { guard case let .user(user) = peer else {
@ -218,29 +218,31 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} }
} }
for featuredEmojiPack in featuredEmojiPacks { if !isStandalone {
if installedCollectionIds.contains(featuredEmojiPack.info.id) { for featuredEmojiPack in featuredEmojiPacks {
continue if installedCollectionIds.contains(featuredEmojiPack.info.id) {
}
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 continue
} }
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem) for item in featuredEmojiPack.topItems {
} else { let resultItem = EmojiPagerContentComponent.Item(
itemGroupIndexById[groupId] = itemGroups.count file: item.file,
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, items: [resultItem])) 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() 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 stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers] 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 stickerContent?.inputInteractionHolder.inputInteraction = self.stickerInputInteraction
self.currentInputData.emoji.inputInteractionHolder.inputInteraction = self.emojiInputInteraction self.currentInputData.emoji.inputInteractionHolder.inputInteraction = self.emojiInputInteraction
let startTime = CFAbsoluteTimeGetCurrent()
let entityKeyboardSize = self.entityKeyboardView.update( let entityKeyboardSize = self.entityKeyboardView.update(
transition: mappedTransition, transition: mappedTransition,
component: AnyComponent(EntityKeyboardComponent( 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)) 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) return (expandedHeight, 0.0)
} }
@ -1849,7 +1860,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
let semaphore = DispatchSemaphore(value: 0) let semaphore = DispatchSemaphore(value: 0)
var emojiComponent: EmojiPagerContentComponent? 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 emojiComponent = value
semaphore.signal() semaphore.signal()
}) })

View File

@ -39,7 +39,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return editPanelNode return editPanelNode
} else { } 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 panelNode.interfaceInteraction = interfaceInteraction
return panelNode return panelNode
} }

View File

@ -297,7 +297,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
textInputPanelNode.context = context textInputPanelNode.context = context
return (textInputPanelNode, nil) return (textInputPanelNode, nil)
} else { } 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) interfaceInteraction?.presentController(controller, nil)
}) })

View File

@ -15,6 +15,9 @@ import PhotoResources
import WebsiteType import WebsiteType
import ChatMessageInteractiveMediaBadge import ChatMessageInteractiveMediaBadge
import GalleryData import GalleryData
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
private let buttonFont = Font.semibold(13.0) private let buttonFont = Font.semibold(13.0)
@ -217,7 +220,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
final class ChatMessageAttachedContentNode: ASDisplayNode { final class ChatMessageAttachedContentNode: ASDisplayNode {
private let lineNode: ASImageNode private let lineNode: ASImageNode
private let textNode: TextNode private let textNode: TextNodeWithEntities
private let inlineImageNode: TransformImageNode private let inlineImageNode: TransformImageNode
private var contentImageNode: ChatMessageInteractiveMediaNode? private var contentImageNode: ChatMessageInteractiveMediaNode?
private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode? private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode?
@ -239,8 +242,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
var visibility: ListViewItemNodeVisibility = .none { var visibility: ListViewItemNodeVisibility = .none {
didSet { didSet {
self.contentImageNode?.visibility = self.visibility != .none if oldValue != self.visibility {
self.contentInstantVideoNode?.visibility = self.visibility != .none 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.displaysAsynchronously = false
self.lineNode.displayWithoutProcessing = true self.lineNode.displayWithoutProcessing = true
self.textNode = TextNode() self.textNode = TextNodeWithEntities()
self.textNode.isUserInteractionEnabled = false self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false self.textNode.textNode.displaysAsynchronously = false
self.textNode.contentsScale = UIScreenScale self.textNode.textNode.contentsScale = UIScreenScale
self.textNode.contentMode = .topLeft self.textNode.textNode.contentMode = .topLeft
self.inlineImageNode = TransformImageNode() self.inlineImageNode = TransformImageNode()
self.inlineImageNode.contentAnimations = [.subsequentUpdates] self.inlineImageNode.contentAnimations = [.subsequentUpdates]
@ -266,13 +281,13 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
super.init() super.init()
self.addSubnode(self.lineNode) self.addSubnode(self.lineNode)
self.addSubnode(self.textNode) self.addSubnode(self.textNode.textNode)
self.addSubnode(self.statusNode) 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))) { 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 = TextNode.asyncLayout(self.textNode) let textAsyncLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let currentImage = self.media as? TelegramMediaImage let currentImage = self.media as? TelegramMediaImage
let imageLayout = self.inlineImageNode.asyncLayout() let imageLayout = self.inlineImageNode.asyncLayout()
let statusLayout = self.statusNode.asyncLayout() let statusLayout = self.statusNode.asyncLayout()
@ -284,7 +299,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode 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 isPreview = presentationData.isPreview
let fontSize: CGFloat let fontSize: CGFloat
if message.adAttribute != nil { 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) 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.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 imageFrame = imageFrame {
if let updateImageSignal = updateInlineImageSignal { if let updateImageSignal = updateInlineImageSignal {
@ -976,9 +1006,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
textVerticalOffset = contentMediaHeight + 7.0 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 { 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 let imageFrame = imageFrame {
if statusFrame.maxY < imageFrame.maxY + 10.0 { if statusFrame.maxY < imageFrame.maxY + 10.0 {
statusFrame.origin.y = max(statusFrame.minY, imageFrame.maxY + 2.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 { func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
let textNodeFrame = self.textNode.frame let textNodeFrame = self.textNode.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { 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 { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true 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) concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
} }
return .url(url: url, concealed: concealed) 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 { if let context = self.context, let message = self.message, let theme = self.theme {
var rects: [CGRect]? var rects: [CGRect]?
if let point = point { if let point = point {
let textNodeFrame = self.textNode.frame let textNodeFrame = self.textNode.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [ let possibleNames: [String] = [
TelegramTextAttributes.URL, TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention, TelegramTextAttributes.PeerMention,
@ -1107,7 +1137,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
] ]
for name in possibleNames { for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] { 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 break
} }
} }
@ -1121,9 +1151,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} else { } else {
linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor) linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor)
self.linkHighlightingNode = linkHighlightingNode 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) linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode { } else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil self.linkHighlightingNode = nil

View File

@ -9,6 +9,12 @@ import TelegramCore
final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubbleContentNode { final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubbleContentNode {
private let contentNode: ChatMessageAttachedContentNode private let contentNode: ChatMessageAttachedContentNode
override var visibility: ListViewItemNodeVisibility {
didSet {
self.contentNode.visibility = visibility
}
}
required init() { required init() {
self.contentNode = ChatMessageAttachedContentNode() self.contentNode = ChatMessageAttachedContentNode()
@ -43,7 +49,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble
} }
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil 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) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)

View File

@ -9,6 +9,12 @@ import TelegramCore
final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContentNode { final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContentNode {
private let contentNode: ChatMessageAttachedContentNode private let contentNode: ChatMessageAttachedContentNode
override var visibility: ListViewItemNodeVisibility {
didSet {
self.contentNode.visibility = visibility
}
}
required init() { required init() {
self.contentNode = ChatMessageAttachedContentNode() self.contentNode = ChatMessageAttachedContentNode()
@ -38,7 +44,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent
let text: String = item.message.text let text: String = item.message.text
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil 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) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)

View File

@ -9,6 +9,12 @@ import TelegramCore
final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleContentNode { final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleContentNode {
private let contentNode: ChatMessageAttachedContentNode private let contentNode: ChatMessageAttachedContentNode
override var visibility: ListViewItemNodeVisibility {
didSet {
self.contentNode.visibility = visibility
}
}
required init() { required init() {
self.contentNode = ChatMessageAttachedContentNode() self.contentNode = ChatMessageAttachedContentNode()
@ -43,7 +49,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont
} }
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil 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) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)

View File

@ -13,7 +13,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode {
override var visibility: ListViewItemNodeVisibility { override var visibility: ListViewItemNodeVisibility {
didSet { 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) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)

View File

@ -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) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)

View File

@ -15,6 +15,9 @@ import PhotoResources
import TelegramStringFormatting import TelegramStringFormatting
import TextFormat import TextFormat
import InvisibleInkDustNode import InvisibleInkDustNode
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
public final class ChatMessageNotificationItem: NotificationItem { public final class ChatMessageNotificationItem: NotificationItem {
let context: AccountContext let context: AccountContext
@ -69,7 +72,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let titleIconNode: ASImageNode private let titleIconNode: ASImageNode
private let titleNode: TextNode private let titleNode: TextNode
private let textNode: TextNode private let textNode: TextNodeWithEntities
private var dustNode: InvisibleInkDustNode? private var dustNode: InvisibleInkDustNode?
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
@ -79,6 +82,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
private var compact: Bool? private var compact: Bool?
private var validLayout: CGFloat? private var validLayout: CGFloat?
private var animationCache: AnimationCache?
private var multiAnimationRenderer: MultiAnimationRenderer?
override init() { override init() {
self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode = AvatarNode(font: avatarFont)
@ -90,8 +96,8 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
self.titleIconNode.displayWithoutProcessing = true self.titleIconNode.displayWithoutProcessing = true
self.titleIconNode.displaysAsynchronously = false self.titleIconNode.displaysAsynchronously = false
self.textNode = TextNode() self.textNode = TextNodeWithEntities()
self.textNode.isUserInteractionEnabled = false self.textNode.textNode.isUserInteractionEnabled = false
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
@ -100,12 +106,20 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
self.addSubnode(self.avatarNode) self.addSubnode(self.avatarNode)
self.addSubnode(self.titleIconNode) self.addSubnode(self.titleIconNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.textNode) self.addSubnode(self.textNode.textNode)
self.addSubnode(self.imageNode) self.addSubnode(self.imageNode)
} }
func setupItem(_ item: ChatMessageNotificationItem, compact: Bool) { func setupItem(_ item: ChatMessageNotificationItem, compact: Bool) {
self.item = item 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 self.compact = compact
if compact { if compact {
self.avatarNode.font = compactAvatarFont self.avatarNode.font = compactAvatarFont
@ -190,6 +204,8 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
messageEntities = message.textEntitiesAttribute?.entities.filter { entity in messageEntities = message.textEntitiesAttribute?.entities.filter { entity in
if case .Spoiler = entity.type { if case .Spoiler = entity.type {
return true return true
} else if case .CustomEmoji = entity.type {
return true
} else { } else {
return false 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 (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 _ = 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 (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 _ = 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 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) 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)) 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 = InvisibleInkDustNode(textNode: nil)
dustNode.isUserInteractionEnabled = false dustNode.isUserInteractionEnabled = false
self.dustNode = dustNode 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.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) }) 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) })

View File

@ -373,7 +373,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
displayLine = false 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) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)

View File

@ -588,7 +588,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? 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 self.presentationInterfaceState = presentationInterfaceState
var hasSpoilers = true var hasSpoilers = true
@ -652,6 +652,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
super.init() super.init()
self.context = context
self.addSubnode(self.clippingNode) self.addSubnode(self.clippingNode)
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
@ -1109,9 +1111,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize)
if let textInputNode = self.textInputNode { if let textInputNode = self.textInputNode {
if let text = textInputNode.attributedText?.string { if let text = textInputNode.attributedText {
let range = textInputNode.selectedRange 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.selectedRange = range
} }
textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor]

View File

@ -13,6 +13,10 @@ import RadialStatusNode
import PhotoResources import PhotoResources
import TelegramStringFormatting import TelegramStringFormatting
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import TextFormat
final class EditAccessoryPanelNode: AccessoryPanelNode { final class EditAccessoryPanelNode: AccessoryPanelNode {
let dateTimeFormat: PresentationDateTimeFormat let dateTimeFormat: PresentationDateTimeFormat
@ -22,7 +26,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
let lineNode: ASImageNode let lineNode: ASImageNode
let iconNode: ASImageNode let iconNode: ASImageNode
let titleNode: ImmediateTextNode let titleNode: ImmediateTextNode
let textNode: ImmediateTextNode let textNode: ImmediateTextNodeWithEntities
let imageNode: TransformImageNode let imageNode: TransformImageNode
let dimNode: ASDisplayNode let dimNode: ASDisplayNode
let drawIconNode: ASImageNode let drawIconNode: ASImageNode
@ -71,7 +75,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? 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.context = context
self.messageId = messageId self.messageId = messageId
self.theme = theme self.theme = theme
@ -99,10 +103,21 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
self.titleNode.maximumNumberOfLines = 1 self.titleNode.maximumNumberOfLines = 1
self.titleNode.displaysAsynchronously = false self.titleNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode() self.textNode = ImmediateTextNodeWithEntities()
self.textNode.maximumNumberOfLines = 1 self.textNode.maximumNumberOfLines = 1
self.textNode.displaysAsynchronously = false self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = true 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 = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.contentAnimations = [.subsequentUpdates]
@ -238,6 +253,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
self.isPhoto = isPhoto self.isPhoto = isPhoto
let isMedia: Bool let isMedia: Bool
let isText: Bool
if let message = message { if let message = message {
var effectiveMessage = message var effectiveMessage = message
if let currentEditMediaReference = self.currentEditMediaReference { 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) { 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: case .text:
isMedia = false isMedia = false
isText = true
default: default:
isMedia = true isMedia = true
isText = false
} }
} else { } else {
isMedia = false isMedia = false
isText = true
} }
let canEditMedia: Bool let canEditMedia: Bool
@ -268,7 +287,29 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
titleString = self.strings.Conversation_EditingMessagePanelTitle titleString = self.strings.Conversation_EditingMessagePanelTitle
} }
self.titleNode.attributedText = NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) 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 let headerString: String = titleString
self.actionArea.accessibilityLabel = "\(headerString).\n\(text)" 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) self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor)
} }
if let text = self.textNode.attributedText?.string { if let text = self.textNode.attributedText {
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) 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 { if let (size, inset, interfaceState) = self.validLayout {

View File

@ -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 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 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);

View File

@ -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 vImage_YpCbCrToARGB info;
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&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}; 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); error = vImagePermuteChannels_ARGB8888(&dest, &dest, permuteMap, kvImageDoNotTile);
if (error != kvImageNoError) { if (error != kvImageNoError) {