diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 327ec276c4..52273a5729 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1198,11 +1198,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: text), elevatedLayout: false, animateInAsReplacement: true, action: { shouldCommit in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: text), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { - return + return false } - if shouldCommit { + if value == .commit { let context = strongSelf.context let presentationData = strongSelf.presentationData let progressSignal = Signal { subscriber in @@ -1230,7 +1230,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } let _ = (signal |> deliverOnMainQueue).start() - } else { + return true + } else if value == .undo { strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!) strongSelf.chatListDisplayNode.chatListNode.updateState({ state in var state = state @@ -1240,7 +1241,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return state }) self?.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!) + return true } + return false }), in: .current) strongSelf.donePressed() @@ -1310,11 +1313,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, }) if value { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .hidArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] shouldCommit in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .hidArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let strongSelf = self else { - return + return false } - if !shouldCommit { + if value == .undo { let _ = (strongSelf.context.account.postbox.transaction { transaction -> Bool in var updatedValue = false updateChatArchiveSettings(transaction: transaction, { settings in @@ -1325,10 +1328,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, }) return updatedValue }).start() + return true } + return false }), in: .current) } else { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .revealedArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveRevealedTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveRevealedText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .revealedArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveRevealedTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveRevealedText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) } }) @@ -1422,11 +1427,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return true }) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: strongSelf.presentationData.strings.Undo_ChatCleared), elevatedLayout: false, animateInAsReplacement: true, action: { shouldCommit in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: strongSelf.presentationData.strings.Undo_ChatCleared), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { - return + return false } - if shouldCommit { + if value == .commit { let _ = clearHistoryInteractively(postbox: strongSelf.context.account.postbox, peerId: peerId, type: type).start(completed: { guard let strongSelf = self else { return @@ -1437,13 +1442,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return state }) }) - } else { + return true + } else if value == .undo { strongSelf.chatListDisplayNode.chatListNode.updateState({ state in var state = state state.pendingClearHistoryPeerIds.remove(peer.peerId) return state }) + return true } + return false }), in: .current) } @@ -1618,11 +1626,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, deleteSendMessageIntents(peerId: peerId) } - let action: (Bool) -> Void = { shouldCommit in + let action: (UndoOverlayAction) -> Bool = { value in guard let strongSelf = self else { - return + return false } - if !shouldCommit { + if value == .undo { strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds[0]) let _ = (postbox.transaction { transaction -> Void in for peerId in peerIds { @@ -1635,6 +1643,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) }) + return true + } else { + return false } } @@ -1721,11 +1732,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return true }) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] shouldCommit in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let strongSelf = self else { - return + return false } - if shouldCommit { + if value == .commit { strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId) if let channel = chatPeer as? TelegramChannel { strongSelf.context.peerChannelMemberCategoriesContextsManager.externallyRemoved(peerId: channel.id, memberId: strongSelf.context.account.peerId) @@ -1744,7 +1755,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, deleteSendMessageIntents(peerId: peerId) }) completion() - } else { + return true + } else if value == .undo { strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId) strongSelf.chatListDisplayNode.chatListNode.updateState({ state in var state = state @@ -1752,7 +1764,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return state }) strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) + return true } + return false }), in: .current) } diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index 933537b4cd..3ba28aaffd 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -13,6 +13,7 @@ import TextFormat import AccountContext import StickerPackPreviewUI import ItemListStickerPackItem +import UndoUI private final class InstalledStickerPacksControllerArguments { let account: Account @@ -494,6 +495,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta let archivedPromise = Promise<[ArchivedStickerPackItem]?>() var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)? + var navigationControllerImpl: (() -> NavigationController?)? let arguments = InstalledStickerPacksControllerArguments(account: context.account, openStickerPack: { info in presentStickerPackController?(info) @@ -511,6 +513,31 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } + let removeAction: (RemoveStickerPackOption) -> Void = { action in + let _ = (removeStickerPackInteractively(postbox: context.account.postbox, id: archivedItem.info.id, option: .archive) + |> deliverOnMainQueue).start(next: { indexAndItems in + guard let (positionInList, items) = indexAndItems else { + return + } + + var animateInAsReplacement = false + if let navigationController = navigationControllerImpl?() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(archivedItem.info.title).0, undo: true, info: archivedItem.info, topItem: archivedItem.topItems.first, account: context.account), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { action in + if case .undo = action { + let _ = addStickerPackInteractively(postbox: context.account.postbox, info: archivedItem.info, items: items, positionInList: positionInList).start() + } + return true + })) + }) + } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: presentationData.strings.StickerSettings_ContextInfo), @@ -524,12 +551,12 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta archivedPromise.set(.single(packs)) updatedPacks(packs) }) - - let _ = removeStickerPackInteractively(postbox: context.account.postbox, id: archivedItem.info.id, option: .archive).start() + + removeAction(.archive) }), ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { dismissAction() - let _ = removeStickerPackInteractively(postbox: context.account.postbox, id: archivedItem.info.id, option: .delete).start() + removeAction(.delete) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -800,10 +827,37 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta packs.insert(packReference, at: 0) } if let mainStickerPack = mainStickerPack { - presentControllerImpl?(StickerPackScreen(context: context, mainStickerPack: mainStickerPack, stickerPacks: packs, parentNavigationController: controller?.navigationController as? NavigationController), nil) + presentControllerImpl?(StickerPackScreen(context: context, mainStickerPack: mainStickerPack, stickerPacks: packs, parentNavigationController: controller?.navigationController as? NavigationController, actionPerformed: { info, items, action in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var animateInAsReplacement = false + if let navigationController = navigationControllerImpl?() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + switch action { + case .add: + navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).0, undo: false, info: info, topItem: items.first, account: context.account), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { _ in + return true + })) + case let .remove(positionInList): + navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).0, undo: true, info: info, topItem: items.first, account: context.account), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { action in + if case .undo = action { + let _ = addStickerPackInteractively(postbox: context.account.postbox, info: info, items: items, positionInList: positionInList).start() + } + return true + })) + } + }), nil) } }) } + navigationControllerImpl = { [weak controller] in + return controller?.navigationController as? NavigationController + } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index f2c7181e6a..77b517f282 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -63,10 +63,13 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese } } - public init(context: AccountContext, stickerPack: StickerPackReference, mode: StickerPackPreviewControllerMode = .default, parentNavigationController: NavigationController?) { + private let actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? + + public init(context: AccountContext, stickerPack: StickerPackReference, mode: StickerPackPreviewControllerMode = .default, parentNavigationController: NavigationController?, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? = nil) { self.context = context self.mode = mode self.parentNavigationController = parentNavigationController + self.actionPerformed = actionPerformed self.stickerPack = stickerPack @@ -133,7 +136,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: parentNavigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: true)) } })) - }) + }, actionPerformed: self.actionPerformed) self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index 12a2bb9989..2230f24959 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -74,6 +74,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol var dismiss: (() -> Void)? var cancel: (() -> Void)? var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? let ready = Promise() private var didSetReady = false @@ -87,10 +88,11 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol private var hapticFeedback: HapticFeedback? - init(context: AccountContext, openShare: (() -> Void)?, openMention: @escaping (String) -> Void) { + init(context: AccountContext, openShare: (() -> Void)?, openMention: @escaping (String) -> Void, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)?) { self.context = context self.openShare = openShare self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.actionPerformed = actionPerformed self.wrappingScrollNode = ASScrollNode() self.wrappingScrollNode.view.alwaysBounceVertical = true @@ -509,23 +511,27 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } @objc func installActionButtonPressed() { - let dismissOnAction: Bool - if let initiallyInstalled = self.stickerPackInitiallyInstalled, initiallyInstalled { - dismissOnAction = false - } else { - dismissOnAction = true - } + let dismissOnAction = true if let stickerPack = self.stickerPack, let stickerSettings = self.stickerSettings { switch stickerPack { case let .result(info, items, installed): if installed { - let _ = removeStickerPackInteractively(postbox: self.context.account.postbox, id: info.id, option: .delete).start() - self.updateStickerPack(.result(info: info, items: items, installed: false), stickerSettings: stickerSettings) + let _ = (removeStickerPackInteractively(postbox: self.context.account.postbox, id: info.id, option: .delete) + |> deliverOnMainQueue).start(next: { [weak self] indexAndItems in + guard let strongSelf = self, let (positionInList, _) = indexAndItems else { + return + } + strongSelf.actionPerformed?(info, items, .remove(positionInList: positionInList)) + }) + if !dismissOnAction { + self.updateStickerPack(.result(info: info, items: items, installed: false), stickerSettings: stickerSettings) + } } else { let _ = addStickerPackInteractively(postbox: self.context.account.postbox, info: info, items: items).start() if !dismissOnAction { self.updateStickerPack(.result(info: info, items: items, installed: true), stickerSettings: stickerSettings) } + self.actionPerformed?(info, items, .add) } if dismissOnAction { self.cancelButtonPressed() diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 0ee8479c3b..a9db7dd57b 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -842,8 +842,13 @@ public final class StickerPackScreenImpl: ViewController { } } -public func StickerPackScreen(context: AccountContext, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil) -> ViewController { - let controller = StickerPackPreviewController(context: context, stickerPack: mainStickerPack, mode: .default, parentNavigationController: parentNavigationController) +public enum StickerPackScreenPerformedAction { + case add + case remove(positionInList: Int) +} + +public func StickerPackScreen(context: AccountContext, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? = nil) -> ViewController { + let controller = StickerPackPreviewController(context: context, stickerPack: mainStickerPack, mode: .default, parentNavigationController: parentNavigationController, actionPerformed: actionPerformed) controller.sendSticker = sendSticker return controller } diff --git a/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift b/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift index 28fa1e1a6d..4605f32262 100644 --- a/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift +++ b/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public func addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal { +public func addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [ItemCollectionItem], positionInList: Int? = nil) -> Signal { return postbox.transaction { transaction -> Void in let namespace: SynchronizeInstalledStickerPacksOperationNamespace? switch info.id.namespace { @@ -23,7 +23,11 @@ public func addStickerPackInteractively(postbox: Postbox, info: StickerPackColle updatedInfos.remove(at: index) updatedInfos.insert(currentInfo, at: 0) } else { - updatedInfos.insert(info, at: 0) + if let positionInList = positionInList, positionInList <= updatedInfos.count { + updatedInfos.insert(info, at: positionInList) + } else { + updatedInfos.insert(info, at: 0) + } transaction.replaceItemCollectionItems(collectionId: info.id, items: items) } transaction.replaceItemCollectionInfos(namespace: info.id.namespace, itemCollectionInfos: updatedInfos.map { ($0.id, $0) }) @@ -36,8 +40,8 @@ public enum RemoveStickerPackOption { case archive } -public func removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal { - return postbox.transaction { transaction -> Void in +public func removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { + return postbox.transaction { transaction -> (Int, [ItemCollectionItem])? in let namespace: SynchronizeInstalledStickerPacksOperationNamespace? switch id.namespace { case Namespaces.ItemCollection.CloudStickerPacks: @@ -55,8 +59,14 @@ public func removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionI case .archive: content = .archive([id]) } + let index = transaction.getItemCollectionsInfos(namespace: id.namespace).index(where: { $0.0 == id }) + let items = transaction.getItemCollectionItems(collectionId: id) + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: content) transaction.removeItemCollection(collectionId: id) + return index.flatMap { ($0, items) } + } else { + return nil } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index a48afb8694..ab34384fad 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -1727,7 +1727,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }, displaySwipeToReplyHint: { [weak self] in if let strongSelf = self, let validLayout = strongSelf.validLayout, min(validLayout.size.width, validLayout.size.height) > 320.0 { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: true, action: { _ in }), in: .window(.root)) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: true, action: { _ in return false }), in: .window(.root)) } }, dismissReplyMarkupMessage: { [weak self] message in guard let strongSelf = self, strongSelf.presentationInterfaceState.keyboardButtonsMessage?.id == message.id else { diff --git a/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift b/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift index 70243dfaa8..4c6543adad 100644 --- a/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift +++ b/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift @@ -20,6 +20,7 @@ import SettingsUI import AlertUI import PresentationDataUtils import ShareController +import UndoUI private enum ChatMessageGalleryControllerData { case url(String) @@ -304,7 +305,31 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.navigationController?.pushViewController(controller) return true case let .stickerPack(reference): - let controller = StickerPackScreen(context: params.context, mainStickerPack: reference, stickerPacks: [reference], sendSticker: params.sendSticker) + let controller = StickerPackScreen(context: params.context, mainStickerPack: reference, stickerPacks: [reference], sendSticker: params.sendSticker, actionPerformed: { info, items, action in + let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } + var animateInAsReplacement = false + if let navigationController = params.navigationController { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + switch action { + case .add: + params.navigationController?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).0, undo: false, info: info, topItem: items.first, account: params.context.account), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { _ in + return true + })) + case let .remove(positionInList): + params.navigationController?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).0, undo: true, info: info, topItem: items.first, account: params.context.account), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { action in + if case .undo = action { + let _ = addStickerPackInteractively(postbox: params.context.account.postbox, info: info, items: items, positionInList: positionInList).start() + } + return true + })) + } + }) params.dismissInput() params.present(controller, nil) return true diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index aec849f42b..8dd4662294 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -2,6 +2,9 @@ import Foundation import UIKit import Display import TelegramPresentationData +import SyncCore +import Postbox +import TelegramCore public enum UndoOverlayContent { case removedChat(text: String) @@ -12,6 +15,13 @@ public enum UndoOverlayContent { case emoji(path: String, text: String) case swipeToReply(title: String, text: String) case actionSucceeded(title: String, text: String, cancel: String) + case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: ItemCollectionItem?, account: Account) +} + +public enum UndoOverlayAction { + case info + case undo + case commit } public final class UndoOverlayController: ViewController { @@ -19,12 +29,12 @@ public final class UndoOverlayController: ViewController { public let content: UndoOverlayContent private let elevatedLayout: Bool private let animateInAsReplacement: Bool - private var action: (Bool) -> Void + private var action: (UndoOverlayAction) -> Bool private var didPlayPresentationAnimation = false private var dismissed = false - public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, animateInAsReplacement: Bool = false, action: @escaping (Bool) -> Void) { + public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, animateInAsReplacement: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) { self.presentationData = presentationData self.content = content self.elevatedLayout = elevatedLayout @@ -42,7 +52,7 @@ public final class UndoOverlayController: ViewController { override public func loadDisplayNode() { self.displayNode = UndoOverlayControllerNode(presentationData: self.presentationData, content: self.content, elevatedLayout: self.elevatedLayout, action: { [weak self] value in - self?.action(value) + return self?.action(value) ?? false }, dismiss: { [weak self] in self?.dismiss() }) @@ -50,12 +60,12 @@ public final class UndoOverlayController: ViewController { } public func dismissWithCommitAction() { - self.action(true) + self.action(.commit) self.dismiss() } public func dismissWithCommitActionAndReplacementAnimation() { - self.action(true) + self.action(.commit) (self.displayNode as! UndoOverlayControllerNode).animateOutWithReplacement(completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) }) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index d18a5ab7e2..c18fd9780c 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -9,23 +9,31 @@ import Markdown import RadialStatusNode import AppBundle import AnimatedStickerNode +import TelegramAnimatedStickerNode import AnimationUI +import SyncCore +import Postbox +import TelegramCore +import StickerResources final class UndoOverlayControllerNode: ViewControllerTracingNode { private let elevatedLayout: Bool - private let statusNode: RadialStatusNode + private var statusNode: RadialStatusNode? private let timerTextNode: ImmediateTextNode private let iconNode: ASImageNode? private let iconCheckNode: RadialStatusNode? private let animationNode: AnimationNode? - private let animatedStickerNode: AnimatedStickerNode? + private var animatedStickerNode: AnimatedStickerNode? + private var stillStickerNode: TransformImageNode? + private var stickerImageSize: CGSize? private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode - private let buttonTextNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode + private let undoButtonTextNode: ImmediateTextNode + private let undoButtonNode: HighlightTrackingButtonNode private let panelNode: ASDisplayNode private let panelWrapperNode: ASDisplayNode - private let action: (Bool) -> Void + private let action: (UndoOverlayAction) -> Bool private let dismiss: () -> Void private let effectView: UIView @@ -38,7 +46,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var validLayout: ContainerViewLayout? - init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, action: @escaping (Bool) -> Void, dismiss: @escaping () -> Void) { + private var fetchResourceDisposable: Disposable? + + init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) { self.elevatedLayout = elevatedLayout self.action = action @@ -55,6 +65,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 0 + self.buttonNode = HighlightTrackingButtonNode() + var displayUndo = true var undoText = presentationData.strings.Undo_Undo var undoTextColor = UIColor(rgb: 0x5ac8fa) @@ -74,6 +86,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) displayUndo = true self.originalRemainingSeconds = 5 + self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) case let .archivedChat(_, title, text, undo): if undo { self.iconNode = ASImageNode() @@ -129,12 +142,17 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.iconCheckNode = nil self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) self.animatedStickerNode = nil + + undoTextColor = UIColor(rgb: 0xff7b74) + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) + self.textNode.attributedText = attributedText displayUndo = true undoText = cancel - undoTextColor = UIColor(rgb: 0xff7b74) self.originalRemainingSeconds = 3 case let .emoji(path, text): self.iconNode = nil @@ -161,17 +179,102 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 2 displayUndo = false self.originalRemainingSeconds = 5 + case let .stickersModified(title, text, undo, info, topItem, account): + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = nil + + let stillStickerNode = TransformImageNode() + + self.stillStickerNode = stillStickerNode + + enum StickerPackThumbnailItem { + case still(TelegramMediaImageRepresentation) + case animated(MediaResource) + } + + var thumbnailItem: StickerPackThumbnailItem? + var resourceReference: MediaResourceReference? + + if let thumbnail = info.thumbnail { + if info.flags.contains(.isAnimated) { + thumbnailItem = .animated(thumbnail.resource) + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource) + } else { + thumbnailItem = .still(thumbnail) + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource) + } + } else if let item = topItem as? StickerPackItem { + if item.file.isAnimatedSticker { + thumbnailItem = .animated(item.file.resource) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) + } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource)) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) + } + } + + var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchSignal: Signal? + + let imageBoundingSize = CGSize(width: 34.0, height: 34.0) + + if let thumbnailItem = thumbnailItem { + switch thumbnailItem { + case let .still(representation): + let stillImageSize = representation.dimensions.cgSize.aspectFitted(imageBoundingSize) + self.stickerImageSize = stillImageSize + + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource) + case let .animated(resource): + self.stickerImageSize = imageBoundingSize + + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true) + } + if let resourceReference = resourceReference { + updatedFetchSignal = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference) + } + } else { + updatedImageSignal = .single({ _ in return nil }) + updatedFetchSignal = .complete() + } + + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) + self.textNode.attributedText = attributedText + self.textNode.maximumNumberOfLines = 2 + displayUndo = undo + self.originalRemainingSeconds = 2 + + if let updatedFetchSignal = updatedFetchSignal { + self.fetchResourceDisposable = updatedFetchSignal.start() + } + + if let updatedImageSignal = updatedImageSignal { + stillStickerNode.setSignal(updatedImageSignal) + } + + if let thumbnailItem = thumbnailItem { + switch thumbnailItem { + case .still: + break + case let .animated(resource): + let animatedStickerNode = AnimatedStickerNode() + self.animatedStickerNode = animatedStickerNode + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .cached) + } + } } self.remainingSeconds = self.originalRemainingSeconds - self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) + self.undoButtonTextNode = ImmediateTextNode() + self.undoButtonTextNode.displaysAsynchronously = false + self.undoButtonTextNode.attributedText = NSAttributedString(string: undoText, font: Font.regular(17.0), textColor: undoTextColor) - self.buttonTextNode = ImmediateTextNode() - self.buttonTextNode.displaysAsynchronously = false - self.buttonTextNode.attributedText = NSAttributedString(string: undoText, font: Font.regular(17.0), textColor: undoTextColor) - - self.buttonNode = HighlightTrackingButtonNode() + self.undoButtonNode = HighlightTrackingButtonNode() self.panelNode = ASDisplayNode() if presentationData.theme.overallDarkAppearance { @@ -191,35 +294,46 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch content { case .removedChat: self.panelWrapperNode.addSubnode(self.timerTextNode) - self.panelWrapperNode.addSubnode(self.statusNode) - case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded: + case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified: break } + self.statusNode.flatMap(self.panelWrapperNode.addSubnode) self.iconNode.flatMap(self.panelWrapperNode.addSubnode) self.iconCheckNode.flatMap(self.panelWrapperNode.addSubnode) self.animationNode.flatMap(self.panelWrapperNode.addSubnode) + self.stillStickerNode.flatMap(self.panelWrapperNode.addSubnode) self.animatedStickerNode.flatMap(self.panelWrapperNode.addSubnode) self.panelWrapperNode.addSubnode(self.titleNode) self.panelWrapperNode.addSubnode(self.textNode) + self.panelWrapperNode.addSubnode(self.buttonNode) if displayUndo { - self.panelWrapperNode.addSubnode(self.buttonTextNode) - self.panelWrapperNode.addSubnode(self.buttonNode) + self.panelWrapperNode.addSubnode(self.undoButtonTextNode) + self.panelWrapperNode.addSubnode(self.undoButtonNode) } self.addSubnode(self.panelNode) self.addSubnode(self.panelWrapperNode) - self.buttonNode.highligthedChanged = { [weak self] highlighted in + self.undoButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.buttonTextNode.layer.removeAnimation(forKey: "opacity") - strongSelf.buttonTextNode.alpha = 0.4 + strongSelf.undoButtonTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.undoButtonTextNode.alpha = 0.4 } else { - strongSelf.buttonTextNode.alpha = 1.0 - strongSelf.buttonTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.undoButtonTextNode.alpha = 1.0 + strongSelf.undoButtonTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.undoButtonNode.addTarget(self, action: #selector(self.undoButtonPressed), forControlEvents: .touchUpInside) + + self.animatedStickerNode?.started = { [weak self] in + self?.stillStickerNode?.isHidden = true + } + } + + deinit { + self.fetchResourceDisposable?.dispose() } override func didLoad() { @@ -230,7 +344,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } @objc private func buttonPressed() { - self.action(false) + if self.action(.info) { + self.dismiss() + } + } + + @objc private func undoButtonPressed() { + self.action(.undo) self.dismiss() } @@ -239,7 +359,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.remainingSeconds -= 1 } if self.remainingSeconds == 0 { - self.action(true) + self.action(.commit) self.dismiss() } else { if !self.timerTextNode.bounds.size.width.isZero, let snapshot = self.timerTextNode.view.snapshotContentTree() { @@ -286,9 +406,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let margin: CGFloat = 16.0 - let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let buttonTextSize = self.undoButtonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) let buttonMinX: CGFloat - if self.buttonNode.supernode != nil { + if self.undoButtonNode.supernode != nil { buttonMinX = layout.size.width - layout.safeInsets.left - rightInset - buttonTextSize.width - margin * 2.0 } else { buttonMinX = layout.size.width - layout.safeInsets.left - rightInset @@ -316,8 +436,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.effectView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight) let buttonTextFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - margin * 2.0, y: floor((contentHeight - buttonTextSize.height) / 2.0)), size: buttonTextSize) - transition.updateFrame(node: self.buttonTextNode, frame: buttonTextFrame) - self.buttonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - margin * 2.0, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + margin, height: contentHeight)) + transition.updateFrame(node: self.undoButtonTextNode, frame: buttonTextFrame) + + let undoButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - margin * 2.0, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + margin, height: contentHeight)) + self.undoButtonNode.frame = undoButtonFrame + + self.buttonNode.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: 0.0), size: CGSize(width: undoButtonFrame.minX - layout.safeInsets.left, height: contentHeight)) var textContentHeight = textSize.height var textOffset: CGFloat = 0.0 @@ -351,7 +475,22 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { transition.updateFrame(node: animationNode, frame: iconFrame) } - if let animatedStickerNode = self.animatedStickerNode { + if let stickerImageSize = self.stickerImageSize { + let iconSize = stickerImageSize + let iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0)), size: iconSize) + + if let stillStickerNode = self.stillStickerNode { + let makeImageLayout = stillStickerNode.asyncLayout() + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stickerImageSize, boundingSize: stickerImageSize, intrinsicInsets: UIEdgeInsets())) + let _ = imageApply() + transition.updateFrame(node: stillStickerNode, frame: iconFrame) + } + + if let animatedStickerNode = self.animatedStickerNode { + animatedStickerNode.updateLayout(size: iconFrame.size) + transition.updateFrame(node: animatedStickerNode, frame: iconFrame) + } + } else if let animatedStickerNode = self.animatedStickerNode { let iconSize = CGSize(width: 32.0, height: 32.0) let iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0)), size: iconSize) animatedStickerNode.updateLayout(size: iconFrame.size) @@ -361,9 +500,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let timerTextSize = self.timerTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.timerTextNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - timerTextSize.width) / 2.0), y: floor((contentHeight - timerTextSize.height) / 2.0)), size: timerTextSize)) let statusSize: CGFloat = 30.0 - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize))) - if firstLayout { - self.statusNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {}) + if let statusNode = self.statusNode { + transition.updateFrame(node: statusNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize))) + if firstLayout { + statusNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {}) + } } }