diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 73f827f844..a2303f8781 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -2067,6 +2067,7 @@ "StickerPack.Share" = "Share"; "StickerPack.Send" = "Send Sticker"; "StickerPack.AddSticker" = "Add Sticker"; +"StickerPack.RemoveStickerSet" = "Remove Sticker Set"; "StickerPack.RemoveStickerCount_1" = "Remove 1 Sticker"; "StickerPack.RemoveStickerCount_2" = "Remove 2 Stickers"; @@ -16214,5 +16215,5 @@ Error: %8$@"; "Chat.AdminAction.ToastReactionsDeletedTitleSingle" = "Reaction Deleted"; "Chat.AdminAction.ToastReactionsDeletedTextSingle" = "Reaction Deleted."; -"Chat.AdminAction.ToastReactionsDeletedTextMultiple" = "Messages Deleted."; +"Chat.AdminAction.ToastReactionsDeletedTextMultiple" = "Reactions Deleted."; "Chat.AdminAction.ToastMessagesAndReactionsDeletedText" = "Messages and reactions deleted."; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 7b1781e32a..496e3c3b11 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1117,6 +1117,7 @@ public protocol ChatController: ViewController { func activateSearch(domain: ChatSearchDomain, query: String) func activateInput(type: ChatControllerActivateInput) func beginClearHistory(type: InteractiveHistoryClearingType) + func presentReactionDeletionOptions(author: Peer, messageId: MessageId) func performScrollToTop() -> Bool func transferScrollingVelocity(_ velocity: CGFloat) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 43de3828f5..2e76203112 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2578,6 +2578,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { case poll case todo case game + case voiceMessage } var messageTypeIcon: MessageTypeIcon? var ignoreForwardedIcon = false @@ -2895,7 +2896,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { messageTypeIcon = .story } else { for media in message.media { - if let _ = media as? TelegramMediaPoll { + if let file = media as? TelegramMediaFile { + if file.isVoice { + messageTypeIcon = .voiceMessage + } + } else if let _ = media as? TelegramMediaPoll { messageTypeIcon = .poll } else if let _ = media as? TelegramMediaTodo { messageTypeIcon = .todo @@ -3106,6 +3111,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { case .game: currentMessageTypeIcon = PresentationResourcesChatList.gameIcon(item.presentationData.theme) currentMessageTypeIconOffset.y = -1.0 + case .voiceMessage: + currentMessageTypeIcon = PresentationResourcesChatList.voiceMessageIcon(item.presentationData.theme) + currentMessageTypeIconOffset.y = -1.0 default: break } @@ -3115,7 +3123,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if !contentImageSpecs.isEmpty { textLeftCutout += forwardedIconSpacing } else { - textLeftCutout += contentImageTrailingSpace + textLeftCutout += contentImageTrailingSpace - 1.0 } } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 2ae5988bd0..c705171903 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -1591,3 +1591,22 @@ public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bo return false } } + +public func canSendReactionsToChat(_ state: ChatPresentationInterfaceState) -> Bool { + if let peer = state.renderedPeer?.peer { + let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: state) + return canSendReactionsToPeer(EnginePeer(peer), ignoreDefault: canBypassRestrictions) + } else if case .customChatContents = state.chatLocation { + if case let .customChatContents(contents) = state.subject { + if case .hashTagSearch = contents.kind { + return false + } else { + return true + } + } else { + return true + } + } else { + return false + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index 3d37e36ab1..5c3f072d76 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -94,7 +94,82 @@ func _internal_deleteAllReactionsWithAuthor(account: Account, peerId: PeerId, au func _internal_deleteReaction(account: Account, messageId: MessageId, authorId: PeerId) -> Signal { return account.postbox.transaction { transaction -> (Peer?, Peer?) in - return (transaction.getPeer(messageId.peerId), transaction.getPeer(authorId)) + let peer = transaction.getPeer(messageId.peerId) + let author = transaction.getPeer(authorId) + + if peer.flatMap(apiInputPeer) != nil && author.flatMap(apiInputPeer) != nil { + transaction.updateMessage(messageId, update: { currentMessage in + var attributes = currentMessage.attributes + var updated = false + + for i in 0 ..< attributes.count { + guard let attribute = attributes[i] as? ReactionsMessageAttribute else { + continue + } + + let removedRecentPeers = attribute.recentPeers.filter { $0.peerId == authorId } + var updatedTopPeers = attribute.topPeers + var removedStarsTopPeerCount: Int32 = 0 + for j in (0 ..< updatedTopPeers.count).reversed() { + if updatedTopPeers[j].peerId == authorId { + removedStarsTopPeerCount += updatedTopPeers[j].count + updatedTopPeers.remove(at: j) + } + } + + if removedRecentPeers.isEmpty && removedStarsTopPeerCount == 0 { + continue + } + + var updatedReactions = attribute.reactions + for removedRecentPeer in removedRecentPeers { + if let index = updatedReactions.firstIndex(where: { $0.value == removedRecentPeer.value }) { + if removedRecentPeer.value != .stars || removedStarsTopPeerCount == 0 { + updatedReactions[index].count -= 1 + } + if removedRecentPeer.isMy { + updatedReactions[index].chosenOrder = nil + } + } + } + if removedStarsTopPeerCount != 0, let index = updatedReactions.firstIndex(where: { $0.value == .stars }) { + updatedReactions[index].count -= removedStarsTopPeerCount + } + for j in (0 ..< updatedReactions.count).reversed() { + if updatedReactions[j].count <= 0 { + updatedReactions.remove(at: j) + } + } + + let updatedRecentPeers = attribute.recentPeers.filter { $0.peerId != authorId } + let updatedAttribute = ReactionsMessageAttribute(canViewList: attribute.canViewList, isTags: attribute.isTags, reactions: updatedReactions, recentPeers: updatedRecentPeers, topPeers: updatedTopPeers) + if updatedAttribute != attribute { + attributes[i] = updatedAttribute + updated = true + } + } + + if !updated { + return .skip + } + + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + + var tags = currentMessage.tags + if attributes.contains(where: { ($0 as? ReactionsMessageAttribute)?.hasUnseen == true }) { + tags.insert(.unseenReaction) + } else { + tags.remove(.unseenReaction) + } + + return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + + return (peer, author) } |> mapToSignal { peer, author in guard let inputPeer = peer.flatMap(apiInputPeer), let inputAuthor = author.flatMap(apiInputPeer) else { diff --git a/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift index 00e74c43ae..d1684d3f0b 100644 --- a/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift +++ b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift @@ -19,3 +19,24 @@ public func canSendMessagesToPeer(_ peer: EnginePeer, ignoreDefault: Bool = fals return channel.hasPermission(.sendSomething, ignoreDefault: ignoreDefault) } } + +public func canSendReactionsToPeer(_ peer: EnginePeer, ignoreDefault: Bool = false) -> Bool { + switch peer { + case .user: + return !peer.isDeleted + case let .legacyGroup(group): + switch group.role { + case .creator, .admin: + return !peer.isDeleted + case .member: + if let defaultBannedRights = group.defaultBannedRights, defaultBannedRights.flags.contains(.banSendReactions) && !ignoreDefault { + return false + } + return !peer.isDeleted + } + case let .secretChat(secretChat): + return secretChat.embeddedState == .active + case let .channel(channel): + return channel.hasBannedPermission(.banSendReactions, ignoreDefault: ignoreDefault) == nil + } +} diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index b1f6611745..ff32a5575d 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -138,6 +138,7 @@ public enum PresentationResourceKey: Int32 { case chatListCallOutgoingIcon case chatListCallVideoIncomingIcon case chatListCallVideoOutgoingIcon + case chatListVoiceMessageIcon case chatListGeneralTopicIcon case chatListGeneralTopicTemplateIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index 2092f8b75b..0994b46a48 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -349,6 +349,12 @@ public struct PresentationResourcesChatList { }) } + public static func voiceMessageIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListVoiceMessageIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/VoiceMessageIcon"), color: theme.chatList.muteIconColor) + }) + } + public static func verifiedIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in if let backgroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconForeground") { diff --git a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift index 0f549ecfeb..bb3f83e615 100644 --- a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift +++ b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift @@ -10,7 +10,7 @@ public enum AllowedReactions { case all } -public func peerMessageAllowedReactions(context: AccountContext, message: Message) -> Signal<(allowedReactions: AllowedReactions?, areStarsEnabled: Bool), NoError> { +public func peerMessageAllowedReactions(context: AccountContext, message: Message, ignoreDefault: Bool = false) -> Signal<(allowedReactions: AllowedReactions?, areStarsEnabled: Bool), NoError> { if message.id.peerId == context.account.peerId { return .single((.all, false)) } @@ -41,6 +41,10 @@ public func peerMessageAllowedReactions(context: AccountContext, message: Messag areStarsEnabled = value } + if let peer, !canSendReactionsToPeer(peer, ignoreDefault: ignoreDefault) { + return (nil, areStarsEnabled) + } + if let effectiveReactions = message.effectiveReactions(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)), effectiveReactions.count >= maxReactionCount { return (.set(Set(effectiveReactions.map(\.value))), areStarsEnabled) } @@ -256,7 +260,7 @@ public func tagMessageReactions(context: AccountContext, subPeerId: EnginePeer.I } } -public func topMessageReactions(context: AccountContext, message: Message, subPeerId: EnginePeer.Id?) -> Signal<[ReactionItem], NoError> { +public func topMessageReactions(context: AccountContext, message: Message, subPeerId: EnginePeer.Id?, ignoreDefault: Bool = false) -> Signal<[ReactionItem], NoError> { if message.id.peerId == context.account.peerId { var loadTags = false if let effectiveReactionsAttribute = message.effectiveReactionsAttribute(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)) { @@ -286,7 +290,7 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe } } - let allowedReactionsWithFiles: Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool)?, NoError> = peerMessageAllowedReactions(context: context, message: message) + let allowedReactionsWithFiles: Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool)?, NoError> = peerMessageAllowedReactions(context: context, message: message, ignoreDefault: ignoreDefault) |> mapToSignal { allowedReactions, areStarsEnabled -> Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool)?, NoError> in guard let allowedReactions = allowedReactions else { return .single(nil) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift index 51f85faf3c..b9255fdffd 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift @@ -220,7 +220,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight)) } - private func openSelectedPackMoreMenu() { + private func openMore() { guard let selectedStickerPack = self.selectedStickerPack, let controlsView = self.navigationButtons.view as? GlassControlPanelComponent.View, let rightItemView = controlsView.rightItemView, let sourceView = rightItemView.itemView(id: AnyHashable("more")) else { return } @@ -272,6 +272,59 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer }), nil) }))) + if selectedStickerPack.installed { + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strings.StickerPack_RemoveStickerSet, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + let info = selectedStickerPack.info + let context = self.context + let _ = (context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) + |> deliverOnMainQueue).start(next: { [weak self] indexAndItems in + guard let self, let (positionInList, items) = indexAndItems else { + return + } + + let stickerItems = items.compactMap { $0 as? StickerPackItem } + if let contentNode = self.contentNode as? StickerPaneSearchContentNode { + contentNode.setPackInstalledState(id: info.id, installed: false) + } + + var animateInAsReplacement = false + if let navigationController = self.interaction.getNavigationController() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme) + let undoController = UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: stickerItems.first, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { [weak self] action in + if case .undo = action { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: stickerItems, positionInList: positionInList).start() + if let contentNode = self?.contentNode as? StickerPaneSearchContentNode { + contentNode.setPackInstalledState(id: info.id, installed: true) + } + } + return true + }) + if let navigationController = self.interaction.getNavigationController() { + navigationController.presentOverlay(controller: undoController) + } else { + self.interaction.presentController(undoController, nil) + } + }) + }))) + } + let contextController = makeContextController( presentationData: presentationData, source: .reference(StickerPaneSearchHeaderContextReferenceContentSource(sourceView: sourceView)), @@ -327,7 +380,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer id: AnyHashable("more"), content: .animation("anim_morewide"), action: { [weak self] in - self?.openSelectedPackMoreMenu() + self?.openMore() } ) ], diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift index 0648188eac..bb5eeb9b62 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -82,6 +82,7 @@ private enum StickerSearchEntry: Identifiable, Comparable { struct StickerPaneSearchSelectedPack { let info: StickerPackCollectionInfo + let installed: Bool } private struct StickerPaneSearchPack: Equatable { @@ -497,19 +498,8 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { guard let strongSelf = self else { return } - - var animateInAsReplacement = false - if let navigationController = strongSelf.interaction.getNavigationController() { - for controller in navigationController.overlayControllers { - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitActionAndReplacementAnimation() - animateInAsReplacement = true - } - } - } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: theme) - strongSelf.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in + strongSelf.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, action: { _ in return true })) })) @@ -688,7 +678,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { strongSelf.currentSearchEntries = entries strongSelf.currentPacks = packItems if let selectedPack = strongSelf.selectedPack, let updatedPack = packItems.first(where: { $0.info.id == selectedPack.info.id }), selectedPack.installed != updatedPack.installed { - strongSelf.selectedPack = StickerPaneSearchPack(info: selectedPack.info, topItems: selectedPack.topItems, installed: updatedPack.installed) + strongSelf.setPackInstalledState(id: selectedPack.info.id, installed: updatedPack.installed, transition: .immediate) } strongSelf.currentSearchIsFinal = final strongSelf.searchIsActive = true @@ -796,20 +786,41 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { return !selectedPack.installed && !self.installedPackIds.contains(selectedPack.info.id) } - private func markPackInstalled(_ id: ItemCollectionId) { - self.installedPackIds.insert(id) - - if let selectedPack = self.selectedPack, selectedPack.info.id == id, !selectedPack.installed { - self.selectedPack = StickerPaneSearchPack(info: selectedPack.info, topItems: selectedPack.topItems, installed: true) + func setPackInstalledState(id: ItemCollectionId, installed: Bool, transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)) { + if installed { + self.installedPackIds.insert(id) + } else { + self.installedPackIds.remove(id) } + var updatedSelectedPack: StickerPaneSearchPack? + if let selectedPack = self.selectedPack, selectedPack.info.id == id { + if selectedPack.installed != installed { + let pack = StickerPaneSearchPack(info: selectedPack.info, topItems: selectedPack.topItems, installed: installed) + self.selectedPack = pack + updatedSelectedPack = pack + } else { + updatedSelectedPack = selectedPack + } + } + + var updatedPacks = false self.currentPacks = self.currentPacks.map { pack in - if pack.info.id == id && !pack.installed { - return StickerPaneSearchPack(info: pack.info, topItems: pack.topItems, installed: true) + if pack.info.id == id && pack.installed != installed { + updatedPacks = true + return StickerPaneSearchPack(info: pack.info, topItems: pack.topItems, installed: installed) } else { return pack } } + + if let updatedSelectedPack { + self.selectedPackUpdated?(StickerPaneSearchSelectedPack(info: updatedSelectedPack.info, installed: updatedSelectedPack.installed)) + } + + if updatedSelectedPack != nil || updatedPacks { + self.requestLayout(transition: transition) + } } private func updatePackPanelExpansionFromScroll() { @@ -865,7 +876,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { self.notFoundNode.isHidden = true self.gridNode.isHidden = false self.trendingPane.isHidden = true - self.selectedPackUpdated?(StickerPaneSearchSelectedPack(info: pack.info)) + self.selectedPackUpdated?(StickerPaneSearchSelectedPack(info: pack.info, installed: pack.installed)) self.enqueueEntries(self.entries(packItems: pack.topItems), interaction: interaction, crossfade: true) self.isPackPanelExpanded = true @@ -896,8 +907,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let packId = selectedPack.info.id let accessHash = selectedPack.info.accessHash - self.markPackInstalled(packId) - self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) + self.setPackInstalledState(id: packId, installed: true) let installSignal = (context.engine.stickers.loadedStickerPack(reference: .id(id: packId.id, accessHash: accessHash), forceActualized: false) |> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in @@ -931,19 +941,8 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { guard let self else { return } - - var animateInAsReplacement = false - if let navigationController = self.interaction.getNavigationController() { - for controller in navigationController.overlayControllers { - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitActionAndReplacementAnimation() - animateInAsReplacement = true - } - } - } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme) - self.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: self.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in + self.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: self.context), elevatedLayout: false, action: { _ in return true })) })) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift index 0f2d18d73f..1798db6f7b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift @@ -23,6 +23,7 @@ final class PeerInfoInteraction { let openAddContact: () -> Void let updateBlocked: (Bool) -> Void let openReport: (PeerInfoReportType) -> Void + let openDeleteReaction: (MessageId) -> Void let openShareBot: () -> Void let openAddBotToGroup: () -> Void let performBotCommand: (PeerInfoBotCommand) -> Void @@ -101,6 +102,7 @@ final class PeerInfoInteraction { openAddContact: @escaping () -> Void, updateBlocked: @escaping (Bool) -> Void, openReport: @escaping (PeerInfoReportType) -> Void, + openDeleteReaction: @escaping (MessageId) -> Void, openShareBot: @escaping () -> Void, openAddBotToGroup: @escaping () -> Void, performBotCommand: @escaping (PeerInfoBotCommand) -> Void, @@ -178,6 +180,7 @@ final class PeerInfoInteraction { self.openAddContact = openAddContact self.updateBlocked = updateBlocked self.openReport = openReport + self.openDeleteReaction = openDeleteReaction self.openShareBot = openShareBot self.openAddBotToGroup = openAddBotToGroup self.performBotCommand = performBotCommand diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift index eeb1b21f0f..799802ed35 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift @@ -42,6 +42,7 @@ func infoItems( presentationData: PresentationData, interaction: PeerInfoInteraction, reactionSourceMessageId: MessageId?, + canDeleteReaction: Bool, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool, @@ -91,9 +92,9 @@ func infoItems( let ItemAffiliateInfo = 4001 let ItemBusinessHours = 5000 let ItemLocation = 5001 - let ItemSendMessage = 6000 - let ItemReport = 6001 - let ItemAddToContacts = 6002 + let ItemAddToContacts = 6000 + let ItemDeleteReaction = 6001 + let ItemReport = 6002 let ItemBlock = 6003 let ItemEncryptionKey = 6004 let ItemBalanceHeader = 7000 @@ -369,23 +370,23 @@ func infoItems( } if !isMyProfile { - if let reactionSourceMessageId = reactionSourceMessageId, !data.isContact { - items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemSendMessage, text: presentationData.strings.UserInfo_SendMessage, action: { - interaction.openChat(nil) + if !data.isContact, user.botInfo == nil { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemAddToContacts, text: presentationData.strings.PeerInfo_AddToContacts, action: { + interaction.openAddContact() })) - + } + + if let reactionSourceMessageId = reactionSourceMessageId { + if canDeleteReaction { + //TODO:localize + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemDeleteReaction, text: "Delete Reaction", color: .destructive, action: { + interaction.openDeleteReaction(reactionSourceMessageId) + })) + } items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemReport, text: presentationData.strings.ReportPeer_BanAndReport, color: .destructive, action: { interaction.openReport(.reaction(reactionSourceMessageId)) })) } else { - if !data.isContact { - if user.botInfo == nil { - items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemAddToContacts, text: presentationData.strings.PeerInfo_AddToContacts, action: { - interaction.openAddContact() - })) - } - } - var isBlocked = false if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked { isBlocked = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index bb01e62bc9..d388044837 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -283,6 +283,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var forceIsContactPromise = ValuePromise(false) let reactionSourceMessageId: MessageId? let sourceMessageId: MessageId? + var canDeleteReaction = false var dataDisposable: Disposable? let activeActionDisposable = MetaDisposable() @@ -460,6 +461,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro openReport: { [weak self] type in self?.openReport(type: type, contextController: nil, backAction: nil) }, + openDeleteReaction: { [weak self] messageId in + self?.openDeleteReaction(messageId: messageId) + }, openShareBot: { [weak self] in self?.openShareBot() }, @@ -2485,14 +2489,37 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.updateNavigation(transition: .immediate, additive: true, animateHeader: true) } + let reactionSourceMessage: Signal + if let reactionSourceMessageId = self.reactionSourceMessageId { + reactionSourceMessage = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: reactionSourceMessageId)) + } else { + reactionSourceMessage = .single(nil) + } + self.dataDisposable = combineLatest( queue: Queue.mainQueue(), screenData, - self.forceIsContactPromise.get() - ).startStrict(next: { [weak self] data, forceIsContact in + self.forceIsContactPromise.get(), + reactionSourceMessage + ).startStrict(next: { [weak self] data, forceIsContact, reactionSourceMessage in guard let strongSelf = self else { return } + + var canDeleteReaction = false + if let sourceMessage = reactionSourceMessage?._asMessage(), let reactionPeerId = data.peer?.id, let channel = sourceMessage.peers[sourceMessage.id.peerId] as? TelegramChannel, channel.hasPermission(.deleteAllMessages) { + for attribute in sourceMessage.attributes { + guard let attribute = attribute as? ReactionsMessageAttribute else { + continue + } + if attribute.recentPeers.contains(where: { $0.peerId == reactionPeerId }) || attribute.topPeers.contains(where: { $0.peerId == reactionPeerId }) { + canDeleteReaction = true + break + } + } + } + strongSelf.canDeleteReaction = canDeleteReaction + if data.isContact && forceIsContact { strongSelf.forceIsContactPromise.set(false) } else { @@ -5414,7 +5441,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro insets.left += sectionInset insets.right += sectionInset - let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile) + let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, reactionSourceMessageId: self.reactionSourceMessageId, canDeleteReaction: self.canDeleteReaction, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile) contentHeight += headerHeight if !((self.isSettings || self.isMyProfile) && self.state.isEditing) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift index b9fba293f5..35f61fa9ed 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift @@ -81,6 +81,50 @@ extension PeerInfoScreenNode { } } + func openDeleteReaction(messageId: MessageId) { + guard let authorPeer = self.data?.peer?._asPeer(), let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId), + TelegramEngine.EngineData.Item.Messages.Message(id: messageId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] sourcePeer, sourceMessage in + guard let self, let sourcePeer, let sourceMessage = sourceMessage?._asMessage() else { + return + } + guard let channel = sourceMessage.peers[sourceMessage.id.peerId] as? TelegramChannel, channel.hasPermission(.deleteAllMessages) else { + return + } + + var hasReaction = false + for attribute in sourceMessage.attributes { + guard let attribute = attribute as? ReactionsMessageAttribute else { + continue + } + if attribute.recentPeers.contains(where: { $0.peerId == authorPeer.id }) || attribute.topPeers.contains(where: { $0.peerId == authorPeer.id }) { + hasReaction = true + break + } + } + guard hasReaction else { + return + } + + let chatLocation: NavigateToChatControllerParams.Location + if case let .channel(channel) = sourcePeer, channel.isForumOrMonoForum, let threadId = sourceMessage.threadId { + chatLocation = .replyThread(ChatReplyThreadMessage(peerId: sourcePeer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, isMonoforumPost: channel.isMonoForum, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + } else { + chatLocation = .peer(sourcePeer) + } + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true, completion: { chatController in + chatController.presentReactionDeletionOptions(author: authorPeer, messageId: messageId) + })) + }) + } + func openChatWithClearedHistory(type: InteractiveHistoryClearingType) { guard let peer = self.data?.chatPeer, let navigationController = self.controller?.navigationController as? NavigationController else { return diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift index b2e43e5e9c..3bc0da8950 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift @@ -496,19 +496,18 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte } } - if "".isEmpty { - //TODO:localize - let automationBotTitle: String - if let botPeer = data.businessConnectedBot { - automationBotTitle = "@\(botPeer.compactDisplayTitle)" - } else { - automationBotTitle = "Off" - } - items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerChatAutomation, label: .text(automationBotTitle), text: "Chat Automation", icon: PresentationResourcesSettings.aiTools, action: { - interaction.editingOpenBusinessChatBots() - })) - items[.info]!.append(PeerInfoScreenCommentItem(id: ItemPeerChatAutomationHelp, text: "Add a bot to reply to messages on your behalf.")) + //TODO:localize + let automationBotTitle: String + if let botPeer = data.businessConnectedBot { + let _ = botPeer + automationBotTitle = "@\(botPeer.compactDisplayTitle)" + } else { + automationBotTitle = "Off" } + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerChatAutomation, label: .text(automationBotTitle), additionalBadgeLabel: nil, text: "Chat Automation", icon: PresentationResourcesSettings.aiTools, action: { + interaction.editingOpenBusinessChatBots() + })) + items[.info]!.append(PeerInfoScreenCommentItem(id: ItemPeerChatAutomationHelp, text: "Add a bot to reply to messages on your behalf.")) items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: { interaction.openSettings(.addAccount) diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index c936e8cdec..1da0fd8d81 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -677,7 +677,11 @@ final class ChatbotSetupScreenComponent: Component { } transition.setPosition(view: self.titleTransformContainer, position: navigationTitleFrame.center) transition.setBounds(view: self.titleTransformContainer, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size)) - transition.setFrame(view: navigationTitleView, frame: CGRect(origin: CGPoint(), size: navigationTitleFrame.size)) + transition.setBounds(view: navigationTitleView, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size)) + transition.setPosition(view: navigationTitleView, position: CGPoint( + x: navigationTitleFrame.size.width * 0.5, + y: navigationTitleFrame.size.height * 0.5 + )) } let bottomContentInset: CGFloat = 24.0 diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/Contents.json new file mode 100644 index 0000000000..75aa24b443 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "voice_message_20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/voice_message_20.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/voice_message_20.pdf new file mode 100644 index 0000000000..4d0f9fb5dc Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/voice_message_20.pdf differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 0080cb5043..a1201fb48a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -18,6 +18,7 @@ import TooltipUI import TopMessageReactions import TelegramNotices import PresentationDataUtils +import ChatPresentationInterfaceState extension ChatControllerImpl { func openMessageContextMenu(message: Message, selectAll: Bool, node: ASDisplayNode, frame: CGRect, anyRecognizer: UIGestureRecognizer?, location: CGPoint?) -> Void { @@ -45,13 +46,15 @@ extension ChatControllerImpl { guard let topMessage = messages.first else { return } - + + let canBypassReactionRestrictions = canBypassRestrictions(chatPresentationInterfaceState: self.presentationInterfaceState) + let _ = combineLatest(queue: .mainQueue(), self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: updatedMessages, controllerInteraction: self.controllerInteraction, selectAll: selectAll, interfaceInteraction: self.interfaceInteraction, messageNode: node as? ChatMessageItemView), - peerMessageAllowedReactions(context: self.context, message: topMessage), + peerMessageAllowedReactions(context: self.context, message: topMessage, ignoreDefault: canBypassReactionRestrictions), peerMessageSelectedReactions(context: self.context, message: topMessage), - topMessageReactions(context: self.context, message: topMessage, subPeerId: self.chatLocation.threadId.flatMap(EnginePeer.Id.init)), + topMessageReactions(context: self.context, message: topMessage, subPeerId: self.chatLocation.threadId.flatMap(EnginePeer.Id.init), ignoreDefault: canBypassReactionRestrictions), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: self.context.sharedContext.accountManager) ).startStandalone(next: { [weak self] peer, actions, allowedReactionsAndStars, selectedReactions, topReactions, chatTextSelectionTips in guard let self else { @@ -370,6 +373,11 @@ extension ChatControllerImpl { controller?.view.endEditing(true) if case .stars = chosenUpdatedReaction.reaction { + if !canSendReactionsToChat(self.presentationInterfaceState) { + controller?.dismiss(completion: {}) + return + } + if isLarge { if let controller { controller.dismiss(completion: { [weak self] in @@ -505,6 +513,11 @@ extension ChatControllerImpl { isFirst = !currentReactions.contains(where: { $0.value == chosenReaction }) } + if removedReaction == nil && !canSendReactionsToChat(self.presentationInterfaceState) { + controller?.dismiss(completion: {}) + return + } + if message.areReactionsTags(accountPeerId: self.context.account.peerId) { if removedReaction == nil, !topReactions.contains(where: { $0.reaction.rawValue == chosenReaction }) { if !self.presentationInterfaceState.isPremium { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5b62ef7ea5..4a1342c6d8 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1679,6 +1679,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .default = reaction, strongSelf.chatLocation.peerId == strongSelf.context.account.peerId { return } + if case .default = reaction, !canSendReactionsToChat(strongSelf.presentationInterfaceState) { + return + } if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject { if case let .hashTagSearch(publicPosts) = customChatContents.kind, publicPosts { return @@ -1747,7 +1750,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let _ = (peerMessageAllowedReactions(context: strongSelf.context, message: message) + let _ = (peerMessageAllowedReactions(context: strongSelf.context, message: message, ignoreDefault: canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState)) |> deliverOnMainQueue).startStandalone(next: { allowedReactions, _ in guard let strongSelf = self else { return @@ -1791,6 +1794,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if case .stars = chosenReaction { + if !canSendReactionsToChat(strongSelf.presentationInterfaceState) { + return + } + if strongSelf.selectPollOptionFeedback == nil { strongSelf.selectPollOptionFeedback = HapticFeedback() } @@ -1957,6 +1964,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if removedReaction == nil { + if !canSendReactionsToChat(strongSelf.presentationInterfaceState) { + return + } + if !canAddMessageReactions(message: message) { itemNode.openMessageContextMenu() return diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index c6fc676eb7..8042d75db9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -261,6 +261,30 @@ extension ChatControllerImpl { })) } + public func presentReactionDeletionOptions(author: Peer, messageId: MessageId) { + guard self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel else { + return + } + let _ = (self.context.sharedContext.chatAvailableMessageActions( + engine: self.context.engine, + accountPeerId: self.context.account.peerId, + messageIds: Set([messageId]), + keepUpdated: false + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] actions in + guard let self, !actions.options.isEmpty else { + return + } + self.presentBanMessageOptions( + accountPeerId: self.context.account.peerId, + author: author, + messageIds: Set([messageId]), + options: actions.options, + reaction: true + ) + }) + } + func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions, reaction: Bool = false) { guard let peerId = self.chatLocation.peerId else { return diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 3f53bb2c28..96ce489741 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -188,33 +188,21 @@ extension ChatControllerImpl { } var dismissController: ((@escaping () -> Void) -> Void)? - let canDeleteReactions = canDeleteOtherUserMessages(message: message) + let canDeleteReactions: Bool + if let channel = message.peers[message.id.peerId] as? TelegramChannel { + canDeleteReactions = channel.hasPermission(.deleteAllMessages) + } else { + canDeleteReactions = false + } let canViewReactions = canViewMessageReactionList(message: message) let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? if canDeleteReactions && canViewReactions { deleteReaction = { [weak self] peer, _ in dismissController?({ [weak self] in - guard let self, self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel else { + guard let self else { return } - let _ = (self.context.sharedContext.chatAvailableMessageActions( - engine: self.context.engine, - accountPeerId: self.context.account.peerId, - messageIds: Set([message.id]), - keepUpdated: false - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] actions in - guard let self, !actions.options.isEmpty else { - return - } - self.presentBanMessageOptions( - accountPeerId: self.context.account.peerId, - author: peer._asPeer(), - messageIds: Set([message.id]), - options: actions.options, - reaction: true - ) - }) + self.presentReactionDeletionOptions(author: peer._asPeer(), messageId: message.id) }) } } else { @@ -408,6 +396,10 @@ extension ChatControllerImpl { } func openMessageSendStarsScreen(message: Message) { + guard canSendReactionsToChat(self.presentationInterfaceState) else { + return + } + if let current = self.currentSendStarsUndoController { self.currentSendStarsUndoController = nil current.dismiss() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index befab1cc12..e6a8b66608 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -52,13 +52,6 @@ func canEditMessage(context: AccountContext, limitsConfiguration: EngineConfigur return canEditMessage(accountPeerId: context.account.peerId, limitsConfiguration: limitsConfiguration, message: message) } -func canDeleteOtherUserMessages(message: Message) -> Bool { - guard let channel = message.peers[message.id.peerId] as? TelegramChannel else { - return false - } - return channel.hasPermission(.deleteAllMessages) -} - private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineConfiguration.Limits, message: Message, reschedule: Bool = false) -> Bool { var hasEditRights = false var unlimitedInterval = reschedule @@ -2181,30 +2174,13 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? - if canDeleteOtherUserMessages(message: message) { + if let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.hasPermission(.deleteAllMessages) { deleteReaction = { [weak c] peer, _ in c?.dismiss(completion: { - guard let chatController = interfaceInteraction.chatController() as? ChatControllerImpl, chatController.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel else { + guard let chatController = interfaceInteraction.chatController() as? ChatController else { return } - let _ = (context.sharedContext.chatAvailableMessageActions( - engine: context.engine, - accountPeerId: context.account.peerId, - messageIds: Set([message.id]), - keepUpdated: false - ) - |> deliverOnMainQueue).startStandalone(next: { actions in - guard !actions.options.isEmpty else { - return - } - chatController.presentBanMessageOptions( - accountPeerId: context.account.peerId, - author: peer._asPeer(), - messageIds: Set([message.id]), - options: actions.options, - reaction: true - ) - }) + chatController.presentReactionDeletionOptions(author: peer._asPeer(), messageId: message.id) }) } } else { diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 90c78a09d6..60f4344fa0 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -182,6 +182,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: (highlight?.quote).flatMap { quote in NavigateToMessageParams.Quote(string: quote.string, offset: quote.offset) }, subject: highlight?.subject, setupReply: setupReply)), animated: isFirst || params.forceAnimatedScroll, completion: { [weak navigationController, weak controller] in if let navigationController = navigationController, let controller = controller { let _ = navigationController.popToViewController(controller, animated: animated) + params.completion(controller) } }, customPresentProgress: { [weak navigationController] c, a in (navigationController?.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a)