From 0dca4a544c06397d6dc59715120c90836927f9d6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 6 Nov 2023 20:19:45 +0400 Subject: [PATCH 1/7] Bump version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 73fae8de01..91021a2282 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.2.3", + "app": "10.2.4", "bazel": "6.4.0", "xcode": "15.0" } From 3d44192e2ffb97b3fa4a960513a5502b5d89f7f5 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 4 Nov 2023 00:35:26 +0400 Subject: [PATCH 2/7] Improve code blocks (cherry picked from commit 94de15f0b68a565ae8c58e66e879036a1f2ca28e) --- submodules/Display/Source/TextNode.swift | 20 ++- .../TextCodeIcon.imageset/Contents.json | 12 ++ .../TextCodeIcon.imageset/codemini.pdf | 147 ++++++++++++++++++ .../Sources/StringWithAppliedEntities.swift | 8 +- 4 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/TextCodeIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/TextCodeIcon.imageset/codemini.pdf diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index c5e17b65e9..6326867e41 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -10,6 +10,10 @@ private let quoteIcon: UIImage = { return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed() }() +private let codeIcon: UIImage = { + return UIImage(bundleImageName: "Chat/Message/TextCodeIcon")!.precomposed() +}() + private final class TextNodeStrikethrough { let range: NSRange let frame: CGRect @@ -1363,7 +1367,9 @@ open class TextNode: ASDisplayNode { case .quote: additionalSegmentRightInset = blockQuoteIconInset case .code: - break + if segment.title != nil { + additionalSegmentRightInset = blockQuoteIconInset + } } } @@ -2245,7 +2251,17 @@ open class TextNode: ASDisplayNode { context.restoreGState() context.resetClip() case .code: - break + if blockQuote.data.title != nil { + let quoteRect = CGRect(origin: CGPoint(x: blockFrame.maxX - 4.0 - codeIcon.size.width, y: blockFrame.minY + 4.0), size: codeIcon.size) + context.saveGState() + context.translateBy(x: quoteRect.midX, y: quoteRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -quoteRect.midX, y: -quoteRect.midY) + context.clip(to: quoteRect, mask: codeIcon.cgImage!) + context.fill(quoteRect) + context.restoreGState() + context.resetClip() + } } let lineFrame = CGRect(origin: CGPoint(x: blockFrame.minX, y: blockFrame.minY), size: CGSize(width: lineWidth, height: blockFrame.height)) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/TextCodeIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/TextCodeIcon.imageset/Contents.json new file mode 100644 index 0000000000..52f3d73f1d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/TextCodeIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "codemini.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/TextCodeIcon.imageset/codemini.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/TextCodeIcon.imageset/codemini.pdf new file mode 100644 index 0000000000..97779fd7ee --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/TextCodeIcon.imageset/codemini.pdf @@ -0,0 +1,147 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.000000 0.677734 cm +0.000000 0.000000 0.000000 scn +3.086899 6.235367 m +3.411034 6.559502 3.411034 7.085029 3.086899 7.409164 c +2.762764 7.733299 2.237236 7.733299 1.913101 7.409164 c +3.086899 6.235367 l +h +0.000000 4.322266 m +-0.586899 4.909164 l +-0.911034 4.585029 -0.911034 4.059502 -0.586899 3.735367 c +0.000000 4.322266 l +h +1.913101 1.235367 m +2.237236 0.911232 2.762764 0.911232 3.086899 1.235367 c +3.411034 1.559502 3.411034 2.085029 3.086899 2.409164 c +1.913101 1.235367 l +h +1.913101 7.409164 m +-0.586899 4.909164 l +0.586899 3.735367 l +3.086899 6.235367 l +1.913101 7.409164 l +h +-0.586899 3.735367 m +1.913101 1.235367 l +3.086899 2.409164 l +0.586899 4.909164 l +-0.586899 3.735367 l +h +f +n +Q +q +-1.000000 0.000000 -0.000000 -1.000000 13.000000 9.322266 cm +0.000000 0.000000 0.000000 scn +3.086899 6.235367 m +3.411034 6.559502 3.411034 7.085029 3.086899 7.409164 c +2.762764 7.733299 2.237236 7.733299 1.913101 7.409164 c +3.086899 6.235367 l +h +0.000000 4.322266 m +-0.586899 4.909164 l +-0.911034 4.585029 -0.911034 4.059502 -0.586899 3.735367 c +0.000000 4.322266 l +h +1.913101 1.235367 m +2.237236 0.911232 2.762764 0.911232 3.086899 1.235367 c +3.411034 1.559502 3.411034 2.085029 3.086899 2.409164 c +1.913101 1.235367 l +h +1.913101 7.409164 m +-0.586899 4.909164 l +0.586899 3.735367 l +3.086899 6.235367 l +1.913101 7.409164 l +h +-0.586899 3.735367 m +1.913101 1.235367 l +3.086899 2.409164 l +0.586899 4.909164 l +-0.586899 3.735367 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 6.000000 -0.833008 cm +0.000000 0.000000 0.000000 scn +2.805218 9.631703 m +2.916396 10.076413 2.646014 10.527049 2.201305 10.638227 c +1.756595 10.749404 1.305959 10.479022 1.194782 10.034312 c +2.805218 9.631703 l +h +-0.805218 2.034312 m +-0.916396 1.589602 -0.646014 1.138967 -0.201305 1.027789 c +0.243405 0.916612 0.694041 1.186994 0.805218 1.631703 c +-0.805218 2.034312 l +h +1.194782 10.034312 m +-0.805218 2.034312 l +0.805218 1.631703 l +2.805218 9.631703 l +1.194782 10.034312 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2021 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 14.000000 10.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002111 00000 n +0000002134 00000 n +0000002307 00000 n +0000002381 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2440 +%%EOF \ No newline at end of file diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 63b88620a2..7eed41bb7d 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -215,9 +215,11 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti nsString = text as NSString } if let codeBlockTitleColor, let codeBlockAccentColor, let codeBlockBackgroundColor { - string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(kind: .code(language: language), title: language.flatMap { - NSAttributedString(string: $0.capitalized, font: boldFont.withSize(round(boldFont.pointSize * 0.8235294117647058)), textColor: codeBlockTitleColor) - }, color: codeBlockAccentColor, secondaryColor: nil, tertiaryColor: nil, backgroundColor: codeBlockBackgroundColor), range: range) + var title: NSAttributedString? + if let language, !language.isEmpty { + title = NSAttributedString(string: language.capitalized, font: boldFont.withSize(round(boldFont.pointSize * 0.8235294117647058)), textColor: codeBlockTitleColor) + } + string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(kind: .code(language: language), title: title, color: codeBlockAccentColor, secondaryColor: nil, tertiaryColor: nil, backgroundColor: codeBlockBackgroundColor), range: range) } case .BlockQuote: addFontAttributes(range, .blockQuote) From 96279df59bb78103008a76d8cca4a59cb3375c3a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 6 Nov 2023 20:18:59 +0400 Subject: [PATCH 3/7] 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 { From a7c63e1b5510cd0e11a45aaec4503d7a55f75e11 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 6 Nov 2023 22:38:43 +0400 Subject: [PATCH 4/7] Cherry-pick various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 14 +- .../Sources/AccountContext.swift | 9 +- .../Sources/DatePickerNode.swift | 7 + .../Sources/CreateGiveawayController.swift | 104 +- .../Sources/GiveawayInfoController.swift | 11 +- .../Sources/PremiumLimitScreen.swift | 4 + .../Sources/ChannelStatsController.swift | 69 +- .../State/UserLimitsConfiguration.swift | 9 +- .../Data/ConfigurationData.swift | 8 +- ...ChatMessageAttachedContentButtonNode.swift | 4 +- .../ChatMessageAttachedContentNode.swift | 889 +----------------- .../Chat/ChatMessageBubbleItemNode/BUILD | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 140 ++- .../ChatMessageGiftBubbleContentNode.swift | 8 +- ...ChatMessageGiveawayBubbleContentNode.swift | 8 +- .../ChatRecentActionsControllerNode.swift | 1 + .../Sources/ChatControllerInteraction.swift | 3 + .../Sources/EmojiStatusComponent.swift | 2 +- .../Sources/FetchVideoMediaResource.swift | 5 + .../Sources/PeerNameColorScreen.swift | 2 +- .../CountriesMultiselectionScreen.swift | 6 +- .../Sources/ShareWithPeersScreenState.swift | 28 +- .../Sources/StoryContainerScreen.swift | 6 +- .../TelegramUI/Sources/ChatController.swift | 59 ++ .../ChatInterfaceStateContextMenus.swift | 9 +- .../OverlayAudioPlayerControllerNode.swift | 1 + .../Sources/PeerInfo/PeerInfoScreen.swift | 51 +- .../Sources/SharedAccountContext.swift | 1 + 28 files changed, 433 insertions(+), 1026 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c0de01da10..120781da56 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10297,8 +10297,8 @@ Sorry for the inconvenience."; "Stats.Boosts.Gift" = "Gift"; "Stats.Boosts.TabBoosts_1" = "%@ Boost"; "Stats.Boosts.TabBoosts_any" = "%@ Boosts"; -"Stats.Boosts.TabGifts_1" = "%@ Boost"; -"Stats.Boosts.TabGifts_any" = "%@ Boosts"; +"Stats.Boosts.TabGifts_1" = "%@ Gift"; +"Stats.Boosts.TabGifts_any" = "%@ Gifts"; "Stats.Boosts.ToBeDistributed" = "To Be Distributed"; "Stats.Boosts.Unclaimed" = "Unclaimed"; "Stats.Boosts.GetBoosts" = "Get Boosts via Gifts"; @@ -10364,6 +10364,8 @@ Sorry for the inconvenience."; "Chat.Giveaway.Info.DidntWin" = "You didn't win a prize in this giveaway."; "Chat.Giveaway.Info.ViewPrize" = "View My Prize"; +"Chat.Giveaway.Info.FullDate" = "**%1$@** on **%2$@**"; + "Chat.Giveaway.Toast.NotAllowed" = "You can't participate in this giveaway."; "Chat.Giveaway.Toast.Participating" = "You are participating in this giveaway."; "Chat.Giveaway.Toast.NotQualified" = "You are not qualified for this giveaway yet."; @@ -10414,6 +10416,7 @@ Sorry for the inconvenience."; "ChannelBoost.EnableColors" = "Enable Colors"; "ChannelBoost.EnableColorsText" = "Your channel needs %1$@ to change channel color.\n\nAsk your **Premium** subscribers to boost your channel with this link:"; +"ChannelBoost.EnableColorsLevelText" = "Your channel needs **Level %1$@** to change channel color.\n\nAsk your **Premium** subscribers to boost your channel with this link:"; "ChannelBoost.BoostAgain" = "Boost Again"; "Settings.New" = "NEW"; @@ -10423,3 +10426,10 @@ Sorry for the inconvenience."; "Channel.ChannelColor" = "Channel Color"; "TextFormat.Code" = "Code"; + +"Notification.ChannelJoinedByYou" = "You joined the channel"; + +"CountriesList.SelectCountries" = "Select Countries"; +"CountriesList.SaveCountries" = "Save Countries"; +"CountriesList.SelectUpTo_1" = "select up to %@ country"; +"CountriesList.SelectUpTo_any" = "select up to %@ countries"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e62f757827..3f1bd1f793 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1064,7 +1064,7 @@ public protocol AccountContext: AnyObject { public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { - return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3) + return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5) } public let isPremiumDisabled: Bool @@ -1072,13 +1072,15 @@ public struct PremiumConfiguration { public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool public let boostsPerGiftCount: Int32 + public let minChannelNameColorLevel: Int32 - fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32) { + fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32) { self.isPremiumDisabled = isPremiumDisabled self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable self.boostsPerGiftCount = boostsPerGiftCount + self.minChannelNameColorLevel = minChannelNameColorLevel } public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration { @@ -1088,7 +1090,8 @@ public struct PremiumConfiguration { showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? false, showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? false, giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false, - boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3) + boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3), + minChannelNameColorLevel: Int32(data["channel_color_level_min"] as? Double ?? 5) ) } else { return .defaultValue diff --git a/submodules/DatePickerNode/Sources/DatePickerNode.swift b/submodules/DatePickerNode/Sources/DatePickerNode.swift index 06fb60598f..61ca6ce416 100644 --- a/submodules/DatePickerNode/Sources/DatePickerNode.swift +++ b/submodules/DatePickerNode/Sources/DatePickerNode.swift @@ -666,6 +666,13 @@ public final class DatePickerNode: ASDisplayNode { } } + if let date = calendar.date(from: dateComponents), date > self.maximumDate { + let maximumDateComponents = calendar.dateComponents([.hour, .minute, .day, .month, .year], from: self.maximumDate) + if let hour = maximumDateComponents.hour { + dateComponents.hour = hour - 1 + } + } + if let date = calendar.date(from: dateComponents), date >= self.minimumDate && date < self.maximumDate { let updatedState = State(minDate: self.state.minDate, maxDate: self.state.maxDate, date: date, displayingMonthSelection: self.state.displayingMonthSelection, displayingDateSelection: self.state.displayingDateSelection, displayingTimeSelection: self.state.displayingTimeSelection, selectedMonth: monthNode.month) self.updateState(updatedState, animated: false) diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 6b9b7dbb02..8eb6fa8688 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -896,58 +896,58 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio buyActionImpl = { [weak controller] in let state = stateValue.with { $0 } - guard let products = productsValue.with({ $0 }), !products.isEmpty else { - return - } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - var selectedProduct: PremiumGiftProduct? - let selectedMonths = state.selectedMonths ?? 12 - switch state.mode { - case .giveaway: - if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == state.subscriptions }) { - selectedProduct = product - } - case .gift: - if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == 1 }) { - selectedProduct = product - } - } - - guard let selectedProduct else { - let alertController = textAlertController(context: context, title: presentationData.strings.BoostGift_ReduceQuantity_Title, text: presentationData.strings.BoostGift_ReduceQuantity_Text("\(state.subscriptions)", "\(selectedMonths)", "\(25)").string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BoostGift_ReduceQuantity_Reduce, action: { - updateState { state in - var updatedState = state - updatedState.subscriptions = 25 - return updatedState - } - })], parseMarkdown: true) - presentControllerImpl?(alertController) - return - } - - let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount - - let purpose: AppStoreTransactionPurpose - let quantity: Int32 - switch state.mode { - case .giveaway: - purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount) - quantity = selectedProduct.giftOption.storeQuantity - case .gift: - purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) - quantity = Int32(state.peers.count) - } - - updateState { state in - var updatedState = state - updatedState.updating = true - return updatedState - } - + switch subject { case .generic: + guard let products = productsValue.with({ $0 }), !products.isEmpty else { + return + } + var selectedProduct: PremiumGiftProduct? + let selectedMonths = state.selectedMonths ?? 12 + switch state.mode { + case .giveaway: + if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == state.subscriptions }) { + selectedProduct = product + } + case .gift: + if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == 1 }) { + selectedProduct = product + } + } + + guard let selectedProduct else { + let alertController = textAlertController(context: context, title: presentationData.strings.BoostGift_ReduceQuantity_Title, text: presentationData.strings.BoostGift_ReduceQuantity_Text("\(state.subscriptions)", "\(selectedMonths)", "\(25)").string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BoostGift_ReduceQuantity_Reduce, action: { + updateState { state in + var updatedState = state + updatedState.subscriptions = 25 + return updatedState + } + })], parseMarkdown: true) + presentControllerImpl?(alertController) + return + } + + updateState { state in + var updatedState = state + updatedState.updating = true + return updatedState + } + + let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount + + let purpose: AppStoreTransactionPurpose + let quantity: Int32 + switch state.mode { + case .giveaway: + purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount) + quantity = selectedProduct.giftOption.storeQuantity + case .gift: + purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) + quantity = Int32(state.peers.count) + } + let _ = (context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).startStandalone(next: { [weak controller] available in if available, let inAppPurchaseManager = context.inAppPurchaseManager { @@ -1033,6 +1033,12 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio } }) case let .prepaid(prepaidGiveaway): + updateState { state in + var updatedState = state + updatedState.updating = true + return updatedState + } + let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time) |> deliverOnMainQueue).startStandalone(completed: { if let controller, let navigationController = controller.navigationController as? NavigationController { diff --git a/submodules/PremiumUI/Sources/GiveawayInfoController.swift b/submodules/PremiumUI/Sources/GiveawayInfoController.swift index 981c4a7ada..390a02f78b 100644 --- a/submodules/PremiumUI/Sources/GiveawayInfoController.swift +++ b/submodules/PremiumUI/Sources/GiveawayInfoController.swift @@ -57,7 +57,10 @@ public func presentGiveawayInfoController( switch giveawayInfo { case let .ongoing(start, status): - let startDate = stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings) + let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate( + stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat), + stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings) + ).string.trimmingCharacters(in: CharacterSet(charactersIn: "*")) title = presentationData.strings.Chat_Giveaway_Info_Title @@ -123,7 +126,11 @@ public func presentGiveawayInfoController( text = "\(intro)\n\n\(ending)\(participation)" case let .finished(status, start, finish, _, activatedCount): - let startDate = stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings) + let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate( + stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat), + stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings) + ).string.trimmingCharacters(in: CharacterSet(charactersIn: "*")) + let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings) title = presentationData.strings.Chat_Giveaway_Info_EndedTitle diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 0ecbf43532..60b99f9dae 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -517,6 +517,10 @@ public class PremiumLimitDisplayComponent: Component { countWidth = 51.0 case 4: countWidth = 60.0 + case 5: + countWidth = 74.0 + case 6: + countWidth = 88.0 default: countWidth = 51.0 } diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index de36e834e4..82e7b01c0e 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -22,7 +22,7 @@ import ShareController import ItemListPeerActionItem import PremiumUI -private let maxUsersDisplayedLimit: Int32 = 5 +private let initialBoostersDisplayedLimit: Int32 = 5 private final class ChannelStatsControllerArguments { let context: AccountContext @@ -652,17 +652,20 @@ public enum ChannelStatsSection { private struct ChannelStatsControllerState: Equatable { let section: ChannelStatsSection let boostersExpanded: Bool + let moreBoostersDisplayed: Int32 let giftsSelected: Bool init() { self.section = .stats self.boostersExpanded = false + self.moreBoostersDisplayed = 0 self.giftsSelected = false } - init(section: ChannelStatsSection, boostersExpanded: Bool, giftsSelected: Bool) { + init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool) { self.section = section self.boostersExpanded = boostersExpanded + self.moreBoostersDisplayed = moreBoostersDisplayed self.giftsSelected = giftsSelected } @@ -673,6 +676,9 @@ private struct ChannelStatsControllerState: Equatable { if lhs.boostersExpanded != rhs.boostersExpanded { return false } + if lhs.moreBoostersDisplayed != rhs.moreBoostersDisplayed { + return false + } if lhs.giftsSelected != rhs.giftsSelected { return false } @@ -680,15 +686,19 @@ private struct ChannelStatsControllerState: Equatable { } func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected) } func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected) + } + + func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState { + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected) } func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, giftsSelected: giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected) } } @@ -826,20 +836,32 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p var boosterIndex: Int32 = 0 var boosters: [ChannelBoostersContext.State.Boost] = selectedState.boosts - var effectiveExpanded = state.boostersExpanded - if boosters.count > maxUsersDisplayedLimit && !state.boostersExpanded { - boosters = Array(boosters.prefix(Int(maxUsersDisplayedLimit))) + + var limit: Int32 + if state.boostersExpanded { + limit = 25 + state.moreBoostersDisplayed } else { - effectiveExpanded = true + limit = initialBoostersDisplayedLimit } + boosters = Array(boosters.prefix(Int(limit))) for booster in boosters { entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster)) boosterIndex += 1 } - if !effectiveExpanded { - entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(Int32(selectedState.count) - maxUsersDisplayedLimit))) + let totalBoostsCount = boosters.reduce(Int32(0)) { partialResult, boost in + return partialResult + boost.multiplier + } + + if totalBoostsCount < selectedState.count { + let moreCount: Int32 + if !state.boostersExpanded { + moreCount = min(80, selectedState.count - totalBoostsCount) + } else { + moreCount = min(200, selectedState.count - totalBoostsCount) + } + entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(moreCount))) } } @@ -862,8 +884,8 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p } public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, statsDatacenterId: Int32?) -> ViewController { - let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false), ignoreRepeated: true) - let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false)) + let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false)) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -993,7 +1015,20 @@ public func channelStatsController(context: AccountContext, updatedPresentationD pushImpl?(controller) }, expandBoosters: { - updateState { $0.withUpdatedBoostersExpanded(true) } + var giftsSelected = false + updateState { state in + giftsSelected = state.giftsSelected + if state.boostersExpanded { + return state.withUpdatedMoreBoostersDisplayed(state.moreBoostersDisplayed + 50) + } else { + return state.withUpdatedBoostersExpanded(true) + } + } + if giftsSelected { + giftsContext.loadMore() + } else { + boostsContext.loadMore() + } }, openGifts: { let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic) @@ -1075,12 +1110,6 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } }) } - controller.visibleBottomContentOffsetChanged = { offset in - let state = stateValue.with { $0 } - if case let .known(value) = offset, value < 510.0, case .boosts = state.section, state.boostersExpanded { - boostsContext.loadMore() - } - } controller.titleControlValueChanged = { value in updateState { $0.withUpdatedSection(value == 1 ? .boosts : .stats) } } diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index b5f99b7392..5922c61571 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -25,7 +25,6 @@ public struct UserLimitsConfiguration: Equatable { public let maxGiveawayChannelsCount: Int32 public let maxGiveawayCountriesCount: Int32 public let maxGiveawayPeriodSeconds: Int32 - public let minChannelNameColorLevel: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -51,8 +50,7 @@ public struct UserLimitsConfiguration: Equatable { maxStoriesSuggestedReactions: 1, maxGiveawayChannelsCount: 10, maxGiveawayCountriesCount: 10, - maxGiveawayPeriodSeconds: 86400 * 7, - minChannelNameColorLevel: 10 + maxGiveawayPeriodSeconds: 86400 * 7 ) } @@ -79,8 +77,7 @@ public struct UserLimitsConfiguration: Equatable { maxStoriesSuggestedReactions: Int32, maxGiveawayChannelsCount: Int32, maxGiveawayCountriesCount: Int32, - maxGiveawayPeriodSeconds: Int32, - minChannelNameColorLevel: Int32 + maxGiveawayPeriodSeconds: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -105,7 +102,6 @@ public struct UserLimitsConfiguration: Equatable { self.maxGiveawayChannelsCount = maxGiveawayChannelsCount self.maxGiveawayCountriesCount = maxGiveawayCountriesCount self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds - self.minChannelNameColorLevel = minChannelNameColorLevel } } @@ -153,6 +149,5 @@ extension UserLimitsConfiguration { self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount) self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount) self.maxGiveawayPeriodSeconds = getGeneralValue("giveaway_period_max", orElse: defaultValue.maxGiveawayPeriodSeconds) - self.minChannelNameColorLevel = getGeneralValue("channel_color_level_min", orElse: defaultValue.minChannelNameColorLevel) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index ca16e2f12b..48a83d7b9a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -59,7 +59,6 @@ public enum EngineConfiguration { public let maxGiveawayChannelsCount: Int32 public let maxGiveawayCountriesCount: Int32 public let maxGiveawayPeriodSeconds: Int32 - public let minChannelNameColorLevel: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -88,8 +87,7 @@ public enum EngineConfiguration { maxStoriesSuggestedReactions: Int32, maxGiveawayChannelsCount: Int32, maxGiveawayCountriesCount: Int32, - maxGiveawayPeriodSeconds: Int32, - minChannelNameColorLevel: Int32 + maxGiveawayPeriodSeconds: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -114,7 +112,6 @@ public enum EngineConfiguration { self.maxGiveawayChannelsCount = maxGiveawayChannelsCount self.maxGiveawayCountriesCount = maxGiveawayCountriesCount self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds - self.minChannelNameColorLevel = minChannelNameColorLevel } } } @@ -174,8 +171,7 @@ public extension EngineConfiguration.UserLimits { maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions, maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount, maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount, - maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds, - minChannelNameColorLevel: userLimitsConfiguration.minChannelNameColorLevel + maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds ) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift index 07d7945ac3..ecb14a0075 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift @@ -7,7 +7,7 @@ import ChatPresentationInterfaceState import ShimmerEffect private let buttonFont = Font.semibold(14.0) -private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate) +private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate) public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { private let textNode: TextNode @@ -60,7 +60,7 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton shimmerEffectNode = current } else { shimmerEffectNode = ShimmerEffectForegroundNode() - shimmerEffectNode.cornerRadius = 5.0 + shimmerEffectNode.cornerRadius = 6.0 self.insertSubnode(shimmerEffectNode, at: 0) self.shimmerEffectNode = shimmerEffectNode } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 63e599cd45..9e04fd426e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -850,6 +850,11 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } } + if let _ = message.adAttribute { + actualSize.height += 2.0 + backgroundInsets.bottom += 2.0 + } + return (actualSize, { animation, synchronousLoads, applyInfo in guard let self else { return @@ -1214,890 +1219,6 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { }) }) }) - - /*var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0) - if displayLine { - horizontalInsets.left += 10.0 - horizontalInsets.right += 9.0 - } - - var titleBeforeMedia = false - var preferMediaBeforeText = false - var preferMediaAspectFilled = false - if let (_, flags) = mediaAndFlags { - preferMediaBeforeText = flags.contains(.preferMediaBeforeText) - preferMediaAspectFilled = flags.contains(.preferMediaAspectFilled) - titleBeforeMedia = flags.contains(.titleBeforeMedia) - } - - var contentMode: InteractiveMediaNodeContentMode = preferMediaAspectFilled ? .aspectFill : .aspectFit - - var edited = false - if attributes.updatingMedia != nil { - edited = true - } - var viewCount: Int? - var dateReplies = 0 - var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message) - if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) { - dateReactionsAndPeers = ([], []) - } - for attribute in message.attributes { - if let attribute = attribute as? EditedMessageAttribute { - edited = !attribute.isHidden - } else if let attribute = attribute as? ViewCountMessageAttribute { - viewCount = attribute.count - } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - dateReplies = Int(attribute.count) - } - } - } - - let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData) - - var webpageGalleryMediaCount: Int? - for media in message.media { - if let media = media as? TelegramMediaWebpage { - if case let .Loaded(content) = media.content, let instantPage = content.instantPage, let image = content.image { - switch instantPageType(of: content) { - case .album: - let count = instantPageGalleryMedia(webpageId: media.webpageId, page: instantPage, galleryMedia: image).count - if count > 1 { - webpageGalleryMediaCount = count - } - default: - break - } - } - } - } - - var textString: NSAttributedString? - var inlineImageDimensions: CGSize? - var inlineImageSize: CGSize? - var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var textCutout = TextNodeCutout() - var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude - var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))? - var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)))? - - var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)? - - let topTitleString = NSMutableAttributedString() - - let string = NSMutableAttributedString() - var notEmpty = false - - let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing - - if let title = title, !title.isEmpty { - if titleBeforeMedia { - topTitleString.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) - } else { - string.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) - notEmpty = true - } - } - - if let subtitle = subtitle, subtitle.length > 0 { - if notEmpty { - string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor)) - } - let updatedSubtitle = NSMutableAttributedString() - updatedSubtitle.append(subtitle) - updatedSubtitle.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length)) - updatedSubtitle.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length)) - string.append(updatedSubtitle) - notEmpty = true - } - - if let text = text, !text.isEmpty { - if notEmpty { - string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor)) - } - if let entities = entities { - string.append(stringWithAppliedEntities(text, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true)) - } else { - string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: messageTheme.primaryTextColor)) - } - notEmpty = true - } - - textString = string - if string.length > 1000 { - textString = string.attributedSubstring(from: NSMakeRange(0, 1000)) - } - - var isReplyThread = false - if case .replyThread = chatLocation { - isReplyThread = true - } - - var skipStandardStatus = false - var isImage = false - var isFile = false - - var automaticPlayback = false - - var textStatusType: ChatMessageDateAndStatusType? - var imageStatusType: ChatMessageDateAndStatusType? - var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? - - if let (media, flags) = mediaAndFlags { - if let file = media as? TelegramMediaFile { - if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { - isImage = true - } else if file.isInstantVideo { - isImage = true - } else if file.isVideo { - isImage = true - } else if file.isSticker || file.isAnimatedSticker { - isImage = true - } else { - isFile = true - } - } else if let _ = media as? TelegramMediaImage { - if !flags.contains(.preferMediaInline) { - isImage = true - } - } else if let _ = media as? TelegramMediaWebFile { - isImage = true - } else if let _ = media as? WallpaperPreviewMedia { - isImage = true - } else if let _ = media as? TelegramMediaStory { - isImage = true - } - } - - if preferMediaBeforeText, let textString, textString.length != 0 { - isImage = false - } - - var statusInText = !isImage - if let textString { - if textString.length == 0 { - statusInText = false - } - } else { - statusInText = false - } - - switch preparePosition { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): - if let count = webpageGalleryMediaCount { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string), iconName: nil) - skipStandardStatus = isImage - } else if let mediaBadge = mediaBadge { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge), iconName: nil) - } else { - skipStandardStatus = isFile - } - - if !skipStandardStatus { - if incoming { - if isImage { - imageStatusType = .ImageIncoming - } else { - textStatusType = .BubbleIncoming - } - } else { - if message.flags.contains(.Failed) { - if isImage { - imageStatusType = .ImageOutgoing(.Failed) - } else { - textStatusType = .BubbleOutgoing(.Failed) - } - } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { - if isImage { - imageStatusType = .ImageOutgoing(.Sending) - } else { - textStatusType = .BubbleOutgoing(.Sending) - } - } else { - if isImage { - imageStatusType = .ImageOutgoing(.Sent(read: messageRead)) - } else { - textStatusType = .BubbleOutgoing(.Sent(read: messageRead)) - } - } - } - } - default: - break - } - - let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in - ChatMessageDateAndStatus( - type: statusType, - edited: edited, - viewCount: viewCount, - dateReactions: dateReactionsAndPeers.reactions, - dateReactionPeers: dateReactionsAndPeers.peers, - dateReplies: dateReplies, - isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - dateText: dateText - ) - } - - if let (media, flags) = mediaAndFlags { - if let file = media as? TelegramMediaFile { - if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else if file.isInstantVideo { - let displaySize = CGSize(width: 212.0, height: 212.0) - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, topMessage: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload, 0.0) - initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight - contentInstantVideoSizeAndApply = (videoLayout, apply) - } else if file.isVideo { - var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none - - if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) { - automaticDownload = .full - } else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) { - automaticDownload = .prefetch - } - if file.isAnimated { - automaticPlayback = context.sharedContext.energyUsageSettings.autoplayGif - } else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo { - var willDownloadOrLocal = false - if case .full = automaticDownload { - willDownloadOrLocal = true - } else { - willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil - } - if willDownloadOrLocal { - automaticPlayback = true - contentMode = .aspectFill - } - } - - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else if file.isSticker || file.isAnimatedSticker { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - - let statusType: ChatMessageDateAndStatusType - if incoming { - statusType = .BubbleIncoming - } else { - if message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { - statusType = .BubbleOutgoing(.Sending) - } else { - statusType = .BubbleOutgoing(.Sent(read: messageRead)) - } - } - - let (_, refineLayout) = contentFileLayout(ChatMessageInteractiveFileNode.Arguments( - context: context, - presentationData: presentationData, - message: message, - topMessage: message, - associatedData: associatedData, - chatLocation: chatLocation, - attributes: attributes, - isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - forcedIsEdited: false, - file: file, - automaticDownload: automaticDownload, - incoming: incoming, - isRecentActions: false, - forcedResourceStatus: associatedData.forcedResourceStatus, - dateAndStatusType: statusType, - displayReactions: false, - messageSelection: nil, - layoutConstants: layoutConstants, - constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height), - controllerInteraction: controllerInteraction - )) - refineContentFileLayout = refineLayout - } - } else if let image = media as? TelegramMediaImage { - if !flags.contains(.preferMediaInline) { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { - inlineImageDimensions = dimensions.cgSize - - if image != currentImage || !currentMediaIsInline { - updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) - } - } - } else if let image = media as? TelegramMediaWebFile { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else if let wallpaper = media as? WallpaperPreviewMedia { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { - skipStandardStatus = true - } - } else if let story = media as? TelegramMediaStory { - var media: Media? - if let storyValue = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyValue { - media = item.media - } - - var automaticDownload = false - if let media { - automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: media) - } - - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, story, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } - } - - if let _ = inlineImageDimensions { - inlineImageSize = CGSize(width: 53.0, height: 53.0) - - if let inlineImageSize = inlineImageSize { - textCutout.topRight = CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0) - } - } - - return (initialWidth, { constrainedSize, position in - var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 0.0, right: horizontalInsets.right) - - switch position { - case let .linear(topNeighbor, bottomNeighbor): - switch topNeighbor { - case .None: - insets.top += 10.0 - default: - break - } - switch bottomNeighbor { - case .None: - insets.bottom += 12.0 - default: - insets.bottom += 0.0 - } - default: - break - } - - let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) - - var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge? - if let _ = additionalImageBadgeContent { - updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge() - } - - let upatedTextCutout = textCutout - - - let (topTitleLayout, topTitleApply) = topTitleAsyncLayout(TextNodeLayoutArguments(attributedString: topTitleString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets())) - - var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? - if statusInText, let textStatusType = textStatusType { - let trailingContentWidth: CGFloat - if textLayout.hasRTL { - trailingContentWidth = 10000.0 - } else { - trailingContentWidth = textLayout.trailingLineWidth - } - statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( - context: context, - presentationData: presentationData, - edited: edited, - impressionCount: viewCount, - dateText: dateText, - type: textStatusType, - layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), - constrainedSize: textConstrainedSize, - availableReactions: associatedData.availableReactions, - reactions: dateReactionsAndPeers.reactions, - reactionPeers: dateReactionsAndPeers.peers, - displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser, - replyCount: dateReplies, - isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message), - animationCache: controllerInteraction.presentationContext.animationCache, - animationRenderer: controllerInteraction.presentationContext.animationRenderer - )) - } - let _ = statusSuggestedWidthAndContinue - - var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) - - textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) - - let mainColor: UIColor - if !incoming { - mainColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor - } else { - var authorNameColor: UIColor? - let author = message.author - if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { - authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] } - if let rawAuthorNameColor = authorNameColor { - var dimColors = false - switch presentationData.theme.theme.name { - case .builtin(.nightAccent), .builtin(.night): - dimColors = true - default: - break - } - if dimColors { - var hue: CGFloat = 0.0 - var saturation: CGFloat = 0.0 - var brightness: CGFloat = 0.0 - rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) - authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) - } - } - } - - if let authorNameColor { - mainColor = authorNameColor - } else { - mainColor = presentationData.theme.theme.chat.message.incoming.accentTextColor - } - } - - var boundingSize = textFrame.size - if titleBeforeMedia { - boundingSize.height += topTitleLayout.size.height + 4.0 - boundingSize.width = max(boundingSize.width, topTitleLayout.size.width) - } - if let inlineImageSize = inlineImageSize { - if boundingSize.height < inlineImageSize.height { - boundingSize.height = inlineImageSize.height - } - } - - if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { - boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0) - } - - var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))? - if let refineContentImageLayout = refineContentImageLayout { - let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0)) - finalizeContentImageLayout = finalizeImageLayout - - boundingSize.width = max(boundingSize.width, refinedWidth) - } - var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode))? - if let refineContentFileLayout = refineContentFileLayout { - let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize) - finalizeContentFileLayout = finalizeFileLayout - - boundingSize.width = max(boundingSize.width, refinedWidth) - } - - if let (videoLayout, _) = contentInstantVideoSizeAndApply { - boundingSize.width = max(boundingSize.width, videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight) - } - - var imageApply: (() -> Void)? - if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions { - let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) - let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor) - imageApply = imageLayout(arguments) - } - - var continueActionButtonLayout: ((CGFloat, CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))? - if let actionTitle = actionTitle, !isPreview { - var buttonIconImage: UIImage? - var buttonHighlightedIconImage: UIImage? - var cornerIcon = false - let titleColor: UIColor - let titleHighlightedColor: UIColor - if incoming { - if let actionIcon { - switch actionIcon { - case .instant: - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! - case .link: - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! - cornerIcon = true - } - } - titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor - let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColor.fill[0] - } else { - if let actionIcon { - switch actionIcon { - case .instant: - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! - case .link: - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! - cornerIcon = true - } - } - titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor - let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColor.fill[0] - } - let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, nil, buttonIconImage, buttonHighlightedIconImage, cornerIcon, actionTitle, titleColor, titleHighlightedColor, false) - boundingSize.width = max(buttonWidth, boundingSize.width) - continueActionButtonLayout = continueLayout - } - - boundingSize.width += insets.left + insets.right - boundingSize.height += insets.top + insets.bottom - - return (boundingSize.width, { boundingWidth in - var adjustedBoundingSize = boundingSize - - var imageFrame: CGRect? - if let inlineImageSize = inlineImageSize { - imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right + 4.0, y: 0.0), size: inlineImageSize) - } - - var contentImageSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)? - if let finalizeContentImageLayout = finalizeContentImageLayout { - let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right) - contentImageSizeAndApply = (size, apply) - - var imageHeightAddition = size.height - if textFrame.size.height > CGFloat.ulpOfOne { - imageHeightAddition += 2.0 - } - - adjustedBoundingSize.height += imageHeightAddition + 7.0 - } - - var contentFileSizeAndApply: (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)? - if let finalizeContentFileLayout = finalizeContentFileLayout { - let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right) - contentFileSizeAndApply = (size, apply) - - var imageHeightAddition = size.height + 6.0 - if textFrame.size.height > CGFloat.ulpOfOne { - imageHeightAddition += 6.0 - } else { - imageHeightAddition += 7.0 - } - - adjustedBoundingSize.height += imageHeightAddition + 5.0 - } - - if let (videoLayout, _) = contentInstantVideoSizeAndApply { - let imageHeightAddition = videoLayout.contentSize.height + 6.0 - - adjustedBoundingSize.height += imageHeightAddition// + 5.0 - } - - var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))? - if let continueActionButtonLayout = continueActionButtonLayout { - let (size, apply) = continueActionButtonLayout(boundingWidth - 5.0 - insets.right, 38.0) - actionButtonSizeAndApply = (size, apply) - adjustedBoundingSize.height += 4.0 + size.height - if let text, !text.isEmpty { - if contentImageSizeAndApply == nil { - adjustedBoundingSize.height += 5.0 - } else if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - adjustedBoundingSize.height += 5.0 - } - } - } - - var statusSizeAndApply: ((CGSize), (ListViewItemUpdateAnimation) -> Void)? - if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { - statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right) - } - if let statusSizeAndApply = statusSizeAndApply { - adjustedBoundingSize.height += statusSizeAndApply.0.height - - if let imageFrame = imageFrame, statusSizeAndApply.0.height == 0.0 { - if statusInText { - adjustedBoundingSize.height = max(adjustedBoundingSize.height, imageFrame.maxY + 8.0 + 15.0) - } - } - } - - adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width) - - var contentMediaHeight: CGFloat? - if let (contentImageSize, _) = contentImageSizeAndApply { - contentMediaHeight = contentImageSize.height - } - - if let (contentFileSize, _) = contentFileSizeAndApply { - contentMediaHeight = contentFileSize.height - } - - if let (videoLayout, _) = contentInstantVideoSizeAndApply { - contentMediaHeight = videoLayout.contentSize.height - } - - var textVerticalOffset: CGFloat = 0.0 - if titleBeforeMedia { - textVerticalOffset += topTitleLayout.size.height + 4.0 - } - if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - textVerticalOffset += contentMediaHeight + 7.0 - } - let adjustedTextFrame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) - - var statusFrame: CGRect? - if let statusSizeAndApply = statusSizeAndApply { - var finalStatusFrame = CGRect(origin: CGPoint(x: adjustedTextFrame.minX, y: adjustedTextFrame.maxY), size: statusSizeAndApply.0) - if let imageFrame = imageFrame { - if finalStatusFrame.maxY < imageFrame.maxY + 10.0 { - finalStatusFrame.origin.y = max(finalStatusFrame.minY, imageFrame.maxY + 2.0) - if finalStatusFrame.height == 0.0 { - finalStatusFrame.origin.y += 14.0 - - adjustedBoundingSize.height += 14.0 - } - } - } - statusFrame = finalStatusFrame - } - - return (adjustedBoundingSize, { [weak self] animation, synchronousLoads, applyInfo in - if let strongSelf = self { - strongSelf.context = context - strongSelf.message = message - strongSelf.media = mediaAndFlags?.0 - strongSelf.theme = presentationData.theme - - let backgroundView: UIImageView - if let current = strongSelf.backgroundView { - backgroundView = current - } else { - backgroundView = UIImageView() - strongSelf.backgroundView = backgroundView - strongSelf.view.insertSubview(backgroundView, at: 0) - } - - if backgroundView.image == nil { - backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme) - } - backgroundView.tintColor = mainColor - - animation.animator.updateFrame(layer: backgroundView.layer, frame: CGRect(origin: CGPoint(x: 11.0, y: insets.top - 3.0), size: CGSize(width: adjustedBoundingSize.width - 4.0 - insets.right, height: adjustedBoundingSize.height - insets.top - insets.bottom + 4.0)), completion: nil) - backgroundView.isHidden = !displayLine - - //strongSelf.borderColor = UIColor.red.cgColor - //strongSelf.borderWidth = 2.0 - - strongSelf.textNode.textNode.displaysAsynchronously = !isPreview - - let _ = topTitleApply() - strongSelf.topTitleNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: insets.top), size: topTitleLayout.size) - - let _ = textApply(TextNodeWithEntities.Arguments( - context: context, - cache: animationCache, - renderer: animationRenderer, - placeholderColor: messageTheme.mediaPlaceholderColor, - attemptSynchronous: synchronousLoads - )) - switch strongSelf.visibility { - case .none: - strongSelf.textNode.visibilityRect = nil - case let .visible(_, subRect): - var subRect = subRect - subRect.origin.x = 0.0 - subRect.size.width = 10000.0 - strongSelf.textNode.visibilityRect = subRect - } - - if let imageFrame = imageFrame { - if let updateImageSignal = updateInlineImageSignal { - strongSelf.inlineImageNode.setSignal(updateImageSignal) - } - animation.animator.updateFrame(layer: strongSelf.inlineImageNode.layer, frame: imageFrame, completion: nil) - if strongSelf.inlineImageNode.supernode == nil { - strongSelf.addSubnode(strongSelf.inlineImageNode) - } - - if let imageApply = imageApply { - imageApply() - } - } else if strongSelf.inlineImageNode.supernode != nil { - strongSelf.inlineImageNode.removeFromSupernode() - } - - if let (contentImageSize, contentImageApply) = contentImageSizeAndApply { - let contentImageNode = contentImageApply(animation, synchronousLoads) - if strongSelf.contentImageNode !== contentImageNode { - strongSelf.contentImageNode = contentImageNode - contentImageNode.activatePinch = { sourceNode in - controllerInteraction.activateMessagePinch(sourceNode) - } - strongSelf.addSubnode(contentImageNode) - contentImageNode.activateLocalContent = { [weak strongSelf] mode in - if let strongSelf = strongSelf { - strongSelf.openMedia?(mode) - } - } - contentImageNode.updateMessageReaction = { [weak controllerInteraction] message, value in - guard let controllerInteraction = controllerInteraction else { - return - } - controllerInteraction.updateMessageReaction(message, value) - } - contentImageNode.visibility = strongSelf.visibility != .none - } - let _ = contentImageApply(animation, synchronousLoads) - var contentImageFrame: CGRect - if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) - if titleBeforeMedia { - contentImageFrame.origin.y += topTitleLayout.size.height + 4.0 - } - } else { - contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize) - } - - contentImageNode.frame = contentImageFrame - } else if let contentImageNode = strongSelf.contentImageNode { - contentImageNode.visibility = false - contentImageNode.removeFromSupernode() - strongSelf.contentImageNode = nil - } - - if let updatedAdditionalImageBadge = updatedAdditionalImageBadge, let contentImageNode = strongSelf.contentImageNode, let contentImageSize = contentImageSizeAndApply?.0 { - if strongSelf.additionalImageBadgeNode != updatedAdditionalImageBadge { - strongSelf.additionalImageBadgeNode?.removeFromSupernode() - } - strongSelf.additionalImageBadgeNode = updatedAdditionalImageBadge - contentImageNode.addSubnode(updatedAdditionalImageBadge) - if mediaBadge != nil { - updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, animated: false) - updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 0.0, height: 0.0)) - } else { - updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, alignment: .right, animated: false) - updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: contentImageSize.width - 6.0, y: contentImageSize.height - 18.0 - 6.0), size: CGSize(width: 0.0, height: 0.0)) - } - } else if let additionalImageBadgeNode = strongSelf.additionalImageBadgeNode { - strongSelf.additionalImageBadgeNode = nil - additionalImageBadgeNode.removeFromSupernode() - } - - if let (contentFileSize, contentFileApply) = contentFileSizeAndApply { - let contentFileNode = contentFileApply(synchronousLoads, animation, applyInfo) - if strongSelf.contentFileNode !== contentFileNode { - strongSelf.contentFileNode = contentFileNode - strongSelf.addSubnode(contentFileNode) - contentFileNode.activateLocalContent = { [weak strongSelf] in - if let strongSelf = strongSelf { - strongSelf.openMedia?(.default) - } - } - contentFileNode.requestUpdateLayout = { [weak strongSelf] _ in - if let strongSelf = strongSelf { - strongSelf.requestUpdateLayout?() - } - } - } - if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize) - } else { - contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 8.0 : 7.0)), size: contentFileSize) - } - } else if let contentFileNode = strongSelf.contentFileNode { - contentFileNode.removeFromSupernode() - strongSelf.contentFileNode = nil - } - - if let (videoLayout, apply) = contentInstantVideoSizeAndApply { - let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), animation) - if strongSelf.contentInstantVideoNode !== contentInstantVideoNode { - strongSelf.contentInstantVideoNode = contentInstantVideoNode - strongSelf.addSubnode(contentInstantVideoNode) - } - if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: videoLayout.contentSize) - } else { - contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: videoLayout.contentSize) - } - } else if let contentInstantVideoNode = strongSelf.contentInstantVideoNode { - contentInstantVideoNode.removeFromSupernode() - strongSelf.contentInstantVideoNode = nil - } - - strongSelf.textNode.textNode.frame = adjustedTextFrame - if let statusSizeAndApply = statusSizeAndApply, let statusFrame = statusFrame { - if strongSelf.statusNode.supernode == nil { - strongSelf.addSubnode(strongSelf.statusNode) - strongSelf.statusNode.frame = statusFrame - statusSizeAndApply.1(.None) - } else { - animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil) - statusSizeAndApply.1(animation) - } - } else if strongSelf.statusNode.supernode != nil { - strongSelf.statusNode.removeFromSupernode() - } - - if let (size, apply) = actionButtonSizeAndApply { - let buttonNode = apply() - - let buttonFrame = CGRect(origin: CGPoint(x: 12.0, y: adjustedBoundingSize.height - insets.bottom - size.height), size: size) - if buttonNode !== strongSelf.buttonNode { - strongSelf.buttonNode?.removeFromSupernode() - strongSelf.buttonNode = buttonNode - buttonNode.isUserInteractionEnabled = false - strongSelf.addSubnode(buttonNode) - buttonNode.pressed = { - if let strongSelf = self { - strongSelf.activateAction?() - } - } - buttonNode.frame = buttonFrame - } else { - animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil) - } - - let buttonSeparatorFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + 8.0, y: buttonFrame.minY - 2.0), size: CGSize(width: buttonFrame.width - 8.0 - 8.0, height: UIScreenPixel)) - - let buttonSeparatorLayer: SimpleLayer - if let current = strongSelf.buttonSeparatorLayer { - buttonSeparatorLayer = current - animation.animator.updateFrame(layer: buttonSeparatorLayer, frame: buttonSeparatorFrame, completion: nil) - } else { - buttonSeparatorLayer = SimpleLayer() - strongSelf.buttonSeparatorLayer = buttonSeparatorLayer - strongSelf.layer.addSublayer(buttonSeparatorLayer) - buttonSeparatorLayer.frame = buttonSeparatorFrame - } - - buttonSeparatorLayer.backgroundColor = mainColor.withMultipliedAlpha(0.5).cgColor - } else { - if let buttonNode = strongSelf.buttonNode { - strongSelf.buttonNode = nil - buttonNode.removeFromSupernode() - } - - if let buttonSeparatorLayer = strongSelf.buttonSeparatorLayer { - strongSelf.buttonSeparatorLayer = nil - buttonSeparatorLayer.removeFromSuperlayer() - } - } - } - }) - }) - })*/ } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index 5b8b444a8d..c945e54830 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -79,6 +79,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 5f870a77b0..d41bd85eb4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -535,9 +535,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var swipeToReplyFeedback: HapticFeedback? private var nameNode: TextNode? + private var nameButtonNode: HighlightTrackingButtonNode? + private var nameHighlightNode: ASImageNode? + private var adminBadgeNode: TextNode? private var credibilityIconView: ComponentHostView? private var credibilityIconComponent: EmojiStatusComponent? + private var credibilityIconContent: EmojiStatusComponent.Content? + private var credibilityButtonNode: HighlightTrackingButtonNode? + private var credibilityHighlightNode: ASImageNode? + private var closeButtonNode: HighlightTrackingButtonNode? private var closeIconNode: ASImageNode? @@ -1064,7 +1071,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return .fail } } - + + if let nameButtonNode = strongSelf.nameButtonNode, nameButtonNode.frame.contains(point) { + return .fail + } + + if let credibilityButtonNode = strongSelf.credibilityButtonNode, credibilityButtonNode.frame.contains(point) { + return .fail + } + if let nameNode = strongSelf.nameNode, nameNode.frame.contains(point) { if let item = strongSelf.item { for attribute in item.message.attributes { @@ -1540,6 +1555,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var hasInstantVideo = false for contentNodeItemValue in contentNodeMessagesAndClasses { let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes) + if contentNodeItem.type == ChatMessageGiveawayBubbleContentNode.self { + maximumContentWidth = 260.0 + break + } if contentNodeItem.type == ChatMessageInstantVideoBubbleContentNode.self, !contentNodeItem.bubbleAttributes.isAttachment { maximumContentWidth = baseWidth - 20.0 hasInstantVideo = true @@ -2825,6 +2844,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return } + let themeUpdated = strongSelf.appliedItem?.presentationData.theme.theme !== item.presentationData.theme.theme let previousContextFrame = strongSelf.mainContainerNode.frame strongSelf.mainContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) strongSelf.mainContextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) @@ -2939,6 +2959,44 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI animation.animator.updateFrame(layer: nameNode.layer, frame: nameNodeFrame, completion: nil) } + let nameButtonNode: HighlightTrackingButtonNode + let nameHighlightNode: ASImageNode + if let currentButton = strongSelf.nameButtonNode, let currentHighlight = strongSelf.nameHighlightNode { + nameButtonNode = currentButton + nameHighlightNode = currentHighlight + } else { + nameHighlightNode = ASImageNode() + nameHighlightNode.alpha = 0.0 + nameHighlightNode.displaysAsynchronously = false + nameHighlightNode.isUserInteractionEnabled = false + strongSelf.clippingNode.addSubnode(nameHighlightNode) + strongSelf.nameHighlightNode = nameHighlightNode + + nameButtonNode = HighlightTrackingButtonNode() + nameButtonNode.highligthedChanged = { [weak nameHighlightNode] highlighted in + guard let nameHighlightNode else { + return + } + if highlighted { + nameHighlightNode.layer.removeAnimation(forKey: "opacity") + nameHighlightNode.alpha = 1.0 + } else { + nameHighlightNode.alpha = 0.0 + nameHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + nameButtonNode.addTarget(strongSelf, action: #selector(strongSelf.nameButtonPressed), forControlEvents: .touchUpInside) + strongSelf.clippingNode.addSubnode(nameButtonNode) + strongSelf.nameButtonNode = nameButtonNode + } + nameHighlightNode.frame = nameNodeFrame.insetBy(dx: -2.0, dy: -1.0) + nameButtonNode.frame = nameNodeFrame.insetBy(dx: -2.0, dy: -3.0) + + let nameColor = authorNameColor ?? item.presentationData.theme.theme.chat.message.outgoing.accentTextColor + if themeUpdated { + nameHighlightNode.image = generateFilledRoundedRectImage(size: CGSize(width: 8.0, height: 8.0), cornerRadius: 4.0, color: nameColor.withAlphaComponent(0.1))?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 4) + } + if let currentCredibilityIcon = currentCredibilityIcon { let credibilityIconView: ComponentHostView if let current = strongSelf.credibilityIconView { @@ -2963,6 +3021,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI action: nil ) strongSelf.credibilityIconComponent = credibilityIconComponent + strongSelf.credibilityIconContent = currentCredibilityIcon let credibilityIconSize = credibilityIconView.update( transition: .immediate, @@ -2971,10 +3030,49 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI containerSize: CGSize(width: 20.0, height: 20.0) ) - credibilityIconView.frame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 3.0, y: nameNode.frame.minY + floor((nameNode.bounds.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize) + let credibilityIconFrame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 3.0, y: nameNode.frame.minY + floor((nameNode.bounds.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize) + credibilityIconView.frame = credibilityIconFrame + + let credibilityButtonNode: HighlightTrackingButtonNode + let credibilityHighlightNode: ASImageNode + if let currentButton = strongSelf.credibilityButtonNode, let currentHighlight = strongSelf.credibilityHighlightNode { + credibilityButtonNode = currentButton + credibilityHighlightNode = currentHighlight + } else { + credibilityHighlightNode = ASImageNode() + credibilityHighlightNode.alpha = 0.0 + credibilityHighlightNode.displaysAsynchronously = false + credibilityHighlightNode.isUserInteractionEnabled = false + strongSelf.clippingNode.addSubnode(credibilityHighlightNode) + strongSelf.credibilityHighlightNode = credibilityHighlightNode + + credibilityButtonNode = HighlightTrackingButtonNode() + credibilityButtonNode.highligthedChanged = { [weak credibilityHighlightNode] highlighted in + guard let credibilityHighlightNode else { + return + } + if highlighted { + credibilityHighlightNode.layer.removeAnimation(forKey: "opacity") + credibilityHighlightNode.alpha = 1.0 + } else { + credibilityHighlightNode.alpha = 0.0 + credibilityHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + credibilityButtonNode.addTarget(strongSelf, action: #selector(strongSelf.credibilityButtonPressed), forControlEvents: .touchUpInside) + strongSelf.clippingNode.addSubnode(credibilityButtonNode) + strongSelf.credibilityButtonNode = credibilityButtonNode + } + credibilityHighlightNode.frame = credibilityIconFrame.insetBy(dx: -1.0, dy: -1.0) + credibilityButtonNode.frame = credibilityIconFrame.insetBy(dx: -2.0, dy: -3.0) + + if themeUpdated { + credibilityHighlightNode.image = generateFilledRoundedRectImage(size: CGSize(width: 8.0, height: 8.0), cornerRadius: 4.0, color: nameColor.withAlphaComponent(0.1))?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 4) + } } else { strongSelf.credibilityIconView?.removeFromSuperview() strongSelf.credibilityIconView = nil + strongSelf.credibilityIconContent = nil } if let adminBadgeNode = adminNodeSizeApply.1() { @@ -3074,6 +3172,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.adminBadgeNode = nil strongSelf.credibilityIconView?.removeFromSuperview() strongSelf.credibilityIconView = nil + strongSelf.nameButtonNode?.removeFromSupernode() + strongSelf.nameButtonNode = nil + strongSelf.nameHighlightNode?.removeFromSupernode() + strongSelf.nameHighlightNode = nil + strongSelf.credibilityButtonNode?.removeFromSupernode() + strongSelf.credibilityButtonNode = nil + strongSelf.credibilityHighlightNode?.removeFromSupernode() + strongSelf.credibilityHighlightNode = nil } } @@ -4420,6 +4526,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return result } + if let nameButtonNode = self.nameButtonNode, nameButtonNode.frame.contains(point) { + return nameButtonNode.view + } + + if let credibilityButtonNode = self.credibilityButtonNode, credibilityButtonNode.frame.contains(point) { + return credibilityButtonNode.view + } + if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view } @@ -4827,6 +4941,28 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + @objc private func nameButtonPressed() { + if let item = self.item, let peer = item.message.author { + let messageReference = MessageReference(item.message) + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + item.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), messageReference, .default) + } else { + item.controllerInteraction.openPeer(EnginePeer(peer), .info, messageReference, .groupParticipant(storyStats: nil, avatarHeaderNode: nil)) + } + } + } + + @objc private func credibilityButtonPressed() { + if let item = self.item, let credibilityIconView = self.credibilityIconView, let iconContent = self.credibilityIconContent, let peer = item.message.author { + var emojiFileId: Int64? + if case let .animation(content, _, _, _, _) = iconContent { + emojiFileId = content.fileId.id + } + + item.controllerInteraction.openPremiumStatusInfo(peer.id, credibilityIconView, emojiFileId, peer.nameColor ?? .blue) + } + } + private var playedSwipeToReplyHaptic = false @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index ea1f605bf7..435105c17a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -28,7 +28,6 @@ private func attributedServiceMessageString(theme: ChatPresentationThemeData, st public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let labelNode: TextNode private var backgroundNode: WallpaperBubbleBackgroundNode? - private var backgroundColorNode: ASDisplayNode private let backgroundMaskNode: ASImageNode private var linkHighlightingNode: LinkHighlightingNode? @@ -78,7 +77,6 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.labelNode.isUserInteractionEnabled = false self.labelNode.displaysAsynchronously = false - self.backgroundColorNode = ASDisplayNode() self.backgroundMaskNode = ASImageNode() self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) @@ -367,9 +365,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateVisibility() strongSelf.labelNode.isHidden = !hasServiceMessage - - strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) - + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame @@ -426,7 +422,6 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if strongSelf.backgroundNode == nil { if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { strongSelf.backgroundNode = backgroundNode - backgroundNode.addSubnode(strongSelf.backgroundColorNode) strongSelf.insertSubnode(backgroundNode, at: 0) } } @@ -448,7 +443,6 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.backgroundMaskNode.image = image strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size) - strongSelf.backgroundColorNode.frame = CGRect(origin: CGPoint(), size: image.size) strongSelf.cachedMaskBackgroundImage = (offset, image, labelRects) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index 9679cac715..35fe59fd49 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -218,13 +218,17 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode let backgroundColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first! let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor let accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor + var badgeTextColor: UIColor = .white + if badgeTextColor.distance(to: accentColor) < 1 { + badgeTextColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first! + } var updatedBadgeImage: UIImage? if themeUpdated { updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: backgroundColor, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil) } - let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: .white) + let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor) let prizeTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor) var prizeTextString: NSAttributedString? @@ -433,7 +437,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode } } } - let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 240.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1)) + let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 220.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1)) maxContentWidth = max(maxContentWidth, channelsWidth) maxContentWidth += 30.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 047d39e37d..ea1badefea 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -569,6 +569,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index a1857ed653..bfe67334c6 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -209,6 +209,7 @@ public final class ChatControllerInteraction { public let saveMediaToFiles: (EngineMessage.Id) -> Void public let openNoAdsDemo: () -> Void public let displayGiveawayParticipationStatus: (EngineMessage.Id) -> Void + public let openPremiumStatusInfo: (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void public let cancelInteractiveKeyboardGestures: () -> Void @@ -327,6 +328,7 @@ public final class ChatControllerInteraction { saveMediaToFiles: @escaping (EngineMessage.Id) -> Void, openNoAdsDemo: @escaping () -> Void, displayGiveawayParticipationStatus: @escaping (EngineMessage.Id) -> Void, + openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, @@ -427,6 +429,7 @@ public final class ChatControllerInteraction { self.saveMediaToFiles = saveMediaToFiles self.openNoAdsDemo = openNoAdsDemo self.displayGiveawayParticipationStatus = displayGiveawayParticipationStatus + self.openPremiumStatusInfo = openPremiumStatusInfo self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures self.dismissTextInput = dismissTextInput diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index fdcc76a0dd..800502be73 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -23,7 +23,7 @@ public final class EmojiStatusComponent: Component { case file(file: TelegramMediaFile) case customEmoji(fileId: Int64) - var fileId: MediaId { + public var fileId: MediaId { switch self { case let .file(file): return file.fileId diff --git a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift index 5acad8a76b..c766b9e28b 100644 --- a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift @@ -886,6 +886,11 @@ private extension MediaEditorValues { if let paintingData = legacyAdjustments.paintingData { if let entitiesData = paintingData.entitiesData { entities = decodeCodableDrawingEntities(data: entitiesData) + + let hasAnimation = entities.first(where: { $0.entity.isAnimated }) != nil + if !hasAnimation { + entities = [] + } } if let imagePath = paintingData.imagePath, let image = UIImage(contentsOfFile: imagePath) { drawing = image diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift index 6a235ce737..70e291a38e 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -233,7 +233,7 @@ private func peerNameColorScreenEntries( } let messageItem = PeerNameColorChatPreviewItem.MessageItem( outgoing: false, - peerId: peer.id, + peerId: PeerId(namespace: peer.id.namespace, id: PeerId.Id._internalFromInt64Value(0)), author: peer.compactDisplayTitle, photo: peer.profileImageRepresentations, nameColor: nameColor, diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift index a4d93ce24f..db68431ac0 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift @@ -829,9 +829,9 @@ final class CountriesMultiselectionScreenComponent: Component { } navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset - let actionButtonTitle = "Save Countries" - let title = "Select Countries" - let subtitle = "select up to \(component.context.userLimits.maxGiveawayCountriesCount) countries" + let actionButtonTitle = environment.strings.CountriesList_SaveCountries + let title = environment.strings.CountriesList_SelectCountries + let subtitle = environment.strings.CountriesList_SelectUpTo(component.context.userLimits.maxGiveawayCountriesCount) let titleComponent = AnyComponent( List([ diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 0a2bde11db..241ee1d82d 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -588,17 +588,22 @@ public extension ShareWithPeersScreen { case let .channels(excludePeerIds, searchQuery): self.stateDisposable = (combineLatest( context.engine.messages.chatList(group: .root, count: 500) |> take(1), + searchQuery.flatMap { context.engine.contacts.searchLocalPeers(query: $0) } ?? .single([]), context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))) ) - |> mapToSignal { chatList, initialPeers -> Signal<(EngineChatList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> in + |> mapToSignal { chatList, searchResults, initialPeers -> Signal<(EngineChatList, [EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> in + var peerIds: [EnginePeer.Id] = [] + peerIds.append(contentsOf: chatList.items.map(\.renderedPeer.peerId)) + peerIds.append(contentsOf: searchResults.map(\.peerId)) + peerIds.append(contentsOf: initialPeers.compactMap(\.value?.id)) return context.engine.data.subscribe( EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) - |> map { participantCountMap -> (EngineChatList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in - return (chatList, initialPeers, participantCountMap) + |> map { participantCountMap -> (EngineChatList, [EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in + return (chatList, searchResults, initialPeers, participantCountMap) } } - |> deliverOnMainQueue).start(next: { [weak self] chatList, initialPeers, participantCounts in + |> deliverOnMainQueue).start(next: { [weak self] chatList, searchResults, initialPeers, participantCounts in guard let self else { return } @@ -612,7 +617,7 @@ public extension ShareWithPeersScreen { var existingIds = Set() var selectedPeers: [EnginePeer] = [] - + for item in chatList.items.reversed() { if let peer = item.renderedPeer.peer { if self.initialPeerIds.contains(peer.id) { @@ -628,6 +633,13 @@ public extension ShareWithPeersScreen { existingIds.insert(peerId) } } + + for item in searchResults { + if let peer = item.peer, case let .channel(channel) = peer, case .broadcast = channel.info { + selectedPeers.append(peer) + existingIds.insert(peer.id) + } + } let queryTokens = stringIndexTokens(searchQuery ?? "", transliteration: .combined) func peerMatchesTokens(peer: EnginePeer, tokens: [ValueBoxKey]) -> Bool { @@ -640,9 +652,15 @@ public extension ShareWithPeersScreen { var peers: [EnginePeer] = [] peers = chatList.items.filter { peer in if let peer = peer.renderedPeer.peer { + if existingIds.contains(peer.id) { + return false + } if excludePeerIds.contains(peer.id) { return false } + if peer.isFake || peer.isScam { + return false + } if let _ = searchQuery, !peerMatchesTokens(peer: peer, tokens: queryTokens) { return false } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 971f2684e8..96c0b8a7a0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -538,7 +538,11 @@ private final class StoryContainerScreenComponent: Component { let fraction = translation.x / (self.bounds.width / 2.0) timestamp = initialSeekTimestamp + duration * fraction } - visibleItemView.seekTo(max(0.0, min(duration, timestamp)), apply: apply) + if translation.y < 64.0 { + visibleItemView.seekTo(max(0.0, min(duration, timestamp)), apply: apply) + } else { + visibleItemView.seekTo(initialSeekTimestamp, apply: apply) + } } longPressRecognizer.updatePanEnded = { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index bf05960376..29150c715c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4852,6 +4852,65 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(controller, in: .current) })) + }, openPremiumStatusInfo: { [weak self] peerId, sourceView, peerStatus, nameColor in + guard let self else { + return + } + + let context = self.context + let source: Signal + if let peerStatus { + source = context.engine.stickers.resolveInlineStickers(fileIds: [peerStatus]) + |> mapToSignal { files in + if let file = files[peerStatus] { + var reference: StickerPackReference? + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { + reference = packReference + break + } + } + + if let reference { + return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) + |> filter { result in + if case .result = result { + return true + } else { + return false + } + } + |> take(1) + |> mapToSignal { result -> Signal in + if case let .result(_, items, _) = result { + return .single(.emojiStatus(peerId, peerStatus, items.first?.file, result)) + } else { + return .single(.emojiStatus(peerId, peerStatus, nil, nil)) + } + } + } else { + return .single(.emojiStatus(peerId, peerStatus, nil, nil)) + } + } else { + return .single(.emojiStatus(peerId, peerStatus, nil, nil)) + } + } + } else { + source = .single(.profile(peerId)) + } + + let _ = (source + |> deliverOnMainQueue).startStandalone(next: { [weak self] source in + guard let self else { + return + } + let controller = PremiumIntroScreen(context: self.context, source: source) + controller.sourceView = sourceView + controller.containerView = self.navigationController?.view + controller.animationColor = self.context.peerNameColors.get(nameColor, dark: self.presentationData.theme.overallDarkAppearance).main + self.push(controller) + }) + }, requestMessageUpdate: { [weak self] id, scroll in if let self { self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 3efb7f2db5..df161c0d6c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -565,6 +565,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState var loadStickerSaveStatus: MediaId? var loadCopyMediaResource: MediaResource? var isAction = false + var isGiveawayLaunch = false var diceEmoji: String? if messages.count == 1 { for media in messages[0].media { @@ -579,6 +580,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } else if media is TelegramMediaAction || media is TelegramMediaExpiredContent { isAction = true + if let action = media as? TelegramMediaAction, case .giveawayLaunched = action.action { + isGiveawayLaunch = true + } } else if let image = media as? TelegramMediaImage { if !messages[0].containsSecretMedia { loadCopyMediaResource = largestImageRepresentation(image.representations)?.resource @@ -639,6 +643,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState canPin = false } + if isGiveawayLaunch { + canReply = false + } + if let peer = messages[0].peers[messages[0].id.peerId] { if peer.isDeleted { canPin = false @@ -925,7 +933,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState resourceAvailable = false } - if !isPremium && isDownloading { var isLargeFile = false for media in message.media { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 6a0ef98092..4a656b7a49 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -170,6 +170,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index b43f14cf92..05f743cb9d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2921,6 +2921,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { @@ -3794,37 +3795,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + let source: Signal + if let peerStatus = peerStatus { + source = emojiStatusFileAndPack + |> take(1) + |> mapToSignal { emojiStatusFileAndPack -> Signal in + if let (file, pack) = emojiStatusFileAndPack { + return .single(.emojiStatus(strongSelf.peerId, peerStatus.fileId, file, pack)) + } else { + return .complete() + } + } + } else { + source = .single(.profile(strongSelf.peerId)) + } + + let _ = (source + |> deliverOnMainQueue).startStandalone(next: { [weak self] source in guard let strongSelf = self else { return } - let source: Signal - if let peerStatus = peerStatus { - source = emojiStatusFileAndPack - |> take(1) - |> mapToSignal { emojiStatusFileAndPack -> Signal in - if let (file, pack) = emojiStatusFileAndPack { - return .single(.emojiStatus(strongSelf.peerId, peerStatus.fileId, file, pack)) - } else { - return .complete() - } - } - } else { - source = .single(.profile(strongSelf.peerId)) - } - - let _ = (source - |> deliverOnMainQueue).startStandalone(next: { [weak self] source in - guard let strongSelf = self else { - return - } - let controller = PremiumIntroScreen(context: strongSelf.context, source: source) - controller.sourceView = sourceView - controller.containerView = strongSelf.controller?.navigationController?.view - controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor - strongSelf.controller?.push(controller) - }) + let controller = PremiumIntroScreen(context: strongSelf.context, source: source) + controller.sourceView = sourceView + controller.containerView = strongSelf.controller?.navigationController?.view + controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor + strongSelf.controller?.push(controller) }) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3da93fdb92..3a3ad0197f 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1559,6 +1559,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { From c8c08d5e338adde8f2e435982325cd8678d6f9b9 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 6 Nov 2023 23:19:22 +0400 Subject: [PATCH 5/7] Cherry-pick various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 18 +++++++++ .../Sources/Node/ChatListItemStrings.swift | 15 +++++-- .../Chat/ChatMessageBubbleItemNode/BUILD | 1 - .../ChatPinnedMessageTitlePanelNode.swift | 39 ++++++++++++++----- 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 120781da56..11c3bd6079 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10433,3 +10433,21 @@ Sorry for the inconvenience."; "CountriesList.SaveCountries" = "Save Countries"; "CountriesList.SelectUpTo_1" = "select up to %@ country"; "CountriesList.SelectUpTo_any" = "select up to %@ countries"; + +"Message.GiveawayOngoing" = "Giveaway: %1$@ on %2$@"; +"Message.GiveawayOngoing.Winners_1" = "%@ winner to be selected"; +"Message.GiveawayOngoing.Winners_any" = "%@ winners to be selected"; + +"Message.GiveawayFinished" = "Giveaway: %1$@ on %2$@"; +"Message.GiveawayFinished.Winners_1" = "%@ winner was selected"; +"Message.GiveawayFinished.Winners_any" = "%@ winners were selected"; + +"Conversation.PinnedGiveaway" = "Giveaway"; + +"Conversation.PinnedGiveaway.Ongoing" = "%1$@ on %2$@"; +"Conversation.PinnedGiveaway.Ongoing.Winners_1" = "%@ winner to be selected"; +"Conversation.PinnedGiveaway.Ongoing.Winners_any" = "%@ winners to be selected"; + +"Conversation.PinnedGiveaway.Finished" = "%1$@ on %2$@"; +"Conversation.PinnedGiveaway.Finished.Winners_1" = "%@ winner was selected"; +"Conversation.PinnedGiveaway.Finished.Winners_any" = "%@ winners were selected"; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 060ab6bd01..df85d40dd5 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -305,9 +305,18 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } else { messageText = strings.Notification_Story } - case _ as TelegramMediaGiveaway: - messageText = strings.Message_Giveaway - case let webpage as TelegramMediaWebpage: + case let giveaway as TelegramMediaGiveaway: + let dateString = stringForDateWithoutYear(date: Date(timeIntervalSince1970: TimeInterval(giveaway.untilDate)), timeZone: .current, strings: strings) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let isFinished = currentTime >= giveaway.untilDate + if isFinished { + let winnersString = strings.Message_GiveawayFinished_Winners(giveaway.quantity) + messageText = strings.Message_GiveawayFinished(winnersString, dateString).string + } else { + let winnersString = strings.Message_GiveawayOngoing_Winners(giveaway.quantity) + messageText = strings.Message_GiveawayOngoing(winnersString, dateString).string + } + case let webpage as TelegramMediaWebpage: if messageText.isEmpty, case let .Loaded(content) = webpage.content { messageText = content.displayUrl } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index c945e54830..5b8b444a8d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -79,7 +79,6 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", - "//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 04eda7684b..320fa27d16 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -583,19 +583,25 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? + let giveaway = pinnedMessage.message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway + var titleStrings: [AnimatedCountLabelNode.Segment] = [] - if pinnedMessage.totalCount == 2 { - if pinnedMessage.index == 0 { - titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedPreviousMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + if let _ = giveaway { + titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedGiveaway) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + } else { + if pinnedMessage.totalCount == 2 { + if pinnedMessage.index == 0 { + titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedPreviousMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + } else { + titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + } + } else if pinnedMessage.totalCount > 1 && pinnedMessage.index != pinnedMessage.totalCount - 1 { + titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + titleStrings.append(.text(1, NSAttributedString(string: " #", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + titleStrings.append(.number(pinnedMessage.index + 1, NSAttributedString(string: "\(pinnedMessage.index + 1)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } else { titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } - } else if pinnedMessage.totalCount > 1 && pinnedMessage.index != pinnedMessage.totalCount - 1 { - titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) - titleStrings.append(.text(1, NSAttributedString(string: " #", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) - titleStrings.append(.number(pinnedMessage.index + 1, NSAttributedString(string: "\(pinnedMessage.index + 1)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) - } else { - titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } if !message.containsSecretMedia { @@ -679,7 +685,20 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let messageText: NSAttributedString let textFont = Font.regular(15.0) - if isText { + if let giveaway { + let dateString = stringForDateWithoutYear(date: Date(timeIntervalSince1970: TimeInterval(giveaway.untilDate)), timeZone: .current, strings: strings) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let isFinished = currentTime >= giveaway.untilDate + let text: String + if isFinished { + let winnersString = strings.Conversation_PinnedGiveaway_Finished_Winners(giveaway.quantity) + text = strings.Conversation_PinnedGiveaway_Finished(winnersString, dateString).string + } else { + let winnersString = strings.Conversation_PinnedGiveaway_Ongoing_Winners(giveaway.quantity) + text = strings.Conversation_PinnedGiveaway_Ongoing(winnersString, dateString).string + } + messageText = NSAttributedString(string: text, font: textFont, textColor: theme.chat.inputPanel.primaryTextColor) + } else if isText { var text = message.text var messageEntities = message.textEntitiesAttribute?.entities ?? [] From 686f3c4a754845bc07a39540b10a8e606c8b1e2d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 6 Nov 2023 23:50:16 +0400 Subject: [PATCH 6/7] Unify link preview image size --- .../ChatMessageWebpageBubbleContentNode.swift | 34 ++++++++++++++++--- .../Chat/ChatMessageActionOptions.swift | 9 ++++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 17e3d8a0e7..6634fce5e5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -22,6 +22,31 @@ import ChatControllerInteraction private let titleFont: UIFont = Font.semibold(15.0) +public func defaultWebpageImageSizeIsSmall(webpage: TelegramMediaWebpageLoadedContent) -> Bool { + let type = websiteType(of: webpage.websiteName) + + let mainMedia: Media? + switch type { + case .instagram, .twitter: + mainMedia = webpage.story ?? webpage.image ?? webpage.file + default: + mainMedia = webpage.story ?? webpage.file ?? webpage.image + } + + if let image = mainMedia as? TelegramMediaImage { + if let type = webpage.type, (["photo", "video", "embed", "gif", "document", "telegram_album"] as [String]).contains(type) { + } else if let type = webpage.type, (["article"] as [String]).contains(type) { + return true + } else if let _ = largestImageRepresentation(image.representations)?.dimensions { + if webpage.instantPage == nil { + return true + } + } + } + + return false +} + public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { private var webPage: TelegramMediaWebpage? @@ -309,10 +334,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } mediaAndFlags = (image, flags) } else if let _ = largestImageRepresentation(image.representations)?.dimensions { - var flags = ChatMessageAttachedContentNodeMediaFlags() - if webpage.instantPage == nil { - flags.insert(.preferMediaInline) - } + let flags = ChatMessageAttachedContentNodeMediaFlags() mediaAndFlags = (image, flags) } } else if let story = mainMedia as? TelegramMediaStory { @@ -441,6 +463,10 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } } + if defaultWebpageImageSizeIsSmall(webpage: webpage) { + mediaAndFlags?.1.insert(.preferMediaInline) + } + if let webPageContent, let isMediaLargeByDefault = webPageContent.isMediaLargeByDefault, !isMediaLargeByDefault { mediaAndFlags?.1.insert(.preferMediaInline) } else if let attribute = item.message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index 8e5851a995..8383797963 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -16,6 +16,7 @@ import TextFormat import ChatMessageItemView import ChatMessageBubbleItemNode import TelegramNotices +import ChatMessageWebpageBubbleContentNode private enum OptionsId: Hashable { case reply @@ -710,7 +711,13 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD var largeMedia = false if webpageHasLargeMedia { - largeMedia = urlPreview.largeMedia ?? true + if let value = urlPreview.largeMedia { + largeMedia = value + } else if case let .Loaded(content) = urlPreview.webPage.content { + largeMedia = !defaultWebpageImageSizeIsSmall(webpage: content) + } else { + largeMedia = true + } } else { largeMedia = false } From c77e8007805df67b8b593033ef57194068e9b50d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 6 Nov 2023 23:50:35 +0400 Subject: [PATCH 7/7] Fix round video panels width --- .../Sources/ChatMessageInteractiveInstantVideoNode.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 84e8df304a..f0aa517214 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -433,7 +433,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { forwardAuthorSignature = forwardInfo.authorSignature } } - let availableWidth: CGFloat = max(60.0, availableContentWidth - 210.0 + 6.0) + let availableWidth: CGFloat = max(60.0, availableContentWidth - 220.0 + 6.0) forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) }