From fb0661774484b9df4d7cdd2e4641332e97fc23ed Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 27 Jul 2023 18:44:11 +0400 Subject: [PATCH 01/12] Fix message stats text layout (cherry picked from commit 9be6a8e6980b52eaa0d378599c540e2fa0a8744b) --- .../Sources/MessageStatsOverviewItem.swift | 19 ++++++++++++++----- .../SynchronizeViewStoriesOperation.swift | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift b/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift index 33f3a9df3f..22a0549e82 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift @@ -166,14 +166,23 @@ class MessageStatsOverviewItemNode: ListViewItemNode { centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, item.stats.forwards - Int($0))))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + var remainingWidth: CGFloat = params.width - leftInset - rightInset - sideInset * 2.0 + let maxItemWidth: CGFloat = floor(remainingWidth / 2.8) - leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + remainingWidth -= leftTitleLabelLayoutAndApply!.0.size.width - 4.0 - centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PublicShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PublicShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + remainingWidth -= centerTitleLabelLayoutAndApply!.0.size.width - 4.0 - rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PrivateShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PrivateShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - height += rightValueLabelLayoutAndApply!.0.size.height + rightTitleLabelLayoutAndApply!.0.size.height + var maxLabelHeight = rightTitleLabelLayoutAndApply!.0.size.height + maxLabelHeight = max(maxLabelHeight, centerTitleLabelLayoutAndApply!.0.size.height) + maxLabelHeight = max(maxLabelHeight, leftTitleLabelLayoutAndApply!.0.size.height) + + height += rightValueLabelLayoutAndApply!.0.size.height + maxLabelHeight let contentSize = CGSize(width: params.width, height: height) return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in @@ -255,7 +264,7 @@ class MessageStatsOverviewItemNode: ListViewItemNode { let maxCenterWidth = max(centerValueLabelLayoutAndApply?.0.size.width ?? 0.0, centerTitleLabelLayoutAndApply?.0.size.width ?? 0.0) let maxRightWidth = max(rightValueLabelLayoutAndApply?.0.size.width ?? 0.0, rightTitleLabelLayoutAndApply?.0.size.width ?? 0.0) - let horizontalSpacing = min(60, (params.width - leftInset - rightInset - sideInset * 2.0 - maxLeftWidth - maxCenterWidth - maxRightWidth) / 2.0) + let horizontalSpacing = max(1.0, min(60, (params.width - leftInset - rightInset - sideInset * 2.0 - maxLeftWidth - maxCenterWidth - maxRightWidth) / 2.0)) var x: CGFloat = leftInset + (params.width - leftInset - rightInset - maxLeftWidth - maxCenterWidth - maxRightWidth - horizontalSpacing * 2.0) / 2.0 if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SynchronizeViewStoriesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SynchronizeViewStoriesOperation.swift index e2d53dc398..4fe3f59f10 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SynchronizeViewStoriesOperation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SynchronizeViewStoriesOperation.swift @@ -35,8 +35,8 @@ func _internal_addSynchronizeViewStoriesOperation(peerId: PeerId, storyId: Int32 if let (topOperation, topLocalIndex) = topOperation { if topOperation.storyId < storyId { let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex) + replace = true } - replace = true } else { replace = true } From dd74339925355a60e9ae4205eb5e9e64c1d3d818 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 27 Jul 2023 21:57:47 +0400 Subject: [PATCH 02/12] Cherry-pick commits --- .../Sources/Network/Download.swift | 8 ++++- .../State/AccountStateManagementUtils.swift | 32 +++++++++++-------- .../TelegramCore/Sources/State/Holes.swift | 10 +++--- .../TelegramEngine/Messages/Stories.swift | 17 ++++++---- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/submodules/TelegramCore/Sources/Network/Download.swift b/submodules/TelegramCore/Sources/Network/Download.swift index d695966a5b..a273231d34 100644 --- a/submodules/TelegramCore/Sources/Network/Download.swift +++ b/submodules/TelegramCore/Sources/Network/Download.swift @@ -293,7 +293,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { |> retryRequest } - func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse)) -> Signal { + func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), automaticFloodWait: Bool = true) -> Signal { return Signal { subscriber in let request = MTRequest() @@ -308,6 +308,12 @@ class Download: NSObject, MTRequestMessageServiceDelegate { request.needsTimeoutTimer = self.useRequestTimeoutTimers request.shouldContinueExecutionWithErrorContext = { errorContext in + guard let errorContext = errorContext else { + return true + } + if errorContext.floodWaitSeconds > 0 && !automaticFloodWait { + return false + } return true } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 63a2ecd68b..5e69732d92 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -2096,25 +2096,29 @@ func resolveStories(postbox: Postbox, source: FetchMessageHistoryHoleSource, while idOffset < allIds.count { let bucketLength = min(100, allIds.count - idOffset) let ids = Array(allIds[idOffset ..< (idOffset + bucketLength)]) - signals.append(_internal_getStoriesById(accountPeerId: accountPeerId, postbox: postbox, source: source, peerId: peerId, peerReference: additionalPeers.get(peerId).flatMap(PeerReference.init), ids: ids) + signals.append(_internal_getStoriesById(accountPeerId: accountPeerId, postbox: postbox, source: source, peerId: peerId, peerReference: additionalPeers.get(peerId).flatMap(PeerReference.init), ids: ids, allowFloodWait: false) |> mapToSignal { result -> Signal in - return postbox.transaction { transaction -> Void in - for id in ids { - let current = transaction.getStory(id: StoryId(peerId: peerId, id: id)) - var updated: CodableEntry? - if let updatedItem = result.first(where: { $0.id == id }) { - if let entry = CodableEntry(updatedItem) { - updated = entry + if let result = result { + return postbox.transaction { transaction -> Void in + for id in ids { + let current = transaction.getStory(id: StoryId(peerId: peerId, id: id)) + var updated: CodableEntry? + if let updatedItem = result.first(where: { $0.id == id }) { + if let entry = CodableEntry(updatedItem) { + updated = entry + } + } else { + updated = CodableEntry(data: Data()) + } + if current != updated { + transaction.setStory(id: StoryId(peerId: peerId, id: id), value: updated ?? CodableEntry(data: Data())) } - } else { - updated = CodableEntry(data: Data()) - } - if current != updated { - transaction.setStory(id: StoryId(peerId: peerId, id: id), value: updated ?? CodableEntry(data: Data())) } } + |> ignoreValues + } else { + return .complete() } - |> ignoreValues }) idOffset += bucketLength } diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index bb5f62e31c..88d9054a43 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -108,12 +108,12 @@ enum FetchMessageHistoryHoleSource { case network(Network) case download(Download) - func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse)) -> Signal { + func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), automaticFloodWait: Bool = true) -> Signal { switch self { - case let .network(network): - return network.request(data) - case let .download(download): - return download.request(data) + case let .network(network): + return network.request(data, automaticFloodWait: automaticFloodWait) + case let .download(download): + return download.request(data, automaticFloodWait: automaticFloodWait) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 1825219a1a..105af244a3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1414,25 +1414,25 @@ func _internal_getStoriesById(accountPeerId: PeerId, postbox: Postbox, network: } } -func _internal_getStoriesById(accountPeerId: PeerId, postbox: Postbox, source: FetchMessageHistoryHoleSource, peerId: PeerId, peerReference: PeerReference?, ids: [Int32]) -> Signal<[Stories.StoredItem], NoError> { +func _internal_getStoriesById(accountPeerId: PeerId, postbox: Postbox, source: FetchMessageHistoryHoleSource, peerId: PeerId, peerReference: PeerReference?, ids: [Int32], allowFloodWait: Bool) -> Signal<[Stories.StoredItem]?, NoError> { return postbox.transaction { transaction -> Api.InputUser? in return transaction.getPeer(peerId).flatMap(apiInputUser) } - |> mapToSignal { inputUser -> Signal<[Stories.StoredItem], NoError> in + |> mapToSignal { inputUser -> Signal<[Stories.StoredItem]?, NoError> in guard let inputUser = inputUser ?? peerReference?.inputUser else { return .single([]) } - return source.request(Api.functions.stories.getStoriesByID(userId: inputUser, id: ids)) + return source.request(Api.functions.stories.getStoriesByID(userId: inputUser, id: ids), automaticFloodWait: allowFloodWait) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal<[Stories.StoredItem], NoError> in + |> mapToSignal { result -> Signal<[Stories.StoredItem]?, NoError> in guard let result = result else { - return .single([]) + return .single(nil) } - return postbox.transaction { transaction -> [Stories.StoredItem] in + return postbox.transaction { transaction -> [Stories.StoredItem]? in switch result { case let .stories(_, stories, users): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) @@ -1883,8 +1883,11 @@ func _internal_exportStoryLink(account: Account, peerId: EnginePeer.Id, id: Int3 } func _internal_refreshStories(account: Account, peerId: PeerId, ids: [Int32]) -> Signal { - return _internal_getStoriesById(accountPeerId: account.peerId, postbox: account.postbox, source: .network(account.network), peerId: peerId, peerReference: nil, ids: ids) + return _internal_getStoriesById(accountPeerId: account.peerId, postbox: account.postbox, source: .network(account.network), peerId: peerId, peerReference: nil, ids: ids, allowFloodWait: true) |> mapToSignal { result -> Signal in + guard let result = result else { + return .complete() + } return account.postbox.transaction { transaction -> Void in var currentItems = transaction.getStoryItems(peerId: peerId) for i in 0 ..< currentItems.count { From 0e8981d77971a21da68bda93bf55144d9292d3a6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 27 Jul 2023 21:58:14 +0400 Subject: [PATCH 03/12] Disable video remuxing for now as the server does it for us (cherry picked from commit 9264e1a2cc02d852c26feba312cf2921497af6b9) --- submodules/TelegramUI/Sources/FetchVideoMediaResource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift index d6783a1947..50a09090fb 100644 --- a/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift @@ -419,7 +419,7 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr var value = stat() if stat(result.fileURL.path, &value) == 0 { let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4") - if let size = fileSize(result.fileURL.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(result.fileURL.path, to: remuxedTempFile.path) { + if !"".isEmpty, let size = fileSize(result.fileURL.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(result.fileURL.path, to: remuxedTempFile.path) { TempBox.shared.dispose(tempFile) subscriber.putNext(.moveTempFile(file: remuxedTempFile)) } else { From 8412f8f01ab148dfb635e3f68b8d04c202062db8 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 27 Jul 2023 22:16:43 +0400 Subject: [PATCH 04/12] Bump version (cherry picked from commit c5f5819aa16726f2cc785a6c5d555ccac2a4aefd) --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 68347b89b1..f55f61b95c 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "9.6.6", + "app": "9.6.7", "bazel": "6.1.1", "xcode": "14.2" } From 3bb43037675fe786f4fd6a591296e5413e700ba4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 27 Jul 2023 22:32:17 +0400 Subject: [PATCH 05/12] Fix audio message presentation (cherry picked from commit af70f82faa008054991a11f030da481d53adb0ef) --- .../Sources/DefaultDayPresentationTheme.swift | 2 +- .../TelegramUI/Sources/ChatMessageInteractiveFileNode.swift | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 00c4211f45..b5fcd56959 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -191,7 +191,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti outgoingLinkTextColor = outgoingAccent outgoingScamColor = UIColor(rgb: 0xff3b30) outgoingControlColor = outgoingAccent - outgoingInactiveControlColor = outgoingAccent + outgoingInactiveControlColor = outgoingAccent.withMultipliedAlpha(0.5) outgoingFileTitleColor = outgoingAccent outgoingPollsProgressColor = outgoingControlColor outgoingSelectionColor = outgoingAccent.withAlphaComponent(0.2) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 8c93bb6fd5..072e82c3ff 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -1564,11 +1564,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } if isSending { - if case .progress = streamingState { - } else { - let adjustedProgress: CGFloat = 0.027 - streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0)) - } + streamingState = .none if case .progress = state { } else { From 7dd5c873d04a5b6115c7283bff9c38011f85187d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 27 Jul 2023 23:58:53 +0400 Subject: [PATCH 06/12] Cherry-pick commits --- .../Sources/NotificationService.swift | 62 ++++++++++++++++++- .../AvatarNode/Sources/AvatarNode.swift | 6 ++ .../Sources/ChatListController.swift | 5 +- .../SyncCore/SyncCore_Namespaces.swift | 1 + .../TelegramEngine/Messages/Stories.swift | 14 +++++ .../Sources/OpenStories.swift | 4 +- .../StoryItemSetContainerComponent.swift | 1 + 7 files changed, 87 insertions(+), 6 deletions(-) diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index bad26abdb6..16b52484aa 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -979,6 +979,7 @@ private final class NotificationServiceHandler { case deleteMessage([MessageId]) case readReactions([MessageId]) case readMessage(MessageId) + case readStories(peerId: PeerId, maxId: Int32) case call(CallData) } @@ -1030,6 +1031,14 @@ private final class NotificationServiceHandler { } } } + case "READ_STORIES": + if let peerId = peerId { + if let storyIdString = payloadJson["max_id"] as? String { + if let maxId = Int32(storyIdString) { + action = .readStories(peerId: peerId, maxId: maxId) + } + } + } default: break } @@ -1786,6 +1795,10 @@ private final class NotificationServiceHandler { } } } + + let wasDisplayed = stateManager.postbox.transaction { transaction -> Bool in + return _internal_getStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId)) + } Logger.shared.log("NotificationService \(episode)", "Will fetch media") let _ = (combineLatest(queue: queue, @@ -1794,10 +1807,11 @@ private final class NotificationServiceHandler { fetchNotificationSoundSignal |> timeout(10.0, queue: queue, alternate: .single(nil)), fetchStoriesSignal - |> timeout(10.0, queue: queue, alternate: .single(Void())) + |> timeout(10.0, queue: queue, alternate: .single(Void())), + wasDisplayed ) - |> deliverOn(queue)).start(next: { mediaData, notificationSoundData, _ in - guard let strongSelf = self, let _ = strongSelf.stateManager else { + |> deliverOn(queue)).start(next: { mediaData, notificationSoundData, _, wasDisplayed in + guard let strongSelf = self, let stateManager = strongSelf.stateManager else { completed() return } @@ -1811,6 +1825,15 @@ private final class NotificationServiceHandler { let _ = try? notificationSoundData.write(to: URL(fileURLWithPath: filePath)) } } + + var content = content + if wasDisplayed { + content = NotificationContent(isLockedMessage: nil) + } else { + let _ = (stateManager.postbox.transaction { transaction -> Void in + _internal_setStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId)) + }).start() + } Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") @@ -2003,6 +2026,39 @@ private final class NotificationServiceHandler { }) } + if !removeIdentifiers.isEmpty { + Logger.shared.log("NotificationService \(episode)", "Will try to remove \(removeIdentifiers.count) notifications") + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers) + queue.after(1.0, { + completeRemoval() + }) + } else { + completeRemoval() + } + }) + }) + case let .readStories(peerId, maxId): + Logger.shared.log("NotificationService \(episode)", "Will read stories peerId: \(peerId) maxId: \(maxId)") + let _ = (stateManager.postbox.transaction { transaction -> Void in + } + |> deliverOn(strongSelf.queue)).start(completed: { + UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in + var removeIdentifiers: [String] = [] + for notification in notifications { + if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["story_id"] as? String, let messageIdValue = Int32(messageIdString) { + if PeerId(peerIdValue) == peerId && messageIdValue <= maxId { + removeIdentifiers.append(notification.request.identifier) + } + } + } + + let completeRemoval: () -> Void = { + let content = NotificationContent(isLockedMessage: nil) + updateCurrentContent(content) + + completed() + } + if !removeIdentifiers.isEmpty { Logger.shared.log("NotificationService \(episode)", "Will try to remove \(removeIdentifiers.count) notifications") UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers) diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 5a6714f198..7a39ef44d1 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -953,6 +953,9 @@ public final class AvatarNode: ASDisplayNode { guard let self else { return } + if let previousDisposable = self.loadingStatuses.copyItemsWithIndices().first(where: { $0.0 == index })?.1 { + previousDisposable.dispose() + } self.loadingStatuses.remove(index) if self.loadingStatuses.isEmpty { self.updateStoryIndicator(transition: .immediate) @@ -964,6 +967,9 @@ public final class AvatarNode: ASDisplayNode { guard let self else { return } + if let previousDisposable = self.loadingStatuses.copyItemsWithIndices().first(where: { $0.0 == index })?.1 { + previousDisposable.dispose() + } self.loadingStatuses.remove(index) if self.loadingStatuses.isEmpty { self.updateStoryIndicator(transition: .immediate) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 401409aa43..0163ed063f 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -207,6 +207,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var preloadStorySubscriptionsDisposable: Disposable? private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:] + private var sharedOpenStoryProgressDisposable = MetaDisposable() + private var fullScreenEffectView: RippleEffectView? public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { @@ -778,6 +780,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.preloadStorySubscriptionsDisposable?.dispose() self.storyProgressDisposable?.dispose() self.storiesPostingAvailabilityDisposable?.dispose() + self.sharedOpenStoryProgressDisposable.dispose() } private func updateNavigationMetadata() { @@ -1362,7 +1365,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController case .archive: StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode) case let .peer(peerId): - StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode) + StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode, sharedProgressDisposable: self.sharedOpenStoryProgressDisposable) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index d2ec5c083d..fb2e149912 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -107,6 +107,7 @@ public struct Namespaces { public static let emojiSearchCategories: Int8 = 25 public static let cachedEmojiQueryResults: Int8 = 26 public static let cachedPeerStoryListHeads: Int8 = 27 + public static let displayedStoryNotifications: Int8 = 28 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 105af244a3..d9d4aa6c51 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1952,3 +1952,17 @@ func _internal_refreshSeenStories(postbox: Postbox, network: Network) -> Signal< |> ignoreValues } } + +public func _internal_getStoryNotificationWasDisplayed(transaction: Transaction, id: StoryId) -> Bool { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: id.peerId.toInt64()) + key.setInt32(8, value: id.id) + return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key)) != nil +} + +public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction, id: StoryId) { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: id.peerId.toInt64()) + key.setInt32(8, value: id.id) + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data())) +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift index edb0754620..6d01f0245b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift @@ -85,7 +85,7 @@ public extension StoryContainerScreen { let _ = avatarNode.pushLoadingStatus(signal: signal) } - static func openPeerStories(context: AccountContext, peerId: EnginePeer.Id, parentController: ViewController, avatarNode: AvatarNode) { + static func openPeerStories(context: AccountContext, peerId: EnginePeer.Id, parentController: ViewController, avatarNode: AvatarNode, sharedProgressDisposable: MetaDisposable? = nil) { return openPeerStoriesCustom( context: context, peerId: peerId, @@ -149,7 +149,7 @@ public extension StoryContainerScreen { guard let avatarNode else { return } - let _ = avatarNode.pushLoadingStatus(signal: signal) + sharedProgressDisposable?.set(avatarNode.pushLoadingStatus(signal: signal)) } ) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index efc9868257..1f7288a131 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2419,6 +2419,7 @@ public final class StoryItemSetContainerComponent: Component { soundImage = "Stories/SoundOn" } + //TODO:anim_storymute let soundButtonSize = self.soundButton.update( transition: transition, component: AnyComponent(PlainButtonComponent( From 0d8fe47078e561d227074c8ac4a9683bb3e1478d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 28 Jul 2023 00:08:01 +0400 Subject: [PATCH 07/12] Fix unpinned story indices (cherry picked from commit 1cee5070f897790dfb110549892c13bdea5daafc) --- .../StoryContainerScreen/Sources/StoryChatContent.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 6b2694c84b..cb1d1039cd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -1170,7 +1170,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { if let current = self.focusedId { if let index = state.items.firstIndex(where: { $0.id == current }) { focusedIndex = index - } else if let index = state.items.firstIndex(where: { $0.id >= current }) { + } else if let index = state.items.firstIndex(where: { $0.id <= current }) { focusedIndex = index } else if !state.items.isEmpty { focusedIndex = 0 @@ -1180,7 +1180,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } else if let initialId = initialId { if let index = state.items.firstIndex(where: { $0.id == initialId }) { focusedIndex = index - } else if let index = state.items.firstIndex(where: { $0.id >= initialId }) { + } else if let index = state.items.firstIndex(where: { $0.id <= initialId }) { focusedIndex = index } else { focusedIndex = nil From cef72539b754bb1699ff2f31dedb4e9f4713101e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 28 Jul 2023 16:50:13 +0400 Subject: [PATCH 08/12] Cherry-pick capture protection --- .../Sources/StoryItemImageView.swift | 76 ++++++------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift index 8e54e4521d..cf7c6a21a4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift @@ -15,7 +15,8 @@ import MultilineTextComponent final class StoryItemImageView: UIView { private let contentView: UIImageView - private var captureProtectedContentLayer: CaptureProtectedContentLayer? + private var captureProtectedView: UITextField? + private var captureProtectedInfo: ComponentView? private var currentMedia: EngineMedia? @@ -30,8 +31,6 @@ final class StoryItemImageView: UIView { self.contentView.contentMode = .scaleAspectFill super.init(frame: frame) - - self.addSubview(self.contentView) } required init?(coder: NSCoder) { @@ -43,28 +42,27 @@ final class StoryItemImageView: UIView { } private func updateImage(image: UIImage, isCaptureProtected: Bool) { + self.contentView.image = image + if isCaptureProtected { - let captureProtectedContentLayer: CaptureProtectedContentLayer - if let current = self.captureProtectedContentLayer { - captureProtectedContentLayer = current + let captureProtectedView: UITextField + if let current = self.captureProtectedView { + captureProtectedView = current } else { - captureProtectedContentLayer = CaptureProtectedContentLayer() - - captureProtectedContentLayer.videoGravity = .resizeAspectFill - if #available(iOS 13.0, *) { - captureProtectedContentLayer.preventsCapture = true - captureProtectedContentLayer.preventsDisplaySleepDuringVideoPlayback = false - } - - captureProtectedContentLayer.frame = self.contentView.frame - self.captureProtectedContentLayer = captureProtectedContentLayer - self.layer.addSublayer(captureProtectedContentLayer) - } - if let cmSampleBuffer = image.cmSampleBuffer { - captureProtectedContentLayer.enqueue(cmSampleBuffer) + captureProtectedView = UITextField(frame: self.contentView.frame) + captureProtectedView.isSecureTextEntry = true + self.captureProtectedView = captureProtectedView + self.layer.addSublayer(captureProtectedView.layer) + captureProtectedView.layer.sublayers?.first?.addSublayer(self.contentView.layer) } } else { - self.contentView.image = image + if self.contentView.layer.superlayer !== self.layer { + self.layer.addSublayer(self.contentView.layer) + } + if let captureProtectedView = self.captureProtectedView { + self.captureProtectedView = nil + captureProtectedView.layer.removeFromSuperlayer() + } } } @@ -86,20 +84,6 @@ final class StoryItemImageView: UIView { dimensions = representation.dimensions.cgSize if isMediaUpdated { - if isCaptureProtected { - if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { - if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) { - self.updateImage(image: image, isCaptureProtected: false) - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in - guard let self else { - return - } - self.contentView.image = nil - }) - } - } - } - if attemptSynchronous, let path = context.account.postbox.mediaBox.completedResourcePath(id: representation.resource.id, pathExtension: nil) { if #available(iOS 15.0, *) { if let image = UIImage(contentsOfFile: path)?.preparingForDisplay() { @@ -159,20 +143,6 @@ final class StoryItemImageView: UIView { dimensions = file.dimensions?.cgSize if isMediaUpdated { - if isCaptureProtected { - if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { - if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) { - self.updateImage(image: image, isCaptureProtected: false) - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in - guard let self else { - return - } - self.contentView.image = nil - }) - } - } - } - let cachedPath = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedVideoFirstFrameRepresentation()) if attemptSynchronous, FileManager.default.fileExists(atPath: cachedPath) { @@ -234,10 +204,12 @@ final class StoryItemImageView: UIView { if let dimensions { let filledSize = dimensions.aspectFilled(size) let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - filledSize.width) * 0.5), y: floor((size.height - filledSize.height) * 0.5)), size: filledSize) - transition.setFrame(view: self.contentView, frame: contentFrame) - if let captureProtectedContentLayer = self.captureProtectedContentLayer { - transition.setFrame(layer: captureProtectedContentLayer, frame: contentFrame) + if let captureProtectedView = self.captureProtectedView { + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: contentFrame.size)) + transition.setFrame(view: captureProtectedView, frame: contentFrame) + } else { + transition.setFrame(view: self.contentView, frame: contentFrame) } } From e87b8aec2913294845ac321f378d1d5c1be63ebf Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 28 Jul 2023 16:48:48 +0400 Subject: [PATCH 09/12] Animate avatar story feed progress transition (cherry picked from commit ff169b87907acd60ccaf391679909a33ffaabb74) --- .../Sources/StoryPeerListItemComponent.swift | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index c3e38c3e5d..a9da59fc1f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -194,11 +194,23 @@ private final class StoryProgressLayer: HierarchyTrackingLayer { switch params.value { case let .progress(progress): + var animateIn = false if self.indefiniteReplicatorLayer.superlayer != nil { - self.indefiniteReplicatorLayer.removeFromSuperlayer() + self.indefiniteReplicatorLayer.opacity = 0.0 + self.indefiniteReplicatorLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.indefiniteReplicatorLayer.removeFromSuperlayer() + }) + animateIn = true } if self.uploadProgressLayer.superlayer == nil { self.addSublayer(self.uploadProgressLayer) + if animateIn { + self.uploadProgressLayer.opacity = 1.0 + self.uploadProgressLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } transition.setShapeLayerStrokeEnd(layer: self.uploadProgressLayer, strokeEnd: CGFloat(progress)) if self.uploadProgressLayer.animation(forKey: "rotation") == nil { @@ -211,11 +223,23 @@ private final class StoryProgressLayer: HierarchyTrackingLayer { self.uploadProgressLayer.add(rotationAnimation, forKey: "rotation") } case .indefinite: - if self.uploadProgressLayer.superlayer == nil { - self.uploadProgressLayer.removeFromSuperlayer() + var animateIn = false + if self.uploadProgressLayer.superlayer != nil { + self.uploadProgressLayer.opacity = 0.0 + self.uploadProgressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.uploadProgressLayer.removeFromSuperlayer() + }) + animateIn = true } if self.indefiniteReplicatorLayer.superlayer == nil { self.addSublayer(self.indefiniteReplicatorLayer) + if animateIn { + self.indefiniteReplicatorLayer.opacity = 1.0 + self.indefiniteReplicatorLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } if self.indefiniteReplicatorLayer.animation(forKey: "rotation") == nil { let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") @@ -882,19 +906,32 @@ public final class StoryPeerListItemComponent: Component { case .loading: progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, value: .indefinite, transition: transition) } + self.indicatorShapeSeenLayer.opacity = 0.0 self.indicatorShapeUnseenLayer.opacity = 0.0 + + if let previousComponent = previousComponent, previousComponent.ringAnimation == nil { + progressLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.indicatorShapeSeenLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.indicatorShapeUnseenLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } } else { self.indicatorShapeSeenLayer.opacity = 1.0 self.indicatorShapeUnseenLayer.opacity = 1.0 if let progressLayer = self.progressLayer { + self.indicatorShapeSeenLayer.opacity = 1.0 + self.indicatorShapeUnseenLayer.opacity = 1.0 + + self.indicatorShapeSeenLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.indicatorShapeUnseenLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.progressLayer = nil if transition.animation.isImmediate { progressLayer.reset() progressLayer.removeFromSuperlayer() } else { - progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak progressLayer] _ in + progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressLayer] _ in progressLayer?.reset() progressLayer?.removeFromSuperlayer() }) From 2d10c3c271ecd602ec4ff9046623cbb600694cc4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 28 Jul 2023 16:49:01 +0400 Subject: [PATCH 10/12] Limit max avatar overscroll scale (cherry picked from commit 140ecde68a296d29d9cbd13c60f6dde5412bd14e) --- .../Sources/StoryPeerListComponent.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index f9fe30d554..348ed018b6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -886,7 +886,8 @@ public final class StoryPeerListComponent: Component { } else { overscrollScaleFactor = 0.0 } - let maximizedItemScale: CGFloat = 1.0 + overscrollStage1 * 0.1 + overscrollScaleFactor * overscrollStage2 * 0.5 + var maximizedItemScale: CGFloat = 1.0 + overscrollStage1 * 0.1 + overscrollScaleFactor * overscrollStage2 * 0.5 + maximizedItemScale = min(1.6, maximizedItemScale) let minItemScale: CGFloat = minimizedItemScale.interpolate(to: minimizedMaxItemScale, amount: collapsedState.minFraction) * (1.0 - collapsedState.activityFraction) + 0.1 * collapsedState.activityFraction From ac2d9630691bff8734a63c7e4bbb9fe84fbe607d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 4 Aug 2023 17:43:11 +0300 Subject: [PATCH 11/12] Stories --- .../Sources/ReactionContextNode.swift | 8 +- submodules/TelegramApi/Sources/Api0.swift | 9 +- submodules/TelegramApi/Sources/Api21.swift | 94 ++++++-- submodules/TelegramApi/Sources/Api29.swift | 34 ++- submodules/TelegramApi/Sources/Api31.swift | 39 +++- .../Account/AccountIntermediateState.swift | 8 +- .../State/AccountStateManagementUtils.swift | 67 +++++- .../TelegramEngine/Messages/Stories.swift | 215 ++++++++++------- .../Messages/StoryListContext.swift | 39 ++-- .../Messages/TelegramEngineMessages.swift | 10 +- .../Sources/MediaEditorScreen.swift | 3 +- .../Sources/StoryPreviewComponent.swift | 3 +- .../MessageInputPanelComponent/BUILD | 1 + .../MessageInputActionButtonComponent.swift | 66 +++++- .../Sources/MessageInputPanelComponent.swift | 34 ++- .../Stories/PeerListItemComponent/BUILD | 2 + .../Sources/PeerListItemComponent.swift | 134 +++++++++-- .../Sources/StoryChatContent.swift | 8 +- .../StoryItemSetContainerComponent.swift | 221 ++++++++---------- .../StoryItemSetViewListComponent.swift | 34 ++- .../Sources/StoryFooterPanelComponent.swift | 69 +++++- 21 files changed, 765 insertions(+), 333 deletions(-) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index ef680b6bfd..b2324d045d 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1776,9 +1776,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { + public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { self.isAnimatingOutToReaction = true + #if DEBUG + let hideNode = true + #endif + var foundItemNode: ReactionNode? for (_, itemNode) in self.visibleItemNodes { if let itemNode = itemNode as? ReactionNode, itemNode.item.reaction.rawValue == value { @@ -1808,7 +1812,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji { switch itemNode.item.reaction.rawValue { case .builtin: - switchToInlineImmediately = false + switchToInlineImmediately = forceSwitchToInlineImmediately case .custom: switchToInlineImmediately = !self.didTriggerExpandedReaction } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index cf0078f6b8..460faa2d2e 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -798,11 +798,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } dict[2008112412] = { return Api.StickerSetCovered.parse_stickerSetNoCovered($0) } dict[1898850301] = { return Api.StoriesStealthMode.parse_storiesStealthMode($0) } - dict[-1806085190] = { return Api.StoryItem.parse_storyItem($0) } + dict[1153718222] = { return Api.StoryItem.parse_storyItem($0) } dict[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) } dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) } - dict[-793729058] = { return Api.StoryView.parse_storyView($0) } - dict[-748199729] = { return Api.StoryViews.parse_storyViews($0) } + dict[-1329730875] = { return Api.StoryView.parse_storyView($0) } + dict[-968094825] = { return Api.StoryViews.parse_storyViews($0) } dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } @@ -915,6 +915,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1706939360] = { return Api.Update.parse_updateRecentStickers($0) } dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) } dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($0) } + dict[-475579104] = { return Api.Update.parse_updateSentStoryReaction($0) } dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) } dict[834816008] = { return Api.Update.parse_updateStickerSets($0) } dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) } @@ -1174,7 +1175,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } dict[1340440049] = { return Api.stories.Stories.parse_stories($0) } dict[-560009955] = { return Api.stories.StoryViews.parse_storyViews($0) } - dict[-79726676] = { return Api.stories.StoryViewsList.parse_storyViewsList($0) } + dict[1189722604] = { return Api.stories.StoryViewsList.parse_storyViewsList($0) } dict[933691231] = { return Api.stories.UserStories.parse_userStories($0) } dict[543450958] = { return Api.updates.ChannelDifference.parse_channelDifference($0) } dict[1041346555] = { return Api.updates.ChannelDifference.parse_channelDifferenceEmpty($0) } diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index cf242d53e2..ba1fa16627 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -408,15 +408,15 @@ public extension Api { } public extension Api { indirect enum StoryItem: TypeConstructorDescription { - case storyItem(flags: Int32, id: Int32, date: Int32, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?) + case storyItem(flags: Int32, id: Int32, date: Int32, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?, sentReaction: Api.Reaction?) case storyItemDeleted(id: Int32) case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyItem(let flags, let id, let date, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views): + case .storyItem(let flags, let id, let date, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction): if boxed { - buffer.appendInt32(-1806085190) + buffer.appendInt32(1153718222) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) @@ -440,6 +440,7 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 3) != 0 {views!.serialize(buffer, true)} + if Int(flags) & Int(1 << 15) != 0 {sentReaction!.serialize(buffer, true)} break case .storyItemDeleted(let id): if boxed { @@ -461,8 +462,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyItem(let flags, let id, let date, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views): - return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any)]) + case .storyItem(let flags, let id, let date, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction): + return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any), ("sentReaction", sentReaction as Any)]) case .storyItemDeleted(let id): return ("storyItemDeleted", [("id", id as Any)]) case .storyItemSkipped(let flags, let id, let date, let expireDate): @@ -501,6 +502,10 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.StoryViews } } + var _11: Api.Reaction? + if Int(_1!) & Int(1 << 15) != 0 {if let signature = reader.readInt32() { + _11 = Api.parse(reader, signature: signature) as? Api.Reaction + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -511,8 +516,9 @@ public extension Api { let _c8 = (Int(_1!) & Int(1 << 14) == 0) || _8 != nil let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { - return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, expireDate: _4!, caption: _5, entities: _6, media: _7!, mediaAreas: _8, privacy: _9, views: _10) + let _c11 = (Int(_1!) & Int(1 << 15) == 0) || _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, expireDate: _4!, caption: _5, entities: _6, media: _7!, mediaAreas: _8, privacy: _9, views: _10, sentReaction: _11) } else { return nil @@ -554,25 +560,26 @@ public extension Api { } public extension Api { enum StoryView: TypeConstructorDescription { - case storyView(flags: Int32, userId: Int64, date: Int32) + case storyView(flags: Int32, userId: Int64, date: Int32, reaction: Api.Reaction?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyView(let flags, let userId, let date): + case .storyView(let flags, let userId, let date, let reaction): if boxed { - buffer.appendInt32(-793729058) + buffer.appendInt32(-1329730875) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {reaction!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyView(let flags, let userId, let date): - return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any)]) + case .storyView(let flags, let userId, let date, let reaction): + return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any), ("reaction", reaction as Any)]) } } @@ -583,11 +590,16 @@ public extension Api { _2 = reader.readInt64() var _3: Int32? _3 = reader.readInt32() + var _4: Api.Reaction? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Reaction + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!) + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!, reaction: _4) } else { return nil @@ -598,16 +610,17 @@ public extension Api { } public extension Api { enum StoryViews: TypeConstructorDescription { - case storyViews(flags: Int32, viewsCount: Int32, recentViewers: [Int64]?) + case storyViews(flags: Int32, viewsCount: Int32, reactionsCount: Int32, recentViewers: [Int64]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyViews(let flags, let viewsCount, let recentViewers): + case .storyViews(let flags, let viewsCount, let reactionsCount, let recentViewers): if boxed { - buffer.appendInt32(-748199729) + buffer.appendInt32(-968094825) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(viewsCount, buffer: buffer, boxed: false) + serializeInt32(reactionsCount, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) buffer.appendInt32(Int32(recentViewers!.count)) for item in recentViewers! { @@ -619,8 +632,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyViews(let flags, let viewsCount, let recentViewers): - return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("recentViewers", recentViewers as Any)]) + case .storyViews(let flags, let viewsCount, let reactionsCount, let recentViewers): + return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("reactionsCount", reactionsCount as Any), ("recentViewers", recentViewers as Any)]) } } @@ -629,15 +642,18 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: [Int64]? + var _3: Int32? + _3 = reader.readInt32() + var _4: [Int64]? if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + _4 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, recentViewers: _3) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, reactionsCount: _3!, recentViewers: _4) } else { return nil @@ -1141,6 +1157,7 @@ public extension Api { case updateRecentStickers case updateSavedGifs case updateSavedRingtones + case updateSentStoryReaction(userId: Int64, storyId: Int32, reaction: Api.Reaction) case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity]) case updateStickerSets(flags: Int32) case updateStickerSetsOrder(flags: Int32, order: [Int64]) @@ -2013,6 +2030,14 @@ public extension Api { buffer.appendInt32(1960361625) } + break + case .updateSentStoryReaction(let userId, let storyId, let reaction): + if boxed { + buffer.appendInt32(-475579104) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(storyId, buffer: buffer, boxed: false) + reaction.serialize(buffer, true) break case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): if boxed { @@ -2346,6 +2371,8 @@ public extension Api { return ("updateSavedGifs", []) case .updateSavedRingtones: return ("updateSavedRingtones", []) + case .updateSentStoryReaction(let userId, let storyId, let reaction): + return ("updateSentStoryReaction", [("userId", userId as Any), ("storyId", storyId as Any), ("reaction", reaction as Any)]) case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): return ("updateServiceNotification", [("flags", flags as Any), ("inboxDate", inboxDate as Any), ("type", type as Any), ("message", message as Any), ("media", media as Any), ("entities", entities as Any)]) case .updateStickerSets(let flags): @@ -4086,6 +4113,25 @@ public extension Api { public static func parse_updateSavedRingtones(_ reader: BufferReader) -> Update? { return Api.Update.updateSavedRingtones } + public static func parse_updateSentStoryReaction(_ reader: BufferReader) -> Update? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.Reaction? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Reaction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateSentStoryReaction(userId: _1!, storyId: _2!, reaction: _3!) + } + else { + return nil + } + } public static func parse_updateServiceNotification(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index f7dd597db4..a8c45964ab 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -568,15 +568,17 @@ public extension Api.stories { } public extension Api.stories { enum StoryViewsList: TypeConstructorDescription { - case storyViewsList(count: Int32, views: [Api.StoryView], users: [Api.User]) + case storyViewsList(flags: Int32, count: Int32, reactionsCount: Int32, views: [Api.StoryView], users: [Api.User], nextOffset: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyViewsList(let count, let views, let users): + case .storyViewsList(let flags, let count, let reactionsCount, let views, let users, let nextOffset): if boxed { - buffer.appendInt32(-79726676) + buffer.appendInt32(1189722604) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) + serializeInt32(reactionsCount, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(views.count)) for item in views { @@ -587,33 +589,43 @@ public extension Api.stories { for item in users { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyViewsList(let count, let views, let users): - return ("storyViewsList", [("count", count as Any), ("views", views as Any), ("users", users as Any)]) + case .storyViewsList(let flags, let count, let reactionsCount, let views, let users, let nextOffset): + return ("storyViewsList", [("flags", flags as Any), ("count", count as Any), ("reactionsCount", reactionsCount as Any), ("views", views as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) } } public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? { var _1: Int32? _1 = reader.readInt32() - var _2: [Api.StoryView]? + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.StoryView]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) } - var _3: [Api.User]? + var _5: [Api.User]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.StoryViewsList.storyViewsList(count: _1!, views: _2!, users: _3!) + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.StoryViewsList.storyViewsList(flags: _1!, count: _2!, reactionsCount: _3!, views: _4!, users: _5!, nextOffset: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 5f57835ae2..69fba326d9 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -8472,15 +8472,15 @@ public extension Api.functions.stickers { } } public extension Api.functions.stories { - static func activateStealthMode(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func activateStealthMode(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(299359662) + buffer.appendInt32(1471926630) serializeInt32(flags, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.activateStealthMode", parameters: [("flags", String(describing: flags))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "stories.activateStealthMode", parameters: [("flags", String(describing: flags))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.Updates? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.Updates } return result }) @@ -8658,14 +8658,15 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func getStoryViewsList(id: Int32, offsetDate: Int32, offsetId: Int64, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getStoryViewsList(flags: Int32, q: String?, id: Int32, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1262182039) + buffer.appendInt32(-111189596) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(q!, buffer: buffer, boxed: false)} serializeInt32(id, buffer: buffer, boxed: false) - serializeInt32(offsetDate, buffer: buffer, boxed: false) - serializeInt64(offsetId, buffer: buffer, boxed: false) + serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.getStoryViewsList", parameters: [("id", String(describing: id)), ("offsetDate", String(describing: offsetDate)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.StoryViewsList? in + return (FunctionDescription(name: "stories.getStoryViewsList", parameters: [("flags", String(describing: flags)), ("q", String(describing: q)), ("id", String(describing: id)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.StoryViewsList? in let reader = BufferReader(buffer) var result: Api.stories.StoryViewsList? if let signature = reader.readInt32() { @@ -8748,6 +8749,24 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func sendReaction(flags: Int32, userId: Api.InputUser, storyId: Int32, reaction: Api.Reaction) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1235921331) + serializeInt32(flags, buffer: buffer, boxed: false) + userId.serialize(buffer, true) + serializeInt32(storyId, buffer: buffer, boxed: false) + reaction.serialize(buffer, true) + return (FunctionDescription(name: "stories.sendReaction", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("storyId", String(describing: storyId)), ("reaction", String(describing: reaction))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.stories { static func sendStory(flags: Int32, media: Api.InputMedia, mediaAreas: [Api.MediaArea]?, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule], randomId: Int64, period: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index b7de343f4b..79b4f16fca 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -123,7 +123,7 @@ enum AccountStateMutationOperation { case UpdateStory(peerId: PeerId, story: Api.StoryItem) case UpdateReadStories(peerId: PeerId, maxId: Int32) case UpdateStoryStealthMode(data: Api.StoriesStealthMode) - case UpdateStoryStealth(expireDate: Int32) + case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) } struct HoleFromPreviousState { @@ -649,13 +649,13 @@ struct AccountMutableState { self.addOperation(.UpdateStoryStealthMode(data: data)) } - mutating func updateStoryStealth(expireDate: Int32) { - self.addOperation(.UpdateStoryStealth(expireDate: expireDate)) + mutating func updateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) { + self.addOperation(.UpdateStorySentReaction(peerId: peerId, id: id, reaction: reaction)) } mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStoryStealth: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction: break case let .AddMessages(messages, location): for message in messages { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 46fc877c64..da1f555d16 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1679,6 +1679,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.readStories(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), maxId: id) case let .updateStoriesStealthMode(stealthMode): updatedState.updateStoryStealthMode(stealthMode) + case let .updateSentStoryReaction(userId, storyId, reaction): + updatedState.updateStorySentReaction(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId, reaction: reaction) default: break } @@ -3167,7 +3169,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStoryStealth: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -4557,10 +4559,65 @@ func replayFinalState( var configuration = _internal_getStoryConfigurationState(transaction: transaction) configuration.stealthModeState = Stories.StealthModeState(apiMode: data) _internal_setStoryConfigurationState(transaction: transaction, state: configuration) - case let .UpdateStoryStealth(expireDate): - var configuration = _internal_getStoryConfigurationState(transaction: transaction) - configuration.stealthModeState.activeUntilTimestamp = expireDate - _internal_setStoryConfigurationState(transaction: transaction, state: configuration) + case let .UpdateStorySentReaction(peerId, id, reaction): + var updatedPeerEntries: [StoryItemsTableEntry] = transaction.getStoryItems(peerId: peerId) + + if let index = updatedPeerEntries.firstIndex(where: { item in + return item.id == id + }) { + if let value = updatedPeerEntries[index].value.get(Stories.StoredItem.self), case let .item(item) = value { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views, + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + myReaction: MessageReaction.Reaction(apiReaction: reaction) + )) + if let entry = CodableEntry(updatedItem) { + updatedPeerEntries[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: item.expirationTimestamp, isCloseFriends: item.isCloseFriends) + } + } + } + transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries) + + if let value = transaction.getStory(id: StoryId(peerId: peerId, id: id))?.get(Stories.StoredItem.self), case let .item(item) = value { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views, + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + myReaction: MessageReaction.Reaction(apiReaction: reaction) + )) + if let entry = CodableEntry(updatedItem) { + transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry) + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 3e2a7fe6d2..05e1d115c9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -41,14 +41,17 @@ public enum Stories { public struct Views: Codable, Equatable { private enum CodingKeys: String, CodingKey { case seenCount = "seenCount" + case reactedCount = "reactedCount" case seenPeerIds = "seenPeerIds" } public var seenCount: Int + public var reactedCount: Int public var seenPeerIds: [PeerId] - public init(seenCount: Int, seenPeerIds: [PeerId]) { + public init(seenCount: Int, reactedCount: Int, seenPeerIds: [PeerId]) { self.seenCount = seenCount + self.reactedCount = reactedCount self.seenPeerIds = seenPeerIds } @@ -56,6 +59,7 @@ public enum Stories { let container = try decoder.container(keyedBy: CodingKeys.self) self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount)) + self.reactedCount = Int(try container.decodeIfPresent(Int32.self, forKey: .reactedCount) ?? 0) self.seenPeerIds = try container.decode([Int64].self, forKey: .seenPeerIds).map(PeerId.init) } @@ -63,6 +67,7 @@ public enum Stories { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(Int32(clamping: self.seenCount), forKey: .seenCount) + try container.encode(Int32(clamping: self.reactedCount), forKey: .reactedCount) try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds) } } @@ -137,7 +142,7 @@ public enum Stories { case isSelectedContacts case isForwardingDisabled case isEdited - case hasLike + case myReaction } public let id: Int32 @@ -157,7 +162,7 @@ public enum Stories { public let isSelectedContacts: Bool public let isForwardingDisabled: Bool public let isEdited: Bool - public let hasLike: Bool + public let myReaction: MessageReaction.Reaction? public init( id: Int32, @@ -177,7 +182,7 @@ public enum Stories { isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, - hasLike: Bool + myReaction: MessageReaction.Reaction? ) { self.id = id self.timestamp = timestamp @@ -196,7 +201,7 @@ public enum Stories { self.isSelectedContacts = isSelectedContacts self.isForwardingDisabled = isForwardingDisabled self.isEdited = isEdited - self.hasLike = hasLike + self.myReaction = myReaction } public init(from decoder: Decoder) throws { @@ -225,7 +230,7 @@ public enum Stories { self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false - self.hasLike = try container.decodeIfPresent(Bool.self, forKey: .hasLike) ?? false + self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction) } public func encode(to encoder: Encoder) throws { @@ -255,7 +260,7 @@ public enum Stories { try container.encode(self.isSelectedContacts, forKey: .isSelectedContacts) try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) try container.encode(self.isEdited, forKey: .isEdited) - try container.encode(self.hasLike, forKey: .hasLike) + try container.encodeIfPresent(self.myReaction, forKey: .myReaction) } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -317,7 +322,7 @@ public enum Stories { if lhs.isEdited != rhs.isEdited { return false } - if lhs.hasLike != rhs.hasLike { + if lhs.myReaction != rhs.myReaction { return false } @@ -948,7 +953,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId for update in updates.allUpdates { if case let .updateStory(_, story) = update { switch story { - case let .storyItem(_, idValue, _, _, _, _, media, _, _, _): + case let .storyItem(_, idValue, _, _, _, _, media, _, _, _, _): if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: accountPeerId, transaction: transaction) { var items = transaction.getStoryItems(peerId: accountPeerId) var updatedItems: [Stories.Item] = [] @@ -971,7 +976,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)) @@ -1093,7 +1098,7 @@ func _internal_editStory(account: Account, id: Int32, media: EngineStoryInputMed for update in updates.allUpdates { if case let .updateStory(_, story) = update { switch story { - case let .storyItem(_, _, _, _, _, _, media, _, _, _): + case let .storyItem(_, _, _, _, _, _, media, _, _, _, _): let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) if let parsedMedia = parsedMedia, let originalMedia = originalMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false) @@ -1135,7 +1140,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { transaction.setStory(id: storyId, value: entry) @@ -1163,7 +1168,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1291,7 +1296,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1318,7 +1323,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) updatedItems.append(updatedItem) } @@ -1344,7 +1349,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory extension Api.StoryItem { var id: Int32 { switch self { - case let .storyItem(_, id, _, _, _, _, _, _, _, _): + case let .storyItem(_, id, _, _, _, _, _, _, _, _, _): return id case let .storyItemDeleted(id): return id @@ -1357,12 +1362,12 @@ extension Api.StoryItem { extension Stories.Item.Views { init(apiViews: Api.StoryViews) { switch apiViews { - case let .storyViews(_, viewsCount, recentViewers): + case let .storyViews(_, viewsCount, reactionsCount, recentViewers): var seenPeerIds: [PeerId] = [] if let recentViewers = recentViewers { seenPeerIds = recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } } - self.init(seenCount: Int(viewsCount), seenPeerIds: seenPeerIds) + self.init(seenCount: Int(viewsCount), reactedCount: Int(reactionsCount), seenPeerIds: seenPeerIds) } } } @@ -1370,7 +1375,7 @@ extension Stories.Item.Views { extension Stories.StoredItem { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { switch apiStoryItem { - case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views): + case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction): let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let parsedMedia = parsedMedia { var parsedPrivacy: Stories.Item.Privacy? @@ -1427,6 +1432,13 @@ extension Stories.StoredItem { mergedViews = views.flatMap(Stories.Item.Views.init(apiViews:)) } + var mergedMyReaction: MessageReaction.Reaction? + if isMin, let existingItem = existingItem { + mergedMyReaction = existingItem.myReaction + } else { + mergedMyReaction = sentReaction.flatMap(MessageReaction.Reaction.init(apiReaction:)) + } + let item = Stories.Item( id: id, timestamp: date, @@ -1445,7 +1457,7 @@ extension Stories.StoredItem { isSelectedContacts: isSelectedContacts, isForwardingDisabled: isForwardingDisabled, isEdited: isEdited, - hasLike: false + myReaction: mergedMyReaction ) self = .item(item) } else { @@ -1532,42 +1544,12 @@ public final class StoryViewList { public let items: [Item] public let totalCount: Int + public let totalReactedCount: Int - public init(items: [Item], totalCount: Int) { + public init(items: [Item], totalCount: Int, totalReactedCount: Int) { self.items = items self.totalCount = totalCount - } -} - -func _internal_getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal { - let accountPeerId = account.peerId - return account.network.request(Api.functions.stories.getStoryViewsList(id: id, offsetDate: offsetTimestamp ?? 0, offsetId: offsetPeerId?.id._internalGetInt64Value() ?? 0, limit: Int32(limit))) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal in - guard let result = result else { - return .single(nil) - } - return account.postbox.transaction { transaction -> StoryViewList? in - switch result { - case let .storyViewsList(count, views, users): - updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) - - var items: [StoryViewList.Item] = [] - for view in views { - switch view { - case let .storyView(_, userId, date): - if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) { - items.append(StoryViewList.Item(peer: EnginePeer(peer), timestamp: date)) - } - } - } - - return StoryViewList(items: items, totalCount: Int(count)) - } - } + self.totalReactedCount = totalReactedCount } } @@ -1602,26 +1584,28 @@ func _internal_getStoryViews(account: Account, ids: [Int32]) -> Signal<[Int32: S public final class EngineStoryViewListContext { public struct LoadMoreToken: Equatable { - var id: Int64 - var timestamp: Int32 + var value: String } public final class Item: Equatable { public let peer: EnginePeer public let timestamp: Int32 public let storyStats: PeerStoryStats? - public let isLike: Bool + public let reaction: MessageReaction.Reaction? + public let reactionFile: TelegramMediaFile? public init( peer: EnginePeer, timestamp: Int32, storyStats: PeerStoryStats?, - isLike: Bool + reaction: MessageReaction.Reaction?, + reactionFile: TelegramMediaFile? ) { self.peer = peer self.timestamp = timestamp self.storyStats = storyStats - self.isLike = isLike + self.reaction = reaction + self.reactionFile = reactionFile } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -1634,7 +1618,10 @@ public final class EngineStoryViewListContext { if lhs.storyStats != rhs.storyStats { return false } - if lhs.isLike != rhs.isLike { + if lhs.reaction != rhs.reaction { + return false + } + if lhs.reactionFile?.fileId != rhs.reactionFile?.fileId { return false } return true @@ -1643,15 +1630,18 @@ public final class EngineStoryViewListContext { public struct State: Equatable { public var totalCount: Int + public var totalReactedCount: Int public var items: [Item] public var loadMoreToken: LoadMoreToken? public init( totalCount: Int, + totalReactedCount: Int, items: [Item], loadMoreToken: LoadMoreToken? ) { self.totalCount = totalCount + self.totalReactedCount = totalReactedCount self.items = items self.loadMoreToken = loadMoreToken } @@ -1659,12 +1649,12 @@ public final class EngineStoryViewListContext { private final class Impl { struct NextOffset: Equatable { - var id: Int64 - var timestamp: Int32 + var value: String } struct InternalState: Equatable { var totalCount: Int + var totalReactedCount: Int var items: [Item] var canLoadMore: Bool var nextOffset: NextOffset? @@ -1688,8 +1678,8 @@ public final class EngineStoryViewListContext { self.account = account self.storyId = storyId - let initialState = State(totalCount: views.seenCount, items: [], loadMoreToken: LoadMoreToken(id: 0, timestamp: 0)) - self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil) + let initialState = State(totalCount: views.seenCount, totalReactedCount: views.reactedCount, items: [], loadMoreToken: LoadMoreToken(value: "")) + self.state = InternalState(totalCount: initialState.totalCount, totalReactedCount: initialState.totalReactedCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil) self.statePromise.set(.single(self.state)) if initialState.loadMoreToken != nil { @@ -1721,7 +1711,7 @@ public final class EngineStoryViewListContext { let signal: Signal = self.account.postbox.transaction { transaction -> Void in } |> mapToSignal { _ -> Signal in - return account.network.request(Api.functions.stories.getStoryViewsList(id: storyId, offsetDate: currentOffset?.timestamp ?? 0, offsetId: currentOffset?.id ?? 0, limit: Int32(limit))) + return account.network.request(Api.functions.stories.getStoryViewsList(flags: 0, q: nil, id: storyId, offset: currentOffset?.value ?? "", limit: Int32(limit))) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -1729,14 +1719,13 @@ public final class EngineStoryViewListContext { |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> InternalState in switch result { - case let .storyViewsList(count, views, users): + case let .storyViewsList(_, count, reactionsCount, views, users, nextOffset): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) var items: [Item] = [] - var nextOffset: NextOffset? for view in views { switch view { - case let .storyView(flags, userId, date): + case let .storyView(flags, userId, date, reaction): let isBlocked = (flags & (1 << 0)) != 0 let isBlockedFromStories = (flags & (1 << 1)) != 0 @@ -1757,9 +1746,21 @@ public final class EngineStoryViewListContext { return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags) }) if let peer = transaction.getPeer(peerId) { - items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId), isLike: false)) - - nextOffset = NextOffset(id: userId, timestamp: date) + let parsedReaction = reaction.flatMap(MessageReaction.Reaction.init(apiReaction:)) + items.append(Item( + peer: EnginePeer(peer), + timestamp: date, + storyStats: transaction.getPeerStoryStats(peerId: peerId), + reaction: parsedReaction, + reactionFile: parsedReaction.flatMap { reaction -> TelegramMediaFile? in + switch reaction { + case .builtin: + return nil + case let .custom(fileId): + return transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile + } + } + )) } } } @@ -1773,7 +1774,7 @@ public final class EngineStoryViewListContext { mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds), + views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, @@ -1783,7 +1784,7 @@ public final class EngineStoryViewListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry) @@ -1802,7 +1803,7 @@ public final class EngineStoryViewListContext { mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds), + views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, @@ -1812,7 +1813,7 @@ public final class EngineStoryViewListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1822,9 +1823,9 @@ public final class EngineStoryViewListContext { } transaction.setStoryItems(peerId: account.peerId, items: currentItems) - return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset) + return InternalState(totalCount: Int(count), totalReactedCount: Int(reactionsCount), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) }) case .none: - return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil) + return InternalState(totalCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil) } } } @@ -1852,10 +1853,22 @@ public final class EngineStoryViewListContext { existingItems.insert(itemHash) strongSelf.state.items.append(item) } + + var allReactedCount = 0 + for item in strongSelf.state.items { + if item.reaction != nil { + allReactedCount += 1 + } else { + break + } + } + if state.canLoadMore { strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count) + strongSelf.state.totalReactedCount = max(state.totalReactedCount, allReactedCount) } else { strongSelf.state.totalCount = strongSelf.state.items.count + strongSelf.state.totalReactedCount = allReactedCount } strongSelf.state.canLoadMore = state.canLoadMore strongSelf.state.nextOffset = state.nextOffset @@ -1883,7 +1896,8 @@ public final class EngineStoryViewListContext { peer: item.peer, timestamp: item.timestamp, storyStats: value, - isLike: false + reaction: item.reaction, + reactionFile: item.reactionFile ) } } @@ -1906,10 +1920,11 @@ public final class EngineStoryViewListContext { disposable.set(impl.statePromise.get().start(next: { state in var loadMoreToken: LoadMoreToken? if let nextOffset = state.nextOffset { - loadMoreToken = LoadMoreToken(id: nextOffset.id, timestamp: nextOffset.timestamp) + loadMoreToken = LoadMoreToken(value: nextOffset.value) } subscriber.putNext(State( totalCount: state.totalCount, + totalReactedCount: state.totalReactedCount, items: state.items, loadMoreToken: loadMoreToken )) @@ -2112,10 +2127,15 @@ func _internal_enableStoryStealthMode(account: Account) -> Signal `catch` { _ -> Signal in - return .single(.boolFalse) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) } |> mapToSignal { result -> Signal in + if let result = result { + account.stateManager.addUpdates(result) + } + return account.postbox.transaction { transaction in let appConfig = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue @@ -2157,8 +2177,15 @@ public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction, transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data())) } -func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal { - return account.postbox.transaction { transaction -> Void in +func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal { + return account.postbox.transaction { transaction -> Api.InputUser? in + guard let peer = transaction.getPeer(peerId) else { + return nil + } + guard let inputUser = apiInputUser(peer) else { + return nil + } + var currentItems = transaction.getStoryItems(peerId: peerId) for i in 0 ..< currentItems.count { if currentItems[i].id == id { @@ -2181,7 +2208,7 @@ func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: hasLike + myReaction: reaction )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -2210,12 +2237,30 @@ func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: hasLike + myReaction: reaction )) if let entry = CodableEntry(updatedItem) { transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry) } } + + return inputUser + } + |> mapToSignal { inputUser -> Signal in + guard let inputUser = inputUser else { + return .complete() + } + return account.network.request(Api.functions.stories.sendReaction(flags: 0, userId: inputUser, storyId: id, reaction: reaction?.apiReaction ?? .reactionEmpty)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + + return .complete() + } } - |> ignoreValues } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index c79f60e2de..68e1322644 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -13,10 +13,12 @@ enum InternalStoryUpdate { public final class EngineStoryItem: Equatable { public final class Views: Equatable { public let seenCount: Int + public let reactedCount: Int public let seenPeers: [EnginePeer] - public init(seenCount: Int, seenPeers: [EnginePeer]) { + public init(seenCount: Int, reactedCount: Int, seenPeers: [EnginePeer]) { self.seenCount = seenCount + self.reactedCount = reactedCount self.seenPeers = seenPeers } @@ -24,6 +26,9 @@ public final class EngineStoryItem: Equatable { if lhs.seenCount != rhs.seenCount { return false } + if lhs.reactedCount != rhs.reactedCount { + return false + } if lhs.seenPeers != rhs.seenPeers { return false } @@ -49,9 +54,9 @@ public final class EngineStoryItem: Equatable { public let isSelectedContacts: Bool public let isForwardingDisabled: Bool public let isEdited: Bool - public let hasLike: Bool + public let myReaction: MessageReaction.Reaction? - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, hasLike: Bool) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, myReaction: MessageReaction.Reaction?) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp @@ -70,7 +75,7 @@ public final class EngineStoryItem: Equatable { self.isSelectedContacts = isSelectedContacts self.isForwardingDisabled = isForwardingDisabled self.isEdited = isEdited - self.hasLike = hasLike + self.myReaction = myReaction } public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { @@ -128,7 +133,7 @@ public final class EngineStoryItem: Equatable { if lhs.isEdited != rhs.isEdited { return false } - if lhs.hasLike != rhs.hasLike { + if lhs.myReaction != rhs.myReaction { return false } return true @@ -148,6 +153,7 @@ extension EngineStoryItem { views: self.views.flatMap { views in return Stories.Item.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeerIds: views.seenPeers.map(\.id) ) }, @@ -165,7 +171,7 @@ extension EngineStoryItem { isSelectedContacts: self.isSelectedContacts, isForwardingDisabled: self.isForwardingDisabled, isEdited: self.isEdited, - hasLike: self.hasLike + myReaction: self.myReaction ) } } @@ -520,6 +526,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) } @@ -535,7 +542,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) items.append(mappedItem) @@ -646,6 +653,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) } @@ -661,7 +669,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) storyItems.append(mappedItem) } @@ -796,6 +804,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -811,7 +820,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) finalUpdatedState = updatedState } @@ -837,6 +846,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -852,7 +862,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) finalUpdatedState = updatedState } else { @@ -880,6 +890,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -895,7 +906,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) updatedState.items.sort(by: { lhs, rhs in return lhs.timestamp > rhs.timestamp @@ -919,6 +930,7 @@ public final class PeerStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -934,7 +946,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) updatedState.items.sort(by: { lhs, rhs in return lhs.timestamp > rhs.timestamp @@ -1082,6 +1094,7 @@ public final class PeerExpiringStoryListContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) } @@ -1097,7 +1110,7 @@ public final class PeerExpiringStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) items.append(.item(mappedItem)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 628ee37757..13839bcd66 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1035,7 +1035,7 @@ public extension TelegramEngine { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1095,10 +1095,6 @@ public extension TelegramEngine { return _internal_updateStoriesArePinned(account: self.account, ids: ids, isPinned: isPinned) } - public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal { - return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit) - } - public func storyViewList(id: Int32, views: EngineStoryItem.Views) -> EngineStoryViewListContext { return EngineStoryViewListContext(account: self.account, storyId: id, views: views) } @@ -1111,8 +1107,8 @@ public extension TelegramEngine { return _internal_enableStoryStealthMode(account: self.account) } - public func setStoryLike(peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal { - return _internal_setStoryLike(account: self.account, peerId: peerId, id: id, hasLike: hasLike) + public func setStoryReaction(peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal { + return _internal_setStoryReaction(account: self.account, peerId: peerId, id: id, reaction: reaction) } } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 0a02fc1b5f..982b49272e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1113,8 +1113,9 @@ final class MediaEditorScreenComponent: Component { stopAndPreviewMediaRecording: nil, discardMediaRecordingPreview: nil, attachmentAction: nil, - hasLike: false, + myReaction: nil, likeAction: nil, + likeOptionsAction: nil, inputModeAction: { [weak self] in if let self { switch self.currentInputMode { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index f590e981d2..b2580c4c84 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -266,8 +266,9 @@ final class StoryPreviewComponent: Component { stopAndPreviewMediaRecording: nil, discardMediaRecordingPreview: nil, attachmentAction: { }, - hasLike: false, + myReaction: nil, likeAction: nil, + likeOptionsAction: nil, inputModeAction: nil, timeoutAction: nil, forwardAction: {}, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index 1951f54982..fd6453217f 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/StickerPeekUI", + "//submodules/Components/ReactionButtonListComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift index 422ca44d16..5a4b1a7e2b 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift @@ -9,6 +9,8 @@ import TelegramPresentationData import ChatPresentationInterfaceState import MoreHeaderButton import ContextUI +import ReactionButtonListComponent +import TelegramCore private extension MessageInputActionButtonComponent.Mode { var iconName: String? { @@ -19,11 +21,11 @@ private extension MessageInputActionButtonComponent.Mode { return "Chat/Input/Text/IconAttachment" case .forward: return "Chat/Input/Text/IconForwardSend" - case let .like(isActive): - if isActive { - return "Stories/InputLikeOn" - } else { + case let .like(reaction, _, _): + if reaction == nil { return "Stories/InputLikeOff" + } else { + return nil } default: return nil @@ -43,7 +45,7 @@ public final class MessageInputActionButtonComponent: Component { case attach case forward case more - case like(isActive: Bool) + case like(reaction: MessageReaction.Reaction?, file: TelegramMediaFile?, animationFileId: Int64?) } public enum Action { @@ -127,12 +129,18 @@ public final class MessageInputActionButtonComponent: Component { public let referenceNode: ContextReferenceContentNode public let containerNode: ContextControllerSourceNode private let sendIconView: UIImageView - private var moreButton: MoreHeaderButton? + private var reactionIconView: ReactionIconView? private var component: MessageInputActionButtonComponent? private weak var componentState: EmptyComponentState? + private var acceptNextButtonPress: Bool = false + + public var likeIconView: UIView? { + return self.reactionIconView + } + override init(frame: CGRect) { self.sendIconView = UIImageView() @@ -157,6 +165,7 @@ public final class MessageInputActionButtonComponent: Component { guard let self, let component = self.component, let longPressAction = component.longPressAction else { return } + self.acceptNextButtonPress = false longPressAction(self, gesture) } @@ -173,8 +182,6 @@ public final class MessageInputActionButtonComponent: Component { self.button.addTarget(self, action: #selector(self.touchDown), forControlEvents: .touchDown) self.button.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) -// but.addTarget(self, action: #selector(self.touchDown), for: .touchDown) -// self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -182,6 +189,8 @@ public final class MessageInputActionButtonComponent: Component { } @objc private func touchDown() { + self.acceptNextButtonPress = true + guard let component = self.component else { return } @@ -189,6 +198,10 @@ public final class MessageInputActionButtonComponent: Component { } @objc private func pressed() { + if !self.acceptNextButtonPress { + return + } + guard let component = self.component else { return } @@ -318,11 +331,10 @@ public final class MessageInputActionButtonComponent: Component { if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName { if let iconName = component.mode.iconName { - var tintColor: UIColor = .white - if case .like(true) = component.mode { - tintColor = UIColor(rgb: 0xFF3B30) - } + let tintColor: UIColor = .white self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: tintColor) + } else if case let .like(reaction, _, _) = component.mode, reaction != nil { + self.sendIconView.image = nil } else if case .apply = component.mode { self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -379,6 +391,36 @@ public final class MessageInputActionButtonComponent: Component { } } + if case let .like(reactionValue, reactionFile, animationFileId) = component.mode, let reaction = reactionValue { + let reactionIconFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 3.0, dy: 3.0) + + let reactionIconView: ReactionIconView + if let current = self.reactionIconView { + reactionIconView = current + } else { + reactionIconView = ReactionIconView(frame: reactionIconFrame) + reactionIconView.isUserInteractionEnabled = false + self.reactionIconView = reactionIconView + self.addSubview(reactionIconView) + } + transition.setFrame(view: reactionIconView, frame: reactionIconFrame) + reactionIconView.update( + size: reactionIconFrame.size, + context: component.context, + file: reactionFile, + fileId: animationFileId ?? reactionFile?.fileId.id ?? 0, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.2), + animateIdle: false, + reaction: reaction, + transition: .immediate + ) + } else if let reactionIconView = self.reactionIconView { + self.reactionIconView = nil + reactionIconView.removeFromSuperview() + } + transition.setFrame(view: self.button.view, frame: CGRect(origin: .zero, size: availableSize)) transition.setFrame(view: self.containerNode.view, frame: CGRect(origin: .zero, size: availableSize)) transition.setFrame(view: self.referenceNode.view, frame: CGRect(origin: .zero, size: availableSize)) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index f15878e994..adfae1ffe7 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -44,6 +44,18 @@ public final class MessageInputPanelComponent: Component { case emoji } + public struct MyReaction: Equatable { + public let reaction: MessageReaction.Reaction + public let file: TelegramMediaFile? + public let animationFileId: Int64? + + public init(reaction: MessageReaction.Reaction, file: TelegramMediaFile?, animationFileId: Int64?) { + self.reaction = reaction + self.file = file + self.animationFileId = animationFileId + } + } + public final class ExternalState { public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var hasText: Bool = false @@ -79,8 +91,9 @@ public final class MessageInputPanelComponent: Component { public let stopAndPreviewMediaRecording: (() -> Void)? public let discardMediaRecordingPreview: (() -> Void)? public let attachmentAction: (() -> Void)? - public let hasLike: Bool + public let myReaction: MyReaction? public let likeAction: (() -> Void)? + public let likeOptionsAction: ((UIView, ContextGesture?) -> Void)? public let inputModeAction: (() -> Void)? public let timeoutAction: ((UIView) -> Void)? public let forwardAction: (() -> Void)? @@ -126,8 +139,9 @@ public final class MessageInputPanelComponent: Component { stopAndPreviewMediaRecording: (() -> Void)?, discardMediaRecordingPreview: (() -> Void)?, attachmentAction: (() -> Void)?, - hasLike: Bool, + myReaction: MyReaction?, likeAction: (() -> Void)?, + likeOptionsAction: ((UIView, ContextGesture?) -> Void)?, inputModeAction: (() -> Void)?, timeoutAction: ((UIView) -> Void)?, forwardAction: (() -> Void)?, @@ -172,8 +186,9 @@ public final class MessageInputPanelComponent: Component { self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording self.discardMediaRecordingPreview = discardMediaRecordingPreview self.attachmentAction = attachmentAction - self.hasLike = hasLike + self.myReaction = myReaction self.likeAction = likeAction + self.likeOptionsAction = likeOptionsAction self.inputModeAction = inputModeAction self.timeoutAction = timeoutAction self.forwardAction = forwardAction @@ -280,12 +295,15 @@ public final class MessageInputPanelComponent: Component { if (lhs.attachmentAction == nil) != (rhs.attachmentAction == nil) { return false } - if lhs.hasLike != rhs.hasLike { + if lhs.myReaction != rhs.myReaction { return false } if (lhs.likeAction == nil) != (rhs.likeAction == nil) { return false } + if (lhs.likeOptionsAction == nil) != (rhs.likeOptionsAction == nil) { + return false + } return true } @@ -345,6 +363,10 @@ public final class MessageInputPanelComponent: Component { return self.likeButton.view } + public var likeIconView: UIView? { + return (self.likeButton.view as? MessageInputActionButtonComponent.View)?.likeIconView + } + override init(frame: CGRect) { self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true) @@ -1064,7 +1086,7 @@ public final class MessageInputPanelComponent: Component { let likeButtonSize = self.likeButton.update( transition: transition, component: AnyComponent(MessageInputActionButtonComponent( - mode: .like(isActive: component.hasLike), + mode: .like(reaction: component.myReaction?.reaction, file: component.myReaction?.file, animationFileId: component.myReaction?.animationFileId), action: { [weak self] _, action, _ in guard let self, let component = self.component else { return @@ -1074,7 +1096,7 @@ public final class MessageInputPanelComponent: Component { } component.likeAction?() }, - longPressAction: nil, + longPressAction: component.likeOptionsAction, switchMediaInputMode: { }, updateMediaCancelFraction: { _ in diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD index 90a414e411..e6d82851b0 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD @@ -25,7 +25,9 @@ swift_library( "//submodules/AppBundle", "//submodules/PeerPresenceStatusManager", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/ContextUI", + "//submodules/TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 2b726efb00..ee3ad3d1a1 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -16,6 +16,8 @@ import AppBundle import PeerPresenceStatusManager import EmojiStatusComponent import ContextUI +import EmojiTextAttachmentView +import TextFormat private let avatarFont = avatarPlaceholderFont(size: 15.0) private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) @@ -44,6 +46,39 @@ public final class PeerListItemComponent: Component { case checks } + public final class Reaction: Equatable { + public let reaction: MessageReaction.Reaction + public let file: TelegramMediaFile? + public let animationFileId: Int64? + + public init( + reaction: MessageReaction.Reaction, + file: TelegramMediaFile?, + animationFileId: Int64? + ) { + self.reaction = reaction + self.file = file + self.animationFileId = animationFileId + } + + public static func ==(lhs: Reaction, rhs: Reaction) -> Bool { + if lhs === rhs { + return true + } + if lhs.reaction != rhs.reaction { + return false + } + if lhs.file?.fileId != rhs.file?.fileId { + return false + } + if lhs.animationFileId != rhs.animationFileId { + return false + } + + return true + } + } + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings @@ -55,7 +90,7 @@ public final class PeerListItemComponent: Component { let subtitle: String? let subtitleAccessory: SubtitleAccessory let presence: EnginePeer.Presence? - let displayLike: Bool + let reaction: Reaction? let selectionState: SelectionState let hasNext: Bool let action: (EnginePeer) -> Void @@ -74,7 +109,7 @@ public final class PeerListItemComponent: Component { subtitle: String?, subtitleAccessory: SubtitleAccessory, presence: EnginePeer.Presence?, - displayLike: Bool = false, + reaction: Reaction? = nil, selectionState: SelectionState, hasNext: Bool, action: @escaping (EnginePeer) -> Void, @@ -92,7 +127,7 @@ public final class PeerListItemComponent: Component { self.subtitle = subtitle self.subtitleAccessory = subtitleAccessory self.presence = presence - self.displayLike = displayLike + self.reaction = reaction self.selectionState = selectionState self.hasNext = hasNext self.action = action @@ -134,7 +169,7 @@ public final class PeerListItemComponent: Component { if lhs.presence != rhs.presence { return false } - if lhs.displayLike != rhs.displayLike { + if lhs.reaction != rhs.reaction { return false } if lhs.selectionState != rhs.selectionState { @@ -160,7 +195,10 @@ public final class PeerListItemComponent: Component { private var iconView: UIImageView? private var checkLayer: CheckLayer? - private var likeIconView: UIImageView? + private var reactionLayer: InlineStickerItemLayer? + private var iconFrame: CGRect? + private var file: TelegramMediaFile? + private var fileDisposable: Disposable? private var component: PeerListItemComponent? private weak var state: EmptyComponentState? @@ -251,6 +289,10 @@ public final class PeerListItemComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.fileDisposable?.dispose() + } + @objc private func pressed() { guard let component = self.component, let peer = component.peer else { return @@ -265,7 +307,49 @@ public final class PeerListItemComponent: Component { component.openStories?(peer, self.avatarNode) } + private func updateReactionLayer() { + guard let component = self.component else { + return + } + + if let reactionLayer = self.reactionLayer { + self.reactionLayer = nil + reactionLayer.removeFromSuperlayer() + } + + guard let file = self.file else { + return + } + + let reactionLayer = InlineStickerItemLayer( + context: component.context, + userLocation: .other, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file), + file: file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.1), + pointSize: CGSize(width: 64.0, height: 64.0) + ) + self.reactionLayer = reactionLayer + + if let reaction = component.reaction, case .custom = reaction.reaction { + reactionLayer.isVisibleForAnimations = true + } + self.layer.addSublayer(reactionLayer) + + if var iconFrame = self.iconFrame { + if let reaction = component.reaction, case .builtin = reaction.reaction { + iconFrame = iconFrame.insetBy(dx: -iconFrame.width * 0.5, dy: -iconFrame.height * 0.5) + } + reactionLayer.frame = iconFrame + } + } + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + var synchronousLoad = false if let hint = transition.userData(TransitionHint.self) { synchronousLoad = hint.synchronousLoad @@ -351,7 +435,7 @@ public final class PeerListItemComponent: Component { leftInset += 9.0 } var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset - if component.displayLike { + if component.reaction != nil { rightInset += 32.0 } @@ -581,25 +665,29 @@ public final class PeerListItemComponent: Component { transition.setFrame(view: labelView, frame: labelFrame) } - if component.displayLike { - let likeIconView: UIImageView - if let current = self.likeIconView { - likeIconView = current + let imageSize = CGSize(width: 22.0, height: 22.0) + self.iconFrame = CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 14.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize) + + if previousComponent?.reaction != component.reaction { + if let reaction = component.reaction { + switch reaction.reaction { + case .builtin: + self.file = reaction.file + self.updateReactionLayer() + case let .custom(fileId): + self.fileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> deliverOnMainQueue).start(next: { [weak self] files in + guard let self, let file = files[fileId] else { + return + } + self.file = file + self.updateReactionLayer() + }) + } } else { - likeIconView = UIImageView() - self.likeIconView = likeIconView - self.containerButton.addSubview(likeIconView) - - likeIconView.image = PresentationResourcesChat.storyViewListLikeIcon(component.theme) + self.file = nil + self.updateReactionLayer() } - - if let _ = likeIconView.image { - let imageSize = CGSize(width: 32.0, height: 32.0) - transition.setFrame(view: likeIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 11.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize)) - } - } else if let likeIconView = self.likeIconView { - self.likeIconView = nil - likeIconView.removeFromSuperview() } if themeUpdated { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 38276c86aa..62fb62a08c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -158,6 +158,7 @@ public final class StoryContentContextImpl: StoryContentContext { views: item.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -173,7 +174,7 @@ public final class StoryContentContextImpl: StoryContentContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, - hasLike: item.hasLike + myReaction: item.myReaction ) } var totalCount = peerStoryItemsView.items.count @@ -198,7 +199,7 @@ public final class StoryContentContextImpl: StoryContentContext { isSelectedContacts: item.privacy.base == .nobody, isForwardingDisabled: false, isEdited: false, - hasLike: false + myReaction: nil )) totalCount += 1 } @@ -1029,6 +1030,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { views: itemValue.views.flatMap { views in return EngineStoryItem.Views( seenCount: views.seenCount, + reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) } @@ -1044,7 +1046,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isSelectedContacts: itemValue.isSelectedContacts, isForwardingDisabled: itemValue.isForwardingDisabled, isEdited: itemValue.isEdited, - hasLike: itemValue.hasLike + myReaction: itemValue.myReaction ) let mainItem = StoryContentItem( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 673e2b5490..ae12847873 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -418,6 +418,8 @@ public final class StoryItemSetContainerComponent: Component { var reactionContextNode: ReactionContextNode? weak var disappearingReactionContextNode: ReactionContextNode? + var displayLikeReactions: Bool = false + var waitingForReactionAnimateOutToLike: MessageReaction.Reaction? weak var contextController: ContextController? weak var privacyController: ShareWithPeersScreen? @@ -778,7 +780,11 @@ public final class StoryItemSetContainerComponent: Component { if let _ = self.sendMessageContext.menuController { return } - if self.hasActiveDeactivateableInput() { + if self.displayLikeReactions { + self.displayLikeReactions = false + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.updateIsProgressPaused() + } else if self.hasActiveDeactivateableInput() { Queue.mainQueue().justDispatch { self.deactivateInput() } @@ -1062,6 +1068,9 @@ public final class StoryItemSetContainerComponent: Component { if let captionItem = self.captionItem, captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText { return .blurred } + if self.displayLikeReactions { + return .blurred + } return .play } @@ -2039,13 +2048,44 @@ public final class StoryItemSetContainerComponent: Component { } self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default) }, - hasLike: component.slice.item.storyItem.hasLike, + myReaction: component.slice.item.storyItem.myReaction.flatMap { value -> MessageInputPanelComponent.MyReaction? in + var centerAnimation: TelegramMediaFile? + var animationFileId: Int64? + + switch value { + case .builtin: + if let availableReactions = component.availableReactions { + for availableReaction in availableReactions.reactionItems { + if availableReaction.reaction.rawValue == value { + centerAnimation = availableReaction.listAnimation + break + } + } + } + case let .custom(fileId): + animationFileId = fileId + } + + if animationFileId == nil && centerAnimation == nil { + return nil + } + + return MessageInputPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId) + }, likeAction: component.slice.peer.isService ? nil : { [weak self] in guard let self else { return } self.performLikeAction() }, + likeOptionsAction: component.slice.peer.isService ? nil : { [weak self] sourceView, gesture in + gesture?.cancel() + + guard let self else { + return + } + self.performLikeOptionsAction(sourceView: sourceView) + }, inputModeAction: { [weak self] in guard let self else { return @@ -2317,6 +2357,7 @@ public final class StoryItemSetContainerComponent: Component { minimizedContentHeight: 325.0, outerExpansionFraction: outerExpansionFraction, outerExpansionDirection: outerExpansionDirection, + availableReactions: component.availableReactions, close: { [weak self] in guard let self else { return @@ -3216,10 +3257,21 @@ public final class StoryItemSetContainerComponent: Component { } } - let reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) + let reactionsAnchorRect: CGRect + if self.displayLikeReactions, let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView { + var likeRect = likeButtonView.convert(likeButtonView.bounds, to: self) + likeRect.origin.y -= 14.0 + likeRect.size.height += 14.0 + reactionsAnchorRect = likeRect + } else { + reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) + } var effectiveDisplayReactions = false - if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText { + if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText { + effectiveDisplayReactions = true + } + if self.displayLikeReactions { effectiveDisplayReactions = true } if self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil { @@ -3300,124 +3352,22 @@ public final class StoryItemSetContainerComponent: Component { self.state?.updated(transition: Transition(transition)) } ) - reactionContextNode.displayTail = false + reactionContextNode.displayTail = self.displayLikeReactions self.reactionContextNode = reactionContextNode - reactionContextNode.reactionSelected = { [weak self, weak reactionContextNode] updateReaction, _ in + reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in guard let self, let component = self.component else { return } - let _ = (component.context.engine.stickers.availableReactions() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] availableReactions in - guard let self, let component = self.component, let availableReactions else { - return - } - - var animation: TelegramMediaFile? - for reaction in availableReactions.reactions { - if reaction.value == updateReaction.reaction { - animation = reaction.centerAnimation - break - } - } - - let targetView = UIView(frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - 100.0) * 0.5), y: floor((self.bounds.height - 100.0) * 0.5)), size: CGSize(width: 100.0, height: 100.0))) - targetView.isUserInteractionEnabled = false - self.addSubview(targetView) - - if let reactionContextNode { - reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction) - reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in - guard let self else { - return - } - standaloneReactionAnimation.frame = self.bounds - self.addSubview(standaloneReactionAnimation.view) - }, completion: { [weak targetView, weak reactionContextNode] in - targetView?.removeFromSuperview() - if let reactionContextNode { - reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false) - reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in - reactionContextNode?.view.removeFromSuperview() - }) - } - }) - } - - if hasFirstResponder(self) { - self.sendMessageContext.currentInputMode = .text - self.endEditing(true) - } + if component.slice.item.storyItem.myReaction == updateReaction.reaction { + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).start() + self.displayLikeReactions = false self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) - - var text = "" - var messageAttributes: [MessageAttribute] = [] - var inlineStickers: [MediaId : Media] = [:] - switch updateReaction { - case let .builtin(textValue): - text = textValue - case let .custom(fileId, file): - if let file { - animation = file - loop: for attribute in file.attributes { - switch attribute { - case let .CustomEmoji(_, _, displayText, _): - text = displayText - let length = (text as NSString).length - messageAttributes = [TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< length, type: .CustomEmoji(stickerPack: nil, fileId: fileId))])] - inlineStickers = [file.fileId: file] - break loop - default: - break - } - } - } - } - - let message: EnqueueMessage = .message( - text: text, - attributes: messageAttributes, - inlineStickers: inlineStickers, - mediaReference: nil, - replyToMessageId: nil, - replyToStoryId: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id), - localGroupingKey: nil, - correlationId: nil, - bubbleUpEmojiOrStickersets: [] - ) - - let context = component.context - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let presentController = component.presentController - let peer = component.slice.peer - - let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: [message]) - |> deliverOnMainQueue).start(next: { [weak self] messageIds in - if let animation, let self, let component = self.component { - let controller = UndoOverlayController( - presentationData: presentationData, - content: .sticker(context: context, file: animation, loop: false, title: nil, text: component.strings.Story_ToastReactionSent, undoText: component.strings.Story_ToastViewInChat, customAction: { [weak self] in - if let messageId = messageIds.first, let self { - self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) }) - } - }), - elevatedLayout: false, - animateInAsReplacement: false, - action: { [weak self] _ in - self?.sendMessageContext.tooltipScreen = nil - self?.updateIsProgressPaused() - return false - } - ) - self.sendMessageContext.tooltipScreen?.dismiss() - self.sendMessageContext.tooltipScreen = controller - self.updateIsProgressPaused() - presentController(controller, nil) - } - }) - }) + } else { + self.waitingForReactionAnimateOutToLike = updateReaction.reaction + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start() + } } reactionContextNode.premiumReactionsSelected = { [weak self] file in @@ -3478,12 +3428,41 @@ public final class StoryItemSetContainerComponent: Component { } } else { reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) - reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition) + reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: !self.displayLikeReactions, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition) if animateReactionsIn { reactionContextNode.animateIn(from: reactionsAnchorRect) } } + + if let waitingReaction = self.waitingForReactionAnimateOutToLike, component.slice.item.storyItem.myReaction == waitingReaction { + self.waitingForReactionAnimateOutToLike = nil + + if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeIconView { + reactionContextNode.willAnimateOutToReaction(value: waitingReaction) + reactionContextNode.animateOutToReaction(value: waitingReaction, targetView: likeButtonView, hideNode: true, animateTargetContainer: nil, addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + standaloneReactionAnimation.frame = self.bounds + self.addSubview(standaloneReactionAnimation.view) + }, completion: { [weak reactionContextNode] in + if let reactionContextNode { + reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in + reactionContextNode?.view.removeFromSuperview() + }) + } + }) + } + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.displayLikeReactions = false + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + } + } } else { if let reactionContextNode = self.reactionContextNode { if let disappearingReactionContextNode = self.disappearingReactionContextNode { @@ -4192,9 +4171,9 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.messages.setStoryLike(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, hasLike: !component.slice.item.storyItem.hasLike).start() + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: component.slice.item.storyItem.myReaction == nil ? .builtin("❤") : nil).start() - if component.slice.item.storyItem.hasLike { + if component.slice.item.storyItem.myReaction != nil { return } @@ -4240,6 +4219,12 @@ public final class StoryItemSetContainerComponent: Component { ) } + private func performLikeOptionsAction(sourceView: UIView) { + self.displayLikeReactions = true + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.updateIsProgressPaused() + } + func dismissAllTooltips() { guard let component = self.component, let controller = component.controller() else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index b32e58ebce..35bfa26582 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -67,6 +67,7 @@ final class StoryItemSetViewListComponent: Component { let minimizedContentHeight: CGFloat let outerExpansionFraction: CGFloat let outerExpansionDirection: Bool + let availableReactions: StoryAvailableReactions? let close: () -> Void let expandViewStats: () -> Void let deleteAction: () -> Void @@ -88,6 +89,7 @@ final class StoryItemSetViewListComponent: Component { minimizedContentHeight: CGFloat, outerExpansionFraction: CGFloat, outerExpansionDirection: Bool, + availableReactions: StoryAvailableReactions?, close: @escaping () -> Void, expandViewStats: @escaping () -> Void, deleteAction: @escaping () -> Void, @@ -108,6 +110,7 @@ final class StoryItemSetViewListComponent: Component { self.minimizedContentHeight = minimizedContentHeight self.outerExpansionFraction = outerExpansionFraction self.outerExpansionDirection = outerExpansionDirection + self.availableReactions = availableReactions self.close = close self.expandViewStats = expandViewStats self.deleteAction = deleteAction @@ -143,6 +146,9 @@ final class StoryItemSetViewListComponent: Component { if lhs.outerExpansionDirection != rhs.outerExpansionDirection { return false } + if lhs.availableReactions !== rhs.availableReactions { + return false + } return true } @@ -517,7 +523,29 @@ final class StoryItemSetViewListComponent: Component { subtitle: dateText, subtitleAccessory: .checks, presence: nil, - displayLike: item.isLike, + reaction: item.reaction.flatMap { reaction -> PeerListItemComponent.Reaction in + var animationFileId: Int64? + var animationFile: TelegramMediaFile? + switch reaction { + case .builtin: + if let availableReactions = component.availableReactions { + for availableReaction in availableReactions.reactionItems { + if availableReaction.reaction.rawValue == reaction { + animationFile = availableReaction.listAnimation + break + } + } + } + case let .custom(fileId): + animationFileId = fileId + animationFile = item.reactionFile + } + return PeerListItemComponent.Reaction( + reaction: reaction, + file: animationFile, + animationFileId: animationFileId + ) + }, selectionState: .none, hasNext: index != viewListState.totalCount - 1, action: { [weak self] peer in @@ -670,7 +698,7 @@ final class StoryItemSetViewListComponent: Component { applyState = true let _ = synchronous } else { - self.viewListState = EngineStoryViewListContext.State(totalCount: 0, items: [], loadMoreToken: nil) + self.viewListState = EngineStoryViewListContext.State(totalCount: 0, totalReactedCount: 0, items: [], loadMoreToken: nil) } } @@ -728,7 +756,7 @@ final class StoryItemSetViewListComponent: Component { var externalViews: EngineStoryItem.Views? = component.storyItem.views if let viewListState = self.viewListState, !viewListState.items.isEmpty { - externalViews = EngineStoryItem.Views(seenCount: viewListState.totalCount, seenPeers: viewListState.items.prefix(3).map(\.peer)) + externalViews = EngineStoryItem.Views(seenCount: viewListState.totalCount, reactedCount: viewListState.totalReactedCount, seenPeers: viewListState.items.prefix(3).map(\.peer)) } let navigationPanelSize = self.navigationPanel.update( diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 7cbb8342c8..8e6202de9f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -76,6 +76,9 @@ public final class StoryFooterPanelComponent: Component { private let viewStatsExpandedText: AnimatedCountLabelView private let deleteButton = ComponentView() + private var reactionStatsIcon: UIImageView? + private var reactionStatsText: AnimatedCountLabelView? + private var statusButton: HighlightableButton? private var statusNode: SemanticStatusNode? private var uploadingText: ComponentView? @@ -114,9 +117,13 @@ public final class StoryFooterPanelComponent: Component { if highlighted { self.avatarsView.alpha = 0.7 self.viewStatsText.alpha = 0.7 + self.reactionStatsIcon?.alpha = 0.7 + self.reactionStatsText?.alpha = 0.7 } else { self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.viewStatsText.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) + self.reactionStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) + self.reactionStatsText?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) } } self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside) @@ -278,8 +285,10 @@ public final class StoryFooterPanelComponent: Component { } var viewCount = 0 + var reactionCount = 0 if let views = component.externalViews ?? component.storyItem?.views, views.seenCount != 0 { viewCount = views.seenCount + reactionCount = views.reactedCount } let viewsText: String @@ -353,7 +362,65 @@ public final class StoryFooterPanelComponent: Component { transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width) } - transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0))) + var statsButtonWidth = viewStatsTextFrame.maxY + 8.0 + + if reactionCount != 0 { + var reactionsTransition = transition + let reactionStatsIcon: UIImageView + if let current = self.reactionStatsIcon { + reactionStatsIcon = current + } else { + reactionsTransition = reactionsTransition.withAnimation(.none) + reactionStatsIcon = UIImageView() + reactionStatsIcon.image = UIImage(bundleImageName: "Stories/InputLikeOn")?.withRenderingMode(.alwaysTemplate) + reactionStatsIcon.tintColor = UIColor(rgb: 0xFF3B30) + + self.reactionStatsIcon = reactionStatsIcon + self.externalContainerView.addSubview(reactionStatsIcon) + } + + let reactionStatsText: AnimatedCountLabelView + if let current = self.reactionStatsText { + reactionStatsText = current + } else { + reactionStatsText = AnimatedCountLabelView(frame: CGRect()) + reactionStatsText.isUserInteractionEnabled = false + self.reactionStatsText = reactionStatsText + self.externalContainerView.addSubview(reactionStatsText) + } + + let reactionStatsLayout = reactionStatsText.update( + size: CGSize(width: availableSize.width, height: size.height), + segments: [ + .number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.regular(15.0), textColor: .white)) + ], + transition: (isFirstTime || reactionsTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) + ) + + let imageSize = CGSize(width: 23.0, height: 23.0) + reactionsTransition.setFrame(view: reactionStatsIcon, frame: CGRect(origin: CGPoint(x: viewStatsTextFrame.maxX + 7.0, y: viewStatsTextFrame.minY - 3.0), size: imageSize)) + + let reactionStatsFrame = CGRect(origin: CGPoint(x: viewStatsTextFrame.maxX + 7.0 + imageSize.width + 3.0, y: viewStatsTextFrame.minY), size: reactionStatsLayout.size) + reactionsTransition.setFrame(view: reactionStatsText, frame: reactionStatsFrame) + + statsButtonWidth = reactionStatsFrame.maxX + 8.0 + } else { + if let reactionStatsIcon = self.reactionStatsIcon { + self.reactionStatsIcon = nil + reactionStatsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsIcon] _ in + reactionStatsIcon?.removeFromSuperview() + }) + } + + if let reactionStatsText = self.reactionStatsText { + self.reactionStatsText = nil + reactionStatsText.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsText] _ in + reactionStatsText?.removeFromSuperview() + }) + } + } + + transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: statsButtonWidth, height: baseHeight))) self.viewStatsButton.isUserInteractionEnabled = component.expandFraction == 0.0 var rightContentOffset: CGFloat = availableSize.width - 12.0 From 85f13c349612340c1498e47474a1e29d1d9fdf8a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 4 Aug 2023 23:19:40 +0300 Subject: [PATCH 12/12] Story reactions --- .../ReactionContextBackgroundNode.swift | 6 +- .../Sources/ReactionContextNode.swift | 22 ++- .../TelegramNotices/Sources/Notices.swift | 26 ++++ .../MessageInputActionButtonComponent.swift | 35 +++-- .../Sources/StoryContainerScreen.swift | 28 ++++ .../StoryItemSetContainerComponent.swift | 125 ++++++++++++++++-- 6 files changed, 214 insertions(+), 28 deletions(-) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index 8243efd2bb..8dcdf78175 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -120,6 +120,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { isMinimized: Bool, isCoveredByInput: Bool, displayTail: Bool, + forceTailToRight: Bool, transition: ContainedViewLayoutTransition ) { let shadowInset: CGFloat = 15.0 @@ -171,7 +172,10 @@ final class ReactionContextBackgroundNode: ASDisplayNode { let largeCircleFrame: CGRect let smallCircleFrame: CGRect - if isLeftAligned { + if forceTailToRight { + largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: size.height - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) + smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) + } else if isLeftAligned { largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: size.height - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) } else { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index b2324d045d..9b1b05b968 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -233,6 +233,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var animationHideNode: Bool = false public var displayTail: Bool = true + public var forceTailToRight: Bool = true private var didAnimateIn: Bool = false public private(set) var isAnimatingOut: Bool = false @@ -636,12 +637,20 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { contentSize.width = max(46.0, contentSize.width) contentSize.height = self.currentContentHeight - let sideInset: CGFloat = 11.0 + insets.left + let sideInset: CGFloat + if self.forceTailToRight { + sideInset = insets.left + } else { + sideInset = 11.0 + insets.left + } let backgroundOffset: CGPoint = CGPoint(x: 22.0, y: -7.0) var rect: CGRect let isLeftAligned: Bool - if anchorRect.minX < containerSize.width - anchorRect.maxX { + if self.forceTailToRight { + rect = CGRect(origin: CGPoint(x: anchorRect.minX - backgroundOffset.x - 4.0, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize) + isLeftAligned = false + } else if anchorRect.minX < containerSize.width - anchorRect.maxX { rect = CGRect(origin: CGPoint(x: anchorRect.maxX - contentSize.width + backgroundOffset.x, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize) isLeftAligned = true } else { @@ -665,7 +674,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } let cloudSourcePoint: CGFloat - if isLeftAligned { + if self.forceTailToRight { + cloudSourcePoint = min(rect.maxX - 46.0 / 2.0, anchorRect.maxX - 4.0) + } else if isLeftAligned { cloudSourcePoint = min(rect.maxX - 46.0 / 2.0, anchorRect.maxX - 4.0) } else { cloudSourcePoint = max(rect.minX + 46.0 / 2.0, anchorRect.minX) @@ -1190,6 +1201,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isMinimized: self.highlightedReaction != nil && !self.highlightedByHover, isCoveredByInput: isCoveredByInput, displayTail: self.displayTail, + forceTailToRight: self.forceTailToRight, transition: transition ) @@ -1779,10 +1791,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { self.isAnimatingOutToReaction = true - #if DEBUG - let hideNode = true - #endif - var foundItemNode: ReactionNode? for (_, itemNode) in self.visibleItemNodes { if let itemNode = itemNode as? ReactionNode, itemNode.item.reaction.rawValue == value { diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 4a914141bc..360ddd9f2c 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -177,6 +177,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case storiesCameraTooltip = 43 case storiesDualCameraTooltip = 44 case displayChatListArchiveTooltip = 45 + case displayStoryReactionTooltip = 46 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -414,6 +415,10 @@ private struct ApplicationSpecificNoticeKeys { static func displayChatListArchiveTooltip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayChatListArchiveTooltip.key) } + + static func displayStoryReactionTooltip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayStoryReactionTooltip.key) + } } public struct ApplicationSpecificNotice { @@ -1546,6 +1551,27 @@ public struct ApplicationSpecificNotice { |> take(1) } + public static func setDisplayStoryReactionTooltip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { + transaction.setNotice(ApplicationSpecificNoticeKeys.displayStoryReactionTooltip(), entry) + } + } + |> ignoreValues + } + + public static func displayStoryReactionTooltip(accountManager: AccountManager) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.displayStoryReactionTooltip()) + |> map { view -> Bool in + if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { + return true + } else { + return false + } + } + |> take(1) + } + public static func setDisplayChatListArchiveTooltip(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift index 5a4b1a7e2b..36ea09ad07 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift @@ -21,12 +21,8 @@ private extension MessageInputActionButtonComponent.Mode { return "Chat/Input/Text/IconAttachment" case .forward: return "Chat/Input/Text/IconForwardSend" - case let .like(reaction, _, _): - if reaction == nil { - return "Stories/InputLikeOff" - } else { - return nil - } + case .like: + return "Stories/InputLikeOff" default: return nil } @@ -220,6 +216,11 @@ public final class MessageInputActionButtonComponent: Component { let themeUpdated = previousComponent?.theme !== component.theme + var transition = transition + if transition.animation.isImmediate, let previousComponent, case .like = previousComponent.mode, case .like = component.mode, previousComponent.mode != component.mode { + transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + } + self.containerNode.isUserInteractionEnabled = component.longPressAction != nil if self.micButton == nil { @@ -319,8 +320,14 @@ public final class MessageInputActionButtonComponent: Component { switch component.mode { case .none: break - case .send, .apply, .attach, .delete, .forward, .like: + case .send, .apply, .attach, .delete, .forward: sendAlpha = 1.0 + case let .like(reaction, _, _): + if reaction != nil { + sendAlpha = 0.0 + } else { + sendAlpha = 1.0 + } case .more: moreAlpha = 1.0 case .videoInput, .voiceInput: @@ -333,8 +340,6 @@ public final class MessageInputActionButtonComponent: Component { if let iconName = component.mode.iconName { let tintColor: UIColor = .white self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: tintColor) - } else if case let .like(reaction, _, _) = component.mode, reaction != nil { - self.sendIconView.image = nil } else if case .apply = component.mode { self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -392,7 +397,7 @@ public final class MessageInputActionButtonComponent: Component { } if case let .like(reactionValue, reactionFile, animationFileId) = component.mode, let reaction = reactionValue { - let reactionIconFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 3.0, dy: 3.0) + let reactionIconFrame = CGRect(origin: .zero, size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: 2.0, dy: 2.0) let reactionIconView: ReactionIconView if let current = self.reactionIconView { @@ -402,6 +407,11 @@ public final class MessageInputActionButtonComponent: Component { reactionIconView.isUserInteractionEnabled = false self.reactionIconView = reactionIconView self.addSubview(reactionIconView) + + if previousComponent != nil { + reactionIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + reactionIconView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) + } } transition.setFrame(view: reactionIconView, frame: reactionIconFrame) reactionIconView.update( @@ -418,7 +428,10 @@ public final class MessageInputActionButtonComponent: Component { ) } else if let reactionIconView = self.reactionIconView { self.reactionIconView = nil - reactionIconView.removeFromSuperview() + reactionIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak reactionIconView] _ in + reactionIconView?.removeFromSuperview() + }) + reactionIconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) } transition.setFrame(view: self.button.view, frame: CGRect(origin: .zero, size: availableSize)) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index d825a85f43..34aeb9a3f1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -20,6 +20,7 @@ import VolumeButtons import TooltipUI import ChatEntityKeyboardInputNode import notify +import TelegramNotices func hasFirstResponder(_ view: UIView) -> Bool { if view.isFirstResponder { @@ -382,6 +383,8 @@ private final class StoryContainerScreenComponent: Component { private var pendingNavigationToItemId: (peerId: EnginePeer.Id, id: Int32)? + private var didDisplayReactionTooltip: Bool = false + override init(frame: CGRect) { self.backgroundLayer = SimpleLayer() self.backgroundLayer.backgroundColor = UIColor.black.cgColor @@ -911,6 +914,30 @@ private final class StoryContainerScreenComponent: Component { self?.layer.allowsGroupOpacity = false }) } + + Queue.mainQueue().after(0.4, { [weak self] in + guard let self, let component = self.component else { + return + } + + let _ = (ApplicationSpecificNotice.displayStoryReactionTooltip(accountManager: component.context.sharedContext.accountManager) + |> delay(1.0, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + if !value { + if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { + currentItemView.maybeDisplayReactionTooltip() + } + } + + self.didDisplayReactionTooltip = true + #if !DEBUG + let _ = ApplicationSpecificNotice.setDisplayStoryReactionTooltip(accountManager: component.context.sharedContext.accountManager).start() + #endif + }) + }) } func animateOut(completion: @escaping () -> Void) { @@ -1094,6 +1121,7 @@ private final class StoryContainerScreenComponent: Component { } } }) + update = true } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index ae12847873..e6b584db75 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -950,10 +950,21 @@ public final class StoryItemSetContainerComponent: Component { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + + if self.displayLikeReactions, let reactionContextNode = self.reactionContextNode { + if let result, result.isDescendant(of: reactionContextNode.view) { + return result + } else { + return self.itemsContainerView + } + } + if let inputView = self.inputPanel.view, let inputViewHitTest = inputView.hitTest(self.convert(point, to: inputView), with: event) { return inputViewHitTest } - guard let result = super.hitTest(point, with: event) else { + + guard let result else { return nil } @@ -1823,6 +1834,67 @@ public final class StoryItemSetContainerComponent: Component { }) } + func maybeDisplayReactionTooltip() { + if "".isEmpty { + return + } + guard let component = self.component else { + return + } + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView else { + return + } + if inputPanelView.isHidden || inputPanelView.alpha == 0.0 { + return + } + if !likeButtonView.isDescendant(of: self) { + return + } + + let rect = likeButtonView.convert(likeButtonView.bounds, to: nil) + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + let text = "Long tap for more reactions" + let controller = TooltipController(content: .text(text), baseFontSize: presentationData.listsFontSize.baseDisplaySize, padding: 2.0) + controller.dismissed = { [weak self] _ in + if let self { + self.voiceMessagesRestrictedTooltipController = nil + self.updateIsProgressPaused() + } + } + component.presentController(controller, TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in + if let self { + return (self, rect) + } + return nil + })) + self.voiceMessagesRestrictedTooltipController = controller + self.updateIsProgressPaused() + + //TODO:localize + /*let tooltipScreen = TooltipScreen( + account: component.context.account, + sharedContext: component.context.sharedContext, + text: .markdown(text: "Long tap for more reactions"), + balancedTextLayout: true, + style: .default, + location: TooltipScreen.Location.point(likeButtonView.convert(likeButtonView.bounds, to: nil).offsetBy(dx: 0.0, dy: 0.0), .bottom), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: true) + } + ) + tooltipScreen.willBecomeDismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.tooltipScreen = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.tooltipScreen?.dismiss() + self.sendMessageContext.tooltipScreen = tooltipScreen + self.updateIsProgressPaused() + component.controller()?.present(tooltipScreen, in: .current)*/ + } + func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let isFirstTime = self.component == nil @@ -3260,8 +3332,9 @@ public final class StoryItemSetContainerComponent: Component { let reactionsAnchorRect: CGRect if self.displayLikeReactions, let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView { var likeRect = likeButtonView.convert(likeButtonView.bounds, to: self) - likeRect.origin.y -= 14.0 - likeRect.size.height += 14.0 + likeRect.origin.y -= 15.0 + likeRect.size.height += 15.0 + likeRect.origin.x -= 30.0 reactionsAnchorRect = likeRect } else { reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) @@ -3306,7 +3379,7 @@ public final class StoryItemSetContainerComponent: Component { animationCache: component.context.animationCache, presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme), items: reactionItems.map(ReactionContextItem.reaction), - selectedItems: Set(), + selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(), getEmojiContent: { [weak self] animationCache, animationRenderer in guard let self, let component = self.component else { preconditionFailure() @@ -3353,6 +3426,7 @@ public final class StoryItemSetContainerComponent: Component { } ) reactionContextNode.displayTail = self.displayLikeReactions + reactionContextNode.forceTailToRight = self.displayLikeReactions self.reactionContextNode = reactionContextNode reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in @@ -3360,12 +3434,45 @@ public final class StoryItemSetContainerComponent: Component { return } - if component.slice.item.storyItem.myReaction == updateReaction.reaction { - let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).start() - self.displayLikeReactions = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + if self.displayLikeReactions { + if component.slice.item.storyItem.myReaction == updateReaction.reaction { + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).start() + self.displayLikeReactions = false + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + } else { + self.waitingForReactionAnimateOutToLike = updateReaction.reaction + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start() + } } else { - self.waitingForReactionAnimateOutToLike = updateReaction.reaction + let targetView = UIView(frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - 100.0) * 0.5), y: floor((self.bounds.height - 100.0) * 0.5)), size: CGSize(width: 100.0, height: 100.0))) + targetView.isUserInteractionEnabled = false + self.addSubview(targetView) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction) + reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + standaloneReactionAnimation.frame = self.bounds + self.addSubview(standaloneReactionAnimation.view) + }, completion: { [weak targetView, weak reactionContextNode] in + targetView?.removeFromSuperview() + if let reactionContextNode { + reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false) + reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in + reactionContextNode?.view.removeFromSuperview() + }) + } + }) + } + + if hasFirstResponder(self) { + self.sendMessageContext.currentInputMode = .text + self.endEditing(true) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start() } }