From 96279df59bb78103008a76d8cca4a59cb3375c3a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 6 Nov 2023 20:18:59 +0400 Subject: [PATCH] Re-implement external sharing to secret chats (cherry picked from commit 21af13cfdb347b76bf7b504ae5d36067ba693b18) --- .../PeerMergedOperationLogIndexTable.swift | 24 +++ .../Sources/PeerMergedOperationLogView.swift | 108 ++++++---- .../Sources/PeerOperationLogTable.swift | 29 ++- submodules/Postbox/Sources/Postbox.swift | 8 +- .../Sources/ShareController.swift | 3 - .../Sources/Account/Account.swift | 2 +- .../PendingMessageUploadedContent.swift | 10 + .../StandaloneSendMessage.swift | 88 +++++++- .../ManagedSecretChatOutgoingOperations.swift | 187 ++++++++++++++++- ...SyncCore_SecretChatOutgoingOperation.swift | 192 +++++++++++++++--- .../ChatInterfaceStateContextQueries.swift | 2 +- .../Sources/ChatTextInputPanelNode.swift | 14 +- 12 files changed, 565 insertions(+), 102 deletions(-) diff --git a/submodules/Postbox/Sources/PeerMergedOperationLogIndexTable.swift b/submodules/Postbox/Sources/PeerMergedOperationLogIndexTable.swift index b04a4652c3..b2572299d6 100644 --- a/submodules/Postbox/Sources/PeerMergedOperationLogIndexTable.swift +++ b/submodules/Postbox/Sources/PeerMergedOperationLogIndexTable.swift @@ -52,6 +52,30 @@ final class PeerMergedOperationLogIndexTable: Table { return result } + func getTagLocalIndices(tag: PeerOperationLogTag, peerId: PeerId, fromMergedIndex: Int32, limit: Int) -> [(PeerId, Int32, Int32)] { + var result: [(PeerId, Int32, Int32)] = [] + self.valueBox.range(self.table, start: self.key(tag: tag, index: fromMergedIndex == 0 ? 0 : fromMergedIndex - 1), end: self.key(tag: tag, index: Int32.max), values: { key, value in + assert(key.getUInt8(0) == tag.rawValue) + var peerIdValue: Int64 = 0 + var tagLocalIndexValue: Int32 = 0 + value.read(&peerIdValue, offset: 0, length: 8) + value.read(&tagLocalIndexValue, offset: 0, length: 4) + + let parsedPeerId = PeerId(peerIdValue) + if parsedPeerId != peerId { + return true + } + + result.append((parsedPeerId, tagLocalIndexValue, key.getInt32(1))) + if result.count >= limit { + return false + } + + return true + }, limit: 0) + return result + } + func tailIndex(tag: PeerOperationLogTag) -> Int32? { var result: Int32? self.valueBox.range(self.table, start: self.key(tag: tag, index: Int32.max), end: self.key(tag: tag, index: 0), keys: { diff --git a/submodules/Postbox/Sources/PeerMergedOperationLogView.swift b/submodules/Postbox/Sources/PeerMergedOperationLogView.swift index af2daac0f6..8912d2c830 100644 --- a/submodules/Postbox/Sources/PeerMergedOperationLogView.swift +++ b/submodules/Postbox/Sources/PeerMergedOperationLogView.swift @@ -2,23 +2,52 @@ import Foundation final class MutablePeerMergedOperationLogView { let tag: PeerOperationLogTag + let filterByPeerId: PeerId? var entries: [PeerMergedOperationLogEntry] var tailIndex: Int32? let limit: Int - init(postbox: PostboxImpl, tag: PeerOperationLogTag, limit: Int) { + init(postbox: PostboxImpl, tag: PeerOperationLogTag, filterByPeerId: PeerId?, limit: Int) { self.tag = tag - self.entries = postbox.peerOperationLogTable.getMergedEntries(tag: tag, fromIndex: 0, limit: limit) + self.filterByPeerId = filterByPeerId + if let filterByPeerId = self.filterByPeerId { + self.entries = postbox.peerOperationLogTable.getMergedEntries(tag: tag, peerId: filterByPeerId, fromIndex: 0, limit: limit) + } else { + self.entries = postbox.peerOperationLogTable.getMergedEntries(tag: tag, fromIndex: 0, limit: limit) + } self.tailIndex = postbox.peerMergedOperationLogIndexTable.tailIndex(tag: tag) self.limit = limit } func replay(postbox: PostboxImpl, operations: [PeerMergedOperationLogOperation]) -> Bool { var updated = false - var invalidatedTail = false - for operation in operations { - switch operation { + if let filterByPeerId = self.filterByPeerId { + if operations.contains(where: { operation in + switch operation { + case let .append(entry): + if entry.tag == self.tag && entry.peerId == filterByPeerId { + return true + } + case let .remove(tag, peerId, _): + if tag == self.tag && peerId == filterByPeerId { + return true + } + case let .updateContents(entry): + if entry.tag == self.tag && entry.peerId == filterByPeerId { + return true + } + } + return false + }) { + self.entries = postbox.peerOperationLogTable.getMergedEntries(tag: tag, peerId: filterByPeerId, fromIndex: 0, limit: limit) + updated = true + } + } else { + var invalidatedTail = false + + for operation in operations { + switch operation { case let .append(entry): if entry.tag == self.tag { if let tailIndex = self.tailIndex { @@ -39,15 +68,15 @@ final class MutablePeerMergedOperationLogView { } case let .updateContents(entry): if entry.tag == self.tag { - loop: for i in 0 ..< self.entries.count { - if self.entries[i].tagLocalIndex == entry.tagLocalIndex { - self.entries[i] = entry - updated = true - break loop - } + loop: for i in 0 ..< self.entries.count { + if self.entries[i].tagLocalIndex == entry.tagLocalIndex { + self.entries[i] = entry + updated = true + break loop } } - case let .remove(tag, mergedIndices): + } + case let .remove(tag, _, mergedIndices): if tag == self.tag { updated = true for i in (0 ..< self.entries.count).reversed() { @@ -60,37 +89,38 @@ final class MutablePeerMergedOperationLogView { invalidatedTail = true } } + } } - } - - if updated { - if invalidatedTail { - self.tailIndex = postbox.peerMergedOperationLogIndexTable.tailIndex(tag: self.tag) - } - if self.entries.count < self.limit { - if let tailIndex = self.tailIndex { - if self.entries.isEmpty || self.entries.last!.mergedIndex < tailIndex { - var fromIndex: Int32 = 0 - if !self.entries.isEmpty { - fromIndex = self.entries.last!.mergedIndex + 1 - } - for entry in postbox.peerOperationLogTable.getMergedEntries(tag: self.tag, fromIndex: fromIndex, limit: self.limit - self.entries.count) { - self.entries.append(entry) - } - for i in 0 ..< self.entries.count { - if i != 0 { - assert(self.entries[i].mergedIndex >= self.entries[i - 1].mergedIndex + 1) + + if updated { + if invalidatedTail { + self.tailIndex = postbox.peerMergedOperationLogIndexTable.tailIndex(tag: self.tag) + } + if self.entries.count < self.limit { + if let tailIndex = self.tailIndex { + if self.entries.isEmpty || self.entries.last!.mergedIndex < tailIndex { + var fromIndex: Int32 = 0 + if !self.entries.isEmpty { + fromIndex = self.entries.last!.mergedIndex + 1 + } + for entry in postbox.peerOperationLogTable.getMergedEntries(tag: self.tag, fromIndex: fromIndex, limit: self.limit - self.entries.count) { + self.entries.append(entry) + } + for i in 0 ..< self.entries.count { + if i != 0 { + assert(self.entries[i].mergedIndex >= self.entries[i - 1].mergedIndex + 1) + } + } + if !self.entries.isEmpty { + assert(self.entries.last!.mergedIndex <= tailIndex) } } - if !self.entries.isEmpty { - assert(self.entries.last!.mergedIndex <= tailIndex) - } } - } - } else { - assert(self.tailIndex != nil) - if let tailIndex = self.tailIndex { - assert(self.entries.last!.mergedIndex <= tailIndex) + } else { + assert(self.tailIndex != nil) + if let tailIndex = self.tailIndex { + assert(self.entries.last!.mergedIndex <= tailIndex) + } } } } diff --git a/submodules/Postbox/Sources/PeerOperationLogTable.swift b/submodules/Postbox/Sources/PeerOperationLogTable.swift index f3c3a9cc95..34e828b4c9 100644 --- a/submodules/Postbox/Sources/PeerOperationLogTable.swift +++ b/submodules/Postbox/Sources/PeerOperationLogTable.swift @@ -2,7 +2,7 @@ import Foundation enum PeerMergedOperationLogOperation { case append(PeerMergedOperationLogEntry) - case remove(tag: PeerOperationLogTag, mergedIndices: Set) + case remove(tag: PeerOperationLogTag, peerId: PeerId, mergedIndices: Set) case updateContents(PeerMergedOperationLogEntry) } @@ -197,7 +197,7 @@ final class PeerOperationLogTable: Table { if !mergedIndices.isEmpty { self.mergedIndexTable.remove(tag: tag, mergedIndices: mergedIndices) - operations.append(.remove(tag: tag, mergedIndices: Set(mergedIndices))) + operations.append(.remove(tag: tag, peerId: peerId, mergedIndices: Set(mergedIndices))) } return removed } @@ -224,7 +224,7 @@ final class PeerOperationLogTable: Table { if !mergedIndices.isEmpty { self.mergedIndexTable.remove(tag: tag, mergedIndices: mergedIndices) - operations.append(.remove(tag: tag, mergedIndices: Set(mergedIndices))) + operations.append(.remove(tag: tag, peerId: peerId, mergedIndices: Set(mergedIndices))) } } @@ -250,7 +250,7 @@ final class PeerOperationLogTable: Table { if !mergedIndices.isEmpty { self.mergedIndexTable.remove(tag: tag, mergedIndices: mergedIndices) - operations.append(.remove(tag: tag, mergedIndices: Set(mergedIndices))) + operations.append(.remove(tag: tag, peerId: peerId, mergedIndices: Set(mergedIndices))) } } @@ -271,6 +271,23 @@ final class PeerOperationLogTable: Table { return entries } + func getMergedEntries(tag: PeerOperationLogTag, peerId: PeerId, fromIndex: Int32, limit: Int) -> [PeerMergedOperationLogEntry] { + var entries: [PeerMergedOperationLogEntry] = [] + for (peerId, tagLocalIndex, mergedIndex) in self.mergedIndexTable.getTagLocalIndices(tag: tag, peerId: peerId, fromMergedIndex: fromIndex, limit: limit) { + if let value = self.valueBox.get(self.table, key: self.key(peerId: peerId, tag: tag, index: tagLocalIndex)) { + if let entry = parseMergedEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, value) { + entries.append(entry) + } else { + assertionFailure() + } + } else { + self.mergedIndexTable.remove(tag: tag, mergedIndices: [mergedIndex]) + assertionFailure() + } + } + return entries + } + func enumerateEntries(peerId: PeerId, tag: PeerOperationLogTag, _ f: (PeerOperationLogEntry) -> Bool) { self.valueBox.range(self.table, start: self.key(peerId: peerId, tag: tag, index: 0).predecessor, end: self.key(peerId: peerId, tag: tag, index: Int32.max).successor, values: { key, value in if let entry = parseEntry(peerId: peerId, tag: tag, tagLocalIndex: key.getInt32(9), value) { @@ -317,12 +334,12 @@ final class PeerOperationLogTable: Table { if let mergedIndexValue = mergedIndex { mergedIndex = nil self.mergedIndexTable.remove(tag: tag, mergedIndices: [mergedIndexValue]) - operations.append(.remove(tag: tag, mergedIndices: Set([mergedIndexValue]))) + operations.append(.remove(tag: tag, peerId: peerId, mergedIndices: Set([mergedIndexValue]))) } case .newAutomatic: if let mergedIndexValue = mergedIndex { self.mergedIndexTable.remove(tag: tag, mergedIndices: [mergedIndexValue]) - operations.append(.remove(tag: tag, mergedIndices: Set([mergedIndexValue]))) + operations.append(.remove(tag: tag, peerId: peerId, mergedIndices: Set([mergedIndexValue]))) } let updatedMergedIndexValue = self.mergedIndexTable.add(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex) mergedIndex = updatedMergedIndexValue diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 13dd203412..17b29d6291 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -3654,9 +3654,9 @@ final class PostboxImpl { } } - public func mergedOperationLogView(tag: PeerOperationLogTag, limit: Int) -> Signal { + public func mergedOperationLogView(tag: PeerOperationLogTag, filterByPeerId: PeerId?, limit: Int) -> Signal { return self.transactionSignal { subscriber, transaction in - let view = MutablePeerMergedOperationLogView(postbox: self, tag: tag, limit: limit) + let view = MutablePeerMergedOperationLogView(postbox: self, tag: tag, filterByPeerId: filterByPeerId, limit: limit) subscriber.putNext(PeerMergedOperationLogView(view)) @@ -4638,12 +4638,12 @@ public class Postbox { } } - public func mergedOperationLogView(tag: PeerOperationLogTag, limit: Int) -> Signal { + public func mergedOperationLogView(tag: PeerOperationLogTag, filterByPeerId: PeerId? = nil, limit: Int) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.mergedOperationLogView(tag: tag, limit: limit).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) + disposable.set(impl.mergedOperationLogView(tag: tag, filterByPeerId: filterByPeerId, limit: limit).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) } return disposable diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index f4e9c00209..b21118ca8f 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -971,9 +971,6 @@ public final class ShareController: ViewController { if self.environment.isMainApp { useLegacy = true } - if peerIds.contains(where: { $0.namespace == Namespaces.Peer.SecretChat }) { - useLegacy = true - } if let currentContext = self.currentContext as? ShareControllerAppAccountContext, let data = currentContext.context.currentAppConfiguration.with({ $0 }).data { if let _ = data["ios_disable_modern_sharing"] { useLegacy = true diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 51a5d7bd18..9180fed079 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1153,7 +1153,7 @@ public class Account { pendingMessageManager?.updatePendingMessageIds(view.ids) })) - self.managedOperationsDisposable.add(managedSecretChatOutgoingOperations(auxiliaryMethods: auxiliaryMethods, postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedSecretChatOutgoingOperations(auxiliaryMethods: auxiliaryMethods, postbox: self.postbox, network: self.network, accountPeerId: peerId, mode: .all).start()) self.managedOperationsDisposable.add(managedCloudChatRemoveMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: true).start()) self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: false).start()) diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 0e81fdcf7f..7025a9bed1 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -963,5 +963,15 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } } } + |> take(until: { result in + var complete = false + switch result { + case .content: + complete = true + case .progress: + complete = false + } + return SignalTakeAction(passthrough: true, complete: complete) + }) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 7e8f5e7440..2d0e9aec51 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -125,7 +125,12 @@ public func standaloneSendEnqueueMessages( threadId: Int64?, messages: [StandaloneSendEnqueueMessage] ) -> Signal { - let signals: [Signal] = messages.map { message in + struct MessageResult { + var result: PendingMessageUploadedContentResult + var media: [Media] + } + + let signals: [Signal] = messages.map { message in var attributes: [MessageAttribute] = [] var text: String = "" var media: [Media] = [] @@ -184,6 +189,9 @@ public func standaloneSendEnqueueMessages( contentResult = .single(value) } return contentResult + |> map { contentResult in + return MessageResult(result: contentResult, media: media) + } } return combineLatest(signals) @@ -192,21 +200,21 @@ public func standaloneSendEnqueueMessages( } |> mapToSignal { contentResults -> Signal in var progressSum: Float = 0.0 - var allResults: [PendingMessageUploadedContentAndReuploadInfo] = [] + var allResults: [(result: PendingMessageUploadedContentAndReuploadInfo, media: [Media])] = [] var allDone = true - for status in contentResults { - switch status { + for result in contentResults { + switch result.result { case let .progress(value): allDone = false progressSum += value case let .content(content): - allResults.append(content) + allResults.append((content, result.media)) } } if allDone { var sendSignals: [Signal] = [] - for content in allResults { + for (content, media) in allResults { var text: String = "" switch content.content { case let .text(textValue): @@ -218,6 +226,7 @@ public func standaloneSendEnqueueMessages( } sendSignals.append(sendUploadedMessageContent( + auxiliaryMethods: auxiliaryMethods, postbox: postbox, network: network, stateManager: stateManager, @@ -226,6 +235,7 @@ public func standaloneSendEnqueueMessages( content: content, text: text, attributes: [], + media: media, threadId: threadId )) } @@ -241,12 +251,70 @@ public func standaloneSendEnqueueMessages( } } -private func sendUploadedMessageContent(postbox: Postbox, network: Network, stateManager: AccountStateManager, accountPeerId: PeerId, peerId: PeerId, content: PendingMessageUploadedContentAndReuploadInfo, text: String, attributes: [MessageAttribute], threadId: Int64?) -> Signal { +private func sendUploadedMessageContent( + auxiliaryMethods: AccountAuxiliaryMethods, + postbox: Postbox, + network: Network, + stateManager: AccountStateManager, + accountPeerId: PeerId, + peerId: PeerId, + content: PendingMessageUploadedContentAndReuploadInfo, + text: String, + attributes: [MessageAttribute], + media: [Media], + threadId: Int64? +) -> Signal { return postbox.transaction { transaction -> Signal in if peerId.namespace == Namespaces.Peer.SecretChat { - assertionFailure() - //PendingMessageManager.sendSecretMessageContent(transaction: transaction, message: message, content: content) - return .complete() + var secretFile: SecretChatOutgoingFile? + switch content.content { + case let .secretMedia(file, size, key): + if let fileReference = SecretChatOutgoingFileReference(file) { + secretFile = SecretChatOutgoingFile(reference: fileReference, size: size, key: key) + } + default: + break + } + + var layer: SecretChatLayer? + let state = transaction.getPeerChatState(peerId) as? SecretChatState + if let state = state { + switch state.embeddedState { + case .terminated, .handshake: + break + case .basicLayer: + layer = .layer8 + case let .sequenceBasedLayer(sequenceState): + layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer + } + } + + if let state = state, let layer = layer { + let messageContents = StandaloneSecretMessageContents( + id: Int64.random(in: Int64.min ... Int64.max), + text: text, + attributes: attributes, + media: media.first, + file: secretFile + ) + + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .sendStandaloneMessage(layer: layer, contents: messageContents), state: state) + if updatedState != state { + transaction.setPeerChatState(peerId, state: updatedState) + } + + return managedSecretChatOutgoingOperations( + auxiliaryMethods: auxiliaryMethods, + postbox: postbox, + network: network, + accountPeerId: accountPeerId, + mode: .standaloneComplete(peerId: peerId) + ) + |> castError(StandaloneSendMessagesError.self) + |> ignoreValues + } else { + return .fail(StandaloneSendMessagesError(peerId: peerId, reason: .none)) + } } else if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var uniqueId: Int64 = 0 var forwardSourceInfoAttribute: ForwardSourceInfoAttribute? diff --git a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index d7fe6ea2be..daf468b03b 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -105,11 +105,20 @@ private func takenImmutableOperation(postbox: Postbox, peerId: PeerId, tagLocalI } } -func managedSecretChatOutgoingOperations(auxiliaryMethods: AccountAuxiliaryMethods, postbox: Postbox, network: Network) -> Signal { - return Signal { _ in +enum ManagedSecretChatOutgoingOperationsMode { + case all + case standaloneComplete(peerId: PeerId) +} + +func managedSecretChatOutgoingOperations(auxiliaryMethods: AccountAuxiliaryMethods, postbox: Postbox, network: Network, accountPeerId: PeerId, mode: ManagedSecretChatOutgoingOperationsMode) -> Signal { + return Signal { subscriber in let helper = Atomic(value: ManagedSecretChatOutgoingOperationsHelper()) - let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SecretOutgoing, limit: 10).start(next: { view in + var filterByPeerId: PeerId? + if case let .standaloneComplete(peerId) = mode { + filterByPeerId = peerId + } + let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SecretOutgoing, filterByPeerId: filterByPeerId, limit: 10).start(next: { view in let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in return helper.update(view.entries) } @@ -118,6 +127,10 @@ func managedSecretChatOutgoingOperations(auxiliaryMethods: AccountAuxiliaryMetho disposable.dispose() } + if case .standaloneComplete = mode, view.entries.isEmpty { + subscriber.putCompletion() + } + for (entry, disposable) in beginOperations { let signal = takenImmutableOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex) |> mapToSignal { entry -> Signal in @@ -128,6 +141,8 @@ func managedSecretChatOutgoingOperations(auxiliaryMethods: AccountAuxiliaryMetho return initialHandshakeAccept(postbox: postbox, network: network, peerId: entry.peerId, accessHash: accessHash, gA: gA, b: b, tagLocalIndex: entry.tagLocalIndex) case let .sendMessage(layer, id, file): return sendMessage(auxiliaryMethods: auxiliaryMethods, postbox: postbox, network: network, messageId: id, file: file, tagLocalIndex: entry.tagLocalIndex, wasDelivered: operation.delivered, layer: layer) + case let .sendStandaloneMessage(layer, contents): + return sendStandaloneMessage(auxiliaryMethods: auxiliaryMethods, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: entry.peerId, contents: contents, tagLocalIndex: entry.tagLocalIndex, wasDelivered: operation.delivered, layer: layer) case let .reportLayerSupport(layer, actionGloballyUniqueId, layerSupport): return sendServiceActionMessage(postbox: postbox, network: network, peerId: entry.peerId, action: .reportLayerSupport(layer: layer, actionGloballyUniqueId: actionGloballyUniqueId, layerSupport: layerSupport), tagLocalIndex: entry.tagLocalIndex, wasDelivered: operation.delivered) case let .deleteMessages(layer, actionGloballyUniqueId, globallyUniqueIds): @@ -1711,9 +1726,9 @@ private func resourceThumbnailData(auxiliaryMethods: AccountAuxiliaryMethods, me } } -private func messageWithThumbnailData(auxiliaryMethods: AccountAuxiliaryMethods, mediaBox: MediaBox, message: Message) -> Signal<[MediaId: (PixelDimensions, Data)], NoError> { +private func messageWithThumbnailData(auxiliaryMethods: AccountAuxiliaryMethods, mediaBox: MediaBox, media: [Media]) -> Signal<[MediaId: (PixelDimensions, Data)], NoError> { var signals: [Signal<(MediaId, PixelDimensions, Data)?, NoError>] = [] - for media in message.media { + for media in media { if let image = media as? TelegramMediaImage { if let smallestRepresentation = smallestImageRepresentation(image.representations) { signals.append(resourceThumbnailData(auxiliaryMethods: auxiliaryMethods, mediaBox: mediaBox, resource: smallestRepresentation.resource, mediaId: image.imageId)) @@ -1739,7 +1754,7 @@ private func messageWithThumbnailData(auxiliaryMethods: AccountAuxiliaryMethods, private func sendMessage(auxiliaryMethods: AccountAuxiliaryMethods, postbox: Postbox, network: Network, messageId: MessageId, file: SecretChatOutgoingFile?, tagLocalIndex: Int32, wasDelivered: Bool, layer: SecretChatLayer) -> Signal { return postbox.transaction { transaction -> Signal<[MediaId: (PixelDimensions, Data)], NoError> in if let message = transaction.getMessage(messageId) { - return messageWithThumbnailData(auxiliaryMethods: auxiliaryMethods, mediaBox: postbox.mediaBox, message: message) + return messageWithThumbnailData(auxiliaryMethods: auxiliaryMethods, mediaBox: postbox.mediaBox, media: message.media) } else { return .single([:]) } @@ -1840,6 +1855,166 @@ private func sendMessage(auxiliaryMethods: AccountAuxiliaryMethods, postbox: Pos } } +private func sendStandaloneMessage(auxiliaryMethods: AccountAuxiliaryMethods, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, contents: StandaloneSecretMessageContents, tagLocalIndex: Int32, wasDelivered: Bool, layer: SecretChatLayer) -> Signal { + return postbox.transaction { transaction -> Signal<[MediaId: (PixelDimensions, Data)], NoError> in + var media: [Media] = [] + if let value = contents.media { + media.append(value) + } + return messageWithThumbnailData(auxiliaryMethods: auxiliaryMethods, mediaBox: postbox.mediaBox, media: media) + } + |> switchToLatest + |> mapToSignal { thumbnailData -> Signal in + return postbox.transaction { transaction -> Signal in + guard let state = transaction.getPeerChatState(peerId) as? SecretChatState, let peer = transaction.getPeer(peerId) as? TelegramSecretChat else { + return .complete() + } + + let globallyUniqueId = contents.id + + var media: [Media] = [] + if let value = contents.media { + media.append(value) + } + let message = Message( + stableId: 1, + stableVersion: 0, + id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 1), + globallyUniqueId: globallyUniqueId, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: 1, + flags: [], + tags: [], + globalTags: [], + localTags: [], + forwardInfo: nil, + author: nil, + text: contents.text, + attributes: contents.attributes, + media: media, + peers: SimpleDictionary(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + + let decryptedMessage = boxedDecryptedMessage(transaction: transaction, message: message, globallyUniqueId: globallyUniqueId, uploadedFile: contents.file, thumbnailData: [:], layer: layer) + return sendBoxedDecryptedMessage(postbox: postbox, network: network, peer: peer, state: state, operationIndex: tagLocalIndex, decryptedMessage: decryptedMessage, globallyUniqueId: globallyUniqueId, file: contents.file, silent: message.muted, asService: wasDelivered, wasDelivered: wasDelivered) + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Void in + let forceRemove: Bool + switch result { + case .message: + forceRemove = false + case .error: + forceRemove = true + } + markOutgoingOperationAsCompleted(transaction: transaction, peerId: peerId, tagLocalIndex: tagLocalIndex, forceRemove: forceRemove) + + var timestamp: Int32? + var encryptedFile: SecretChatFileReference? + if case let .message(result) = result { + switch result { + case let .sentEncryptedMessage(date): + timestamp = date + case let .sentEncryptedFile(date, file): + timestamp = date + encryptedFile = SecretChatFileReference(file) + } + } + + if let timestamp = timestamp { + var updatedMedia: [Media] = [] + for item in media { + if let file = item as? TelegramMediaFile, let encryptedFile = encryptedFile, let sourceFile = contents.file { + let updatedFile = TelegramMediaFile( + fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: encryptedFile.id), + partialReference: nil, + resource: SecretFileMediaResource(fileId: encryptedFile.id, accessHash: encryptedFile.accessHash, containerSize: encryptedFile.size, decryptedSize: sourceFile.size, datacenterId: Int(encryptedFile.datacenterId), key: sourceFile.key), + previewRepresentations: file.previewRepresentations, + videoThumbnails: file.videoThumbnails, + immediateThumbnailData: file.immediateThumbnailData, + mimeType: file.mimeType, + size: file.size, + attributes: file.attributes + ) + updatedMedia.append(updatedFile) + } else if let image = item as? TelegramMediaImage, let encryptedFile = encryptedFile, let sourceFile = contents.file, let representation = image.representations.last { + let updatedImage = TelegramMediaImage( + imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: encryptedFile.id), + representations: [ + TelegramMediaImageRepresentation( + dimensions: representation.dimensions, + resource: SecretFileMediaResource(fileId: encryptedFile.id, accessHash: encryptedFile.accessHash, containerSize: encryptedFile.size, decryptedSize: sourceFile.size, datacenterId: Int(encryptedFile.datacenterId), key: sourceFile.key), + progressiveSizes: [], + immediateThumbnailData: image.immediateThumbnailData, + hasVideo: false, + isPersonal: false + )], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + updatedMedia.append(updatedImage) + } else { + updatedMedia.append(item) + } + } + + let entitiesAttribute = message.textEntitiesAttribute + let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: contents.attributes, media: updatedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) + + let storedMessage = StoreMessage( + peerId: peerId, + namespace: Namespaces.Message.Local, + globallyUniqueId: globallyUniqueId, + groupingKey: nil, + threadId: nil, + timestamp: timestamp, + flags: [], + tags: tags, + globalTags: globalTags, + localTags: [], + forwardInfo: nil, + authorId: accountPeerId, + text: message.text, + attributes: message.attributes, + media: updatedMedia + ) + + let idMapping = transaction.addMessages([storedMessage], location: .Random) + if let id = idMapping[globallyUniqueId] { + maybeReadSecretOutgoingMessage(transaction: transaction, index: MessageIndex(id: id, timestamp: timestamp)) + } + + var sentStickers: [TelegramMediaFile] = [] + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isSticker { + sentStickers.append(file) + } + } + } + + for file in sentStickers { + addRecentlyUsedSticker(transaction: transaction, fileReference: .standalone(media: file)) + } + + if case .error(.chatCancelled) = result { + } + } + } + } + } + |> switchToLatest + } +} + private func sendServiceActionMessage(postbox: Postbox, network: Network, peerId: PeerId, action: SecretMessageAction, tagLocalIndex: Int32, wasDelivered: Bool) -> Signal { return postbox.transaction { transaction -> Signal in if let state = transaction.getPeerChatState(peerId) as? SecretChatState, let peer = transaction.getPeer(peerId) as? TelegramSecretChat { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SecretChatOutgoingOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SecretChatOutgoingOperation.swift index 54dd67db1a..59931fec81 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SecretChatOutgoingOperation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SecretChatOutgoingOperation.swift @@ -7,47 +7,97 @@ private enum SecretChatOutgoingFileValue: Int32 { case uploadedLarge = 2 } -public enum SecretChatOutgoingFileReference: PostboxCoding { +public enum SecretChatOutgoingFileReference: PostboxCoding, Codable { case remote(id: Int64, accessHash: Int64) case uploadedRegular(id: Int64, partCount: Int32, md5Digest: String, keyFingerprint: Int32) case uploadedLarge(id: Int64, partCount: Int32, keyFingerprint: Int32) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { - case SecretChatOutgoingFileValue.remote.rawValue: - self = .remote(id: decoder.decodeInt64ForKey("i", orElse: 0), accessHash: decoder.decodeInt64ForKey("a", orElse: 0)) - case SecretChatOutgoingFileValue.uploadedRegular.rawValue: - self = .uploadedRegular(id: decoder.decodeInt64ForKey("i", orElse: 0), partCount: decoder.decodeInt32ForKey("p", orElse: 0), md5Digest: decoder.decodeStringForKey("d", orElse: ""), keyFingerprint: decoder.decodeInt32ForKey("f", orElse: 0)) - case SecretChatOutgoingFileValue.uploadedLarge.rawValue: - self = .uploadedLarge(id: decoder.decodeInt64ForKey("i", orElse: 0), partCount: decoder.decodeInt32ForKey("p", orElse: 0), keyFingerprint: decoder.decodeInt32ForKey("f", orElse: 0)) - default: - assertionFailure() - self = .remote(id: 0, accessHash: 0) + case SecretChatOutgoingFileValue.remote.rawValue: + self = .remote(id: decoder.decodeInt64ForKey("i", orElse: 0), accessHash: decoder.decodeInt64ForKey("a", orElse: 0)) + case SecretChatOutgoingFileValue.uploadedRegular.rawValue: + self = .uploadedRegular(id: decoder.decodeInt64ForKey("i", orElse: 0), partCount: decoder.decodeInt32ForKey("p", orElse: 0), md5Digest: decoder.decodeStringForKey("d", orElse: ""), keyFingerprint: decoder.decodeInt32ForKey("f", orElse: 0)) + case SecretChatOutgoingFileValue.uploadedLarge.rawValue: + self = .uploadedLarge(id: decoder.decodeInt64ForKey("i", orElse: 0), partCount: decoder.decodeInt32ForKey("p", orElse: 0), keyFingerprint: decoder.decodeInt32ForKey("f", orElse: 0)) + default: + assertionFailure() + self = .remote(id: 0, accessHash: 0) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + switch try container.decode(Int32.self, forKey: "v") { + case SecretChatOutgoingFileValue.remote.rawValue: + self = .remote( + id: try container.decode(Int64.self, forKey: "i"), + accessHash: try container.decode(Int64.self, forKey: "a") + ) + case SecretChatOutgoingFileValue.uploadedRegular.rawValue: + self = .uploadedRegular( + id: try container.decode(Int64.self, forKey: "i"), + partCount: try container.decode(Int32.self, forKey: "p"), + md5Digest: try container.decode(String.self, forKey: "d"), + keyFingerprint: try container.decode(Int32.self, forKey: "f") + ) + case SecretChatOutgoingFileValue.uploadedLarge.rawValue: + self = .uploadedLarge( + id: try container.decode(Int64.self, forKey: "i"), + partCount: try container.decode(Int32.self, forKey: "p"), + keyFingerprint: try container.decode(Int32.self, forKey: "f") + ) + default: + assertionFailure() + self = .remote(id: 0, accessHash: 0) } } public func encode(_ encoder: PostboxEncoder) { switch self { - case let .remote(id, accessHash): - encoder.encodeInt32(SecretChatOutgoingFileValue.remote.rawValue, forKey: "v") - encoder.encodeInt64(id, forKey: "i") - encoder.encodeInt64(accessHash, forKey: "a") - case let .uploadedRegular(id, partCount, md5Digest, keyFingerprint): - encoder.encodeInt32(SecretChatOutgoingFileValue.uploadedRegular.rawValue, forKey: "v") - encoder.encodeInt64(id, forKey: "i") - encoder.encodeInt32(partCount, forKey: "p") - encoder.encodeString(md5Digest, forKey: "d") - encoder.encodeInt32(keyFingerprint, forKey: "f") - case let .uploadedLarge(id, partCount, keyFingerprint): - encoder.encodeInt32(SecretChatOutgoingFileValue.uploadedLarge.rawValue, forKey: "v") - encoder.encodeInt64(id, forKey: "i") - encoder.encodeInt32(partCount, forKey: "p") - encoder.encodeInt32(keyFingerprint, forKey: "f") + case let .remote(id, accessHash): + encoder.encodeInt32(SecretChatOutgoingFileValue.remote.rawValue, forKey: "v") + encoder.encodeInt64(id, forKey: "i") + encoder.encodeInt64(accessHash, forKey: "a") + case let .uploadedRegular(id, partCount, md5Digest, keyFingerprint): + encoder.encodeInt32(SecretChatOutgoingFileValue.uploadedRegular.rawValue, forKey: "v") + encoder.encodeInt64(id, forKey: "i") + encoder.encodeInt32(partCount, forKey: "p") + encoder.encodeString(md5Digest, forKey: "d") + encoder.encodeInt32(keyFingerprint, forKey: "f") + case let .uploadedLarge(id, partCount, keyFingerprint): + encoder.encodeInt32(SecretChatOutgoingFileValue.uploadedLarge.rawValue, forKey: "v") + encoder.encodeInt64(id, forKey: "i") + encoder.encodeInt32(partCount, forKey: "p") + encoder.encodeInt32(keyFingerprint, forKey: "f") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + switch self { + case let .remote(id, accessHash): + try container.encode(SecretChatOutgoingFileValue.remote.rawValue, forKey: "v") + try container.encode(id, forKey: "i") + try container.encode(accessHash, forKey: "a") + case let .uploadedRegular(id, partCount, md5Digest, keyFingerprint): + try container.encode(SecretChatOutgoingFileValue.uploadedRegular.rawValue, forKey: "v") + try container.encode(id, forKey: "i") + try container.encode(partCount, forKey: "p") + try container.encode(md5Digest, forKey: "d") + try container.encode(keyFingerprint, forKey: "f") + case let .uploadedLarge(id, partCount, keyFingerprint): + try container.encode(SecretChatOutgoingFileValue.uploadedLarge.rawValue, forKey: "v") + try container.encode(id, forKey: "i") + try container.encode(partCount, forKey: "p") + try container.encode(keyFingerprint, forKey: "f") } } } -public struct SecretChatOutgoingFile: PostboxCoding { +public struct SecretChatOutgoingFile: PostboxCoding, Codable { public let reference: SecretChatOutgoingFileReference public let size: Int64 public let key: SecretFileEncryptionKey @@ -68,12 +118,32 @@ public struct SecretChatOutgoingFile: PostboxCoding { self.key = SecretFileEncryptionKey(aesKey: decoder.decodeBytesForKey("k")!.makeData(), aesIv: decoder.decodeBytesForKey("i")!.makeData()) } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.reference = try container.decode(SecretChatOutgoingFileReference.self, forKey: "r") + self.size = try container.decode(Int64.self, forKey: "s64") + self.key = SecretFileEncryptionKey( + aesKey: try container.decode(Data.self, forKey: "k"), + aesIv: try container.decode(Data.self, forKey: "i") + ) + } + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObject(self.reference, forKey: "r") encoder.encodeInt64(self.size, forKey: "s64") encoder.encodeBytes(MemoryBuffer(data: self.key.aesKey), forKey: "k") encoder.encodeBytes(MemoryBuffer(data: self.key.aesIv), forKey: "i") } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.reference, forKey: "r") + try container.encode(self.size, forKey: "s64") + try container.encode(self.key.aesKey, forKey: "k") + try container.encode(self.key.aesIv, forKey: "i") + } } public enum SecretChatSequenceBasedLayer: Int32 { @@ -112,11 +182,72 @@ private enum SecretChatOutgoingOperationValue: Int32 { case noop = 12 case setMessageAutoremoveTimeout = 13 case terminate = 14 + case sendStandaloneMessage = 15 +} + +public struct StandaloneSecretMessageContents: Codable { + private enum CodingKeys: String, CodingKey { + case id = "i" + case text = "t" + case attributes = "a" + case media = "m" + case file = "f" + } + + public var id: Int64 + public var text: String + public var attributes: [MessageAttribute] + public var media: Media? + public var file: SecretChatOutgoingFile? + + public init(id: Int64, text: String, attributes: [MessageAttribute], media: Media?, file: SecretChatOutgoingFile?) { + self.id = id + self.text = text + self.attributes = attributes + self.media = media + self.file = file + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decode(Int64.self, forKey: .id) + self.text = try container.decode(String.self, forKey: .text) + + let attributes = try container.decode([Data].self, forKey: .attributes) + self.attributes = attributes.compactMap { attribute -> MessageAttribute? in + return PostboxDecoder(buffer: MemoryBuffer(data: attribute)).decodeRootObject() as? MessageAttribute + } + self.media = (try container.decodeIfPresent(Data.self, forKey: .media)).flatMap { media in + return PostboxDecoder(buffer: MemoryBuffer(data: media)).decodeRootObject() as? Media + } + self.file = try container.decodeIfPresent(SecretChatOutgoingFile.self, forKey: .file) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.id, forKey: .id) + try container.encode(self.text, forKey: .text) + let attributes = self.attributes.map { attribute -> Data in + let innerEncoder = PostboxEncoder() + innerEncoder.encodeRootObject(attribute) + return innerEncoder.makeData() + } + try container.encode(attributes, forKey: .attributes) + try container.encodeIfPresent(self.media.flatMap { media in + let innerEncoder = PostboxEncoder() + innerEncoder.encodeRootObject(media) + return innerEncoder.makeData() + }, forKey: .media) + try container.encodeIfPresent(self.file, forKey: .file) + } } public enum SecretChatOutgoingOperationContents: PostboxCoding { case initialHandshakeAccept(gA: MemoryBuffer, accessHash: Int64, b: MemoryBuffer) case sendMessage(layer: SecretChatLayer, id: MessageId, file: SecretChatOutgoingFile?) + case sendStandaloneMessage(layer: SecretChatLayer, contents: StandaloneSecretMessageContents) case readMessagesContent(layer: SecretChatLayer, actionGloballyUniqueId: Int64, globallyUniqueIds: [Int64]) case deleteMessages(layer: SecretChatLayer, actionGloballyUniqueId: Int64, globallyUniqueIds: [Int64]) case screenshotMessages(layer: SecretChatLayer, actionGloballyUniqueId: Int64, globallyUniqueIds: [Int64], messageId: MessageId) @@ -137,6 +268,11 @@ public enum SecretChatOutgoingOperationContents: PostboxCoding { self = .initialHandshakeAccept(gA: decoder.decodeBytesForKey("g")!, accessHash: decoder.decodeInt64ForKey("h", orElse: 0), b: decoder.decodeBytesForKey("b")!) case SecretChatOutgoingOperationValue.sendMessage.rawValue: self = .sendMessage(layer: SecretChatLayer(rawValue: decoder.decodeInt32ForKey("l", orElse: 0))!, id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("i.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("i.n", orElse: 0), id: decoder.decodeInt32ForKey("i.i", orElse: 0)), file: decoder.decodeObjectForKey("f", decoder: { SecretChatOutgoingFile(decoder: $0) }) as? SecretChatOutgoingFile) + case SecretChatOutgoingOperationValue.sendStandaloneMessage.rawValue: + self = .sendStandaloneMessage( + layer: SecretChatLayer(rawValue: decoder.decodeInt32ForKey("l", orElse: 0))!, + contents: decoder.decodeCodable(StandaloneSecretMessageContents.self, forKey: "c") ?? StandaloneSecretMessageContents(id: 0, text: "", attributes: [], media: nil, file: nil) + ) case SecretChatOutgoingOperationValue.readMessagesContent.rawValue: self = .readMessagesContent(layer: SecretChatLayer(rawValue: decoder.decodeInt32ForKey("l", orElse: 0))!, actionGloballyUniqueId: decoder.decodeInt64ForKey("i", orElse: 0), globallyUniqueIds: decoder.decodeInt64ArrayForKey("u")) case SecretChatOutgoingOperationValue.deleteMessages.rawValue: @@ -187,6 +323,10 @@ public enum SecretChatOutgoingOperationContents: PostboxCoding { } else { encoder.encodeNil(forKey: "f") } + case let .sendStandaloneMessage(layer, contents): + encoder.encodeInt32(SecretChatOutgoingOperationValue.sendStandaloneMessage.rawValue, forKey: "r") + encoder.encodeInt32(layer.rawValue, forKey: "l") + encoder.encodeCodable(contents, forKey: "c") case let .readMessagesContent(layer, actionGloballyUniqueId, globallyUniqueIds): encoder.encodeInt32(SecretChatOutgoingOperationValue.readMessagesContent.rawValue, forKey: "r") encoder.encodeInt32(layer.rawValue, forKey: "l") diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 6317e1f974..29b95179ec 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -520,7 +520,7 @@ func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: Acco } if let _ = dataDetector { let detectedUrls = detectUrls(inputText) - if detectedUrls != currentQuery?.detectedUrls { + if detectedUrls != (currentQuery?.detectedUrls ?? []) { if !detectedUrls.isEmpty { return (UrlPreviewState(detectedUrls: detectedUrls), webpagePreview(account: context.account, urls: detectedUrls) |> mapToSignal { result -> Signal<(TelegramMediaWebpage, String)?, NoError> in diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 35eb70f6f4..a068e32b83 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -3741,17 +3741,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } else { var children: [UIAction] = [] - children.append(UIAction(title: self.strings?.TextFormat_Quote ?? "Quote", image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesQuote(strongSelf) - } - }) - var hasSpoilers = true if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { hasSpoilers = false } + if hasSpoilers { + children.append(UIAction(title: self.strings?.TextFormat_Quote ?? "Quote", image: nil) { [weak self] (action) in + if let strongSelf = self { + strongSelf.formatAttributesQuote(strongSelf) + } + }) + } + if hasSpoilers { children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in if let strongSelf = self {