diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2b5816eca0..f09b8368f3 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8134,5 +8134,11 @@ Sorry for the inconvenience."; "Channel.EditAdmin.PermissionCreateTopics" = "Create Topics"; "ChatList.Search.FilterTopics" = "Topics"; - "DialogList.SearchSectionTopics" = "Topics"; + +"ChatListFolderSettings.SubscribeToMoveAll" = "Subscribe to **Telegram Premium** to move the \"All Chats\" folder."; +"ChatListFolderSettings.SubscribeToMoveAllAction" = "More"; + +"Channel.AdminLog.MessageChangedGroupUsernames" = "%@ changed group links:"; +"Channel.AdminLog.MessageChangedChannelUsernames" = "%@ changed channel links:"; +"Channel.AdminLog.MessagePreviousLinks" = "Previous links"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 1dbc3c9889..6a2d8f8cd2 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1792,6 +1792,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self?.askForFilterRemoval(id: id) } } + self.tabContainerNode.presentPremiumTip = { [weak self] in + if let strongSelf = self { + let context = strongSelf.context + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: nil, text: strongSelf.presentationData.strings.ChatListFolderSettings_SubscribeToMoveAll, customUndoText: strongSelf.presentationData.strings.ChatListFolderSettings_SubscribeToMoveAllAction), elevatedLayout: true, animateInAsReplacement: false, action: { action in + if case .undo = action { + strongSelf.push(PremiumIntroScreen(context: context, source: .folders)) + } + return false }), in: .window(.root)) + } + } let tabContextGesture: (Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool, Bool) -> Void = { [weak self] id, sourceNode, gesture, keepInPlace, isDisabled in guard let strongSelf = self else { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index f624a09491..50767e3c89 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -11,6 +11,7 @@ import AccountContext import ItemListPeerActionItem import ChatListFilterSettingsHeaderItem import PremiumUI +import UndoUI private final class ChatListFilterPresetListControllerArguments { let context: AccountContext @@ -223,7 +224,7 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present var folderCount = 0 for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { - if isPremium, case .allChats = filter { + if case .allChats = filter { entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false)) } if case let .filter(_, title, _, _) = filter { @@ -512,6 +513,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch |> afterDisposed { } + var previousOrder: [Int32]? + let controller = ItemListController(context: context, state: signal) controller.isOpaqueWhenInOverlay = true controller.blocksBackgroundWhenInOverlay = true @@ -598,6 +601,28 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } } }) + controller.setReorderCompleted({ (entries: [ChatListFilterPresetListEntry]) -> Void in + let _ = (combineLatest( + updatedFilterOrder.get() |> take(1), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + ) + |> deliverOnMainQueue).start(next: { order, peer in + let isPremium = peer?.isPremium ?? false + if !isPremium, let order = order, order.first != 0 { + updatedFilterOrder.set(.single(previousOrder)) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: nil, text: presentationData.strings.ChatListFolderSettings_SubscribeToMoveAll, customUndoText: presentationData.strings.ChatListFolderSettings_SubscribeToMoveAllAction), elevatedLayout: false, animateInAsReplacement: false, action: { action in + if case .undo = action { + pushControllerImpl?(PremiumIntroScreen(context: context, source: .folders)) + } + return false }) + ) + } else { + previousOrder = order + } + }) + }) return controller } diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 46a7d32b1a..ddb971bef5 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -224,9 +224,7 @@ private final class ItemNode: ASDisplayNode { self.selectionFraction = selectionFraction self.unreadCount = unreadCount - - transition.updateAlpha(node: self.containerNode, alpha: (isReordering && isNoFilter && !canReorderAllChats) ? 0.5 : 1.0) - + if isReordering && !isNoFilter { if self.deleteButtonNode == nil { let deleteButtonNode = ItemNodeDeleteButtonNode(pressed: { [weak self] in @@ -275,7 +273,7 @@ private final class ItemNode: ASDisplayNode { if self.isReordering != isReordering { self.isReordering = isReordering - if self.isReordering && (!isNoFilter || canReorderAllChats) { + if self.isReordering { self.startShaking() } else { self.layer.removeAnimation(forKey: "shaking_position") @@ -476,11 +474,13 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? var addFilter: (() -> Void)? var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? + var presentPremiumTip: (() -> Void)? private var reorderingGesture: ReorderingGestureRecognizer? private var reorderingItem: ChatListFilterTabEntryId? private var reorderingItemPosition: (initial: CGFloat, offset: CGFloat)? private var reorderingAutoScrollAnimator: ConstantDisplayLinkAnimator? + private var initialReorderedItemIds: [ChatListFilterTabEntryId]? private var reorderedItemIds: [ChatListFilterTabEntryId]? private lazy var hapticFeedback = { HapticFeedback() }() @@ -539,11 +539,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { guard let strongSelf = self else { return false } - for (id, itemNode) in strongSelf.itemNodes { + for (_, itemNode) in strongSelf.itemNodes { if itemNode.view.convert(itemNode.bounds, to: strongSelf.view).contains(point) { - if case .all = id, !(strongSelf.currentParams?.canReorderAllChats ?? false) { - return false - } return true } } @@ -552,6 +549,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { guard let strongSelf = self, let _ = strongSelf.currentParams else { return } + strongSelf.initialReorderedItemIds = strongSelf.reorderedItemIds for (id, itemNode) in strongSelf.itemNodes { let itemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.view) if itemFrame.contains(point) { @@ -594,6 +592,11 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { strongSelf.scrollNode.addSubnode(itemNode) } + if strongSelf.currentParams?.canReorderAllChats == false, let firstItem = strongSelf.reorderedItemIds?.first, case .filter = firstItem { + strongSelf.reorderedItemIds = strongSelf.initialReorderedItemIds + strongSelf.presentPremiumTip?() + } + strongSelf.reorderingItem = nil strongSelf.reorderingItemPosition = nil strongSelf.reorderingAutoScrollAnimator?.invalidate() @@ -606,7 +609,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { return } - let minIndex = (strongSelf.currentParams?.canReorderAllChats ?? false) ? 0 : 1 + let minIndex = 0 if let reorderingItemNode = strongSelf.itemNodes[reorderingItem], let (initial, _) = strongSelf.reorderingItemPosition, let reorderedItemIds = strongSelf.reorderedItemIds, let currentItemIndex = reorderedItemIds.firstIndex(of: reorderingItem) { for (id, itemNode) in strongSelf.itemNodes { @@ -927,13 +930,6 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { } else { transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) } - let lineAlpha: CGFloat - if isReordering && canReorderAllChats { - lineAlpha = 0.0 - } else { - lineAlpha = isReordering && selectedFilter == .all ? 0.5 : 1.0 - } - transition.updateAlpha(node: self.selectedLineNode, alpha: lineAlpha) if let previousSelectedFrame = self.previousSelectedFrame { let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0))) diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index b2358f7a01..0651503435 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -1285,7 +1285,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) + controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String @@ -1294,7 +1294,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } else { text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string } - controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { action in + controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { action in if case .info = action { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs) controllerInteraction?.pushController(controller) @@ -1489,7 +1489,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) + controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String @@ -1498,7 +1498,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } else { text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string } - controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { action in + controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { action in if case .info = action { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs) controllerInteraction?.pushController(controller) diff --git a/submodules/TelegramUI/Resources/Animations/anim_reorder.json b/submodules/TelegramUI/Resources/Animations/anim_reorder.json new file mode 100644 index 0000000000..6ea39cd8fb --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_reorder.json @@ -0,0 +1 @@ +{"v":"5.9.6","fr":60,"ip":0,"op":61,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"reo Outlines 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2,"l":2},"a":{"a":0,"k":[36,36,0],"ix":1,"l":2},"s":{"a":0,"k":[640,640,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":34,"s":[{"i":[[0,0],[0,0],[-0.117,0.117],[0,0]],"o":[[0,0],[0.117,0.117],[0,0],[0,0]],"v":[[-10.109,-23.167],[-0.212,-13.27],[0.212,-13.27],[10.109,-23.167]],"c":false}]},{"t":45,"s":[{"i":[[0,0],[0,0],[-0.117,0.117],[0,0]],"o":[[0,0],[0.117,0.117],[0,0],[0,0]],"v":[[-10.109,-5.007],[-0.212,4.89],[0.212,4.89],[10.109,-5.007]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[50]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[50]},{"t":45,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[50]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[50]},{"t":45,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[59,29.451],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-3.696,0],[0,0],[0,-3.699],[0,-3.125]],"o":[[0,0],[0,-3.699],[0,0],[4.267,0],[0,6.25],[0,0]],"v":[[-23.5,17.75],[-23.5,-11.052],[-17.376,-17.75],[17.519,-17.75],[23.5,-11.052],[23.5,5.01]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"t":45,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[35.5,26.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":61,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"reo Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2,"l":2},"a":{"a":0,"k":[36,36,0],"ix":1,"l":2},"s":{"a":0,"k":[640,640,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":15,"s":[{"i":[[0,0],[0,0],[-0.117,0.117],[0,0]],"o":[[0,0],[0.117,0.117],[0,0],[0,0]],"v":[[-10.109,-5.007],[-0.212,4.89],[0.212,4.89],[10.109,-5.007]],"c":false}]},{"t":32,"s":[{"i":[[0,0],[0,0],[-0.117,0.117],[0,0]],"o":[[0,0],[0.117,0.117],[0,0],[0,0]],"v":[[-10.109,16.852],[-0.212,26.749],[0.212,26.749],[10.109,16.852]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[0]},{"t":32,"s":[50]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[100]},{"t":32,"s":[50]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[5]},{"t":32,"s":[2]}],"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[59,29.451],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":6,"s":[{"i":[[-1.933,0],[0,0],[0,-1.933],[0,0],[1.933,0],[0,0],[0,1.933],[0,0]],"o":[[0,0],[1.933,0],[0,0],[0,1.933],[0,0],[-1.933,0],[0,0],[0,-1.933]],"v":[[-4,-7.5],[4,-7.5],[7.5,-4],[7.5,4],[4,7.5],[-4,7.5],[-7.5,4],[-7.5,-4]],"c":true}]},{"t":25,"s":[{"i":[[-1.933,0],[0,0],[0,-1.933],[0,0],[1.933,0],[0,0],[0,1.933],[0,0]],"o":[[0,0],[1.933,0],[0,0],[0,1.933],[0,0],[-1.933,0],[0,0],[0,-1.933]],"v":[[-28,-7.5],[-20,-7.5],[-16.5,-4],[-16.5,4],[-20,7.5],[-28,7.5],[-31.5,4],[-31.5,-4]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[60,53],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":3,"s":[{"i":[[-1.933,0],[0,0],[0,-1.933],[0,0],[1.933,0],[0,0],[0,1.933],[0,0]],"o":[[0,0],[1.933,0],[0,0],[0,1.933],[0,0],[-1.933,0],[0,0],[0,-1.933]],"v":[[-4,-7.5],[4,-7.5],[7.5,-4],[7.5,4],[4,7.5],[-4,7.5],[-7.5,4],[-7.5,-4]],"c":true}]},{"t":22,"s":[{"i":[[-1.933,0],[0,0],[0,-1.933],[0,0],[1.933,0],[0,0],[0,1.933],[0,0]],"o":[[0,0],[1.933,0],[0,0],[0,1.933],[0,0],[-1.933,0],[0,0],[0,-1.933]],"v":[[-28,-7.5],[-20,-7.5],[-16.5,-4],[-16.5,4],[-20,7.5],[-28,7.5],[-31.5,4],[-31.5,-4]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[36,53],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.933,0],[0,0],[0,-1.933],[0,0],[1.933,0],[0,0],[0,1.933],[0,0]],"o":[[0,0],[1.933,0],[0,0],[0,1.933],[0,0],[-1.933,0],[0,0],[0,-1.933]],"v":[[-4,-7.5],[4,-7.5],[7.5,-4],[7.5,4],[4,7.5],[-4,7.5],[-7.5,4],[-7.5,-4]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.8,"y":0},"t":0,"s":[12,53],"to":[0,-6.333],"ti":[-7.833,1]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[18,9],"to":[7.833,-1],"ti":[-0.167,-7.333]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[60,9],"to":[0.042,1.839],"ti":[0.005,-4.289]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[60.047,20.602],"to":[-0.004,2.839],"ti":[0.013,-3.162]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":26,"s":[60.02,32.7],"to":[-0.045,11.112],"ti":[-0.167,-7.333]},{"t":40,"s":[60,53]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[1],"y":[0]},"t":0,"s":[0]},{"t":20,"s":[90]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":15,"s":[{"i":[[0,0],[0,0],[-3.696,0],[0,0],[0,-3.699],[0,-3.125]],"o":[[0,0],[0,-3.699],[0,0],[4.267,0],[0,6.25],[0,0]],"v":[[-23.5,17.75],[-23.5,-11.052],[-17.376,-17.75],[17.519,-17.75],[23.5,-11.052],[23.5,5.01]],"c":false}]},{"t":32,"s":[{"i":[[0,0],[0,0],[-3.696,0],[0,0],[0,-3.699],[0,-3.125]],"o":[[0,0],[0,-3.699],[0,0],[4.267,0],[0,6.25],[0,0]],"v":[[-23.5,17.75],[-23.5,-11.052],[-17.376,-17.75],[17.519,-17.75],[23.5,-11.052],[23.5,26.869]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.753],"y":[0.386]},"o":{"x":[0.436],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.722],"y":[0.663]},"o":{"x":[0.402],"y":[0.197]},"t":9,"s":[10.82]},{"i":{"x":[0.679],"y":[0.748]},"o":{"x":[0.348],"y":[0.347]},"t":13,"s":[35.224]},{"i":{"x":[0.675],"y":[0.548]},"o":{"x":[0.344],"y":[0.38]},"t":14,"s":[42.626]},{"i":{"x":[0.662],"y":[0.696]},"o":{"x":[0.338],"y":[0.294]},"t":15,"s":[47.891]},{"i":{"x":[0.639],"y":[0.732]},"o":{"x":[0.314],"y":[0.319]},"t":17,"s":[64.73]},{"i":{"x":[0.65],"y":[0.661]},"o":{"x":[0.318],"y":[0.507]},"t":19,"s":[79.604]},{"i":{"x":[0.65],"y":[0.942]},"o":{"x":[0.318],"y":[0.49]},"t":20,"s":[83.079]},{"i":{"x":[0.651],"y":[0.516]},"o":{"x":[0.318],"y":[0.045]},"t":21,"s":[85.267]},{"i":{"x":[0.623],"y":[0.306]},"o":{"x":[0.294],"y":[0.995]},"t":22,"s":[87.881]},{"i":{"x":[0.595],"y":[1]},"o":{"x":[0.269],"y":[0.418]},"t":25,"s":[91.094]},{"t":32,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[5.2]},{"t":32,"s":[2]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[35.5,26.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":61,"st":0,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index a3871b5ebd..9608275a83 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -1855,7 +1855,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } switch result { case .generic: - strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String @@ -1864,7 +1864,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } else { text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string } - strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in + strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in guard let strongSelf = self else { return false } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index bce96616f4..d4b8fda7b4 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1391,7 +1391,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState Queue.mainQueue().after(0.2) { switch result { case .generic: - controllerInteraction.presentControllerInCurrent(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + controllerInteraction.presentControllerInCurrent(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String @@ -1400,7 +1400,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } else { text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string } - controllerInteraction.presentControllerInCurrent(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in + controllerInteraction.presentControllerInCurrent(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedGifs) controllerInteraction.navigationController()?.pushViewController(controller) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index c9724d21f4..9761bfd768 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1535,7 +1535,7 @@ final class ChatMediaInputNode: ChatInputNode { |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String @@ -1544,7 +1544,7 @@ final class ChatMediaInputNode: ChatInputNode { } else { text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string } - controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in + controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedGifs) controllerInteraction.navigationController()?.pushViewController(controller) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 7821224342..e374afb19a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -2270,84 +2270,91 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + private var playedSwipeToReplyHaptic = false + @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + var offset: CGFloat = 0.0 + var swipeOffset: CGFloat = 45.0 + if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { + offset = -24.0 + } else { + offset = 10.0 + swipeOffset = 60.0 + } + switch recognizer.state { - case .began: - self.currentSwipeToReplyTranslation = 0.0 - if self.swipeToReplyFeedback == nil { - self.swipeToReplyFeedback = HapticFeedback() - self.swipeToReplyFeedback?.prepareImpact() - } - (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() - case .changed: - var translation = recognizer.translation(in: self.view) - translation.x = max(-80.0, min(0.0, translation.x)) - var animateReplyNodeIn = false - if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) { - if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { - self.swipeToReplyFeedback?.impact(.heavy) - + case .began: + self.playedSwipeToReplyHaptic = false + self.currentSwipeToReplyTranslation = 0.0 + if self.swipeToReplyFeedback == nil { + self.swipeToReplyFeedback = HapticFeedback() + self.swipeToReplyFeedback?.prepareImpact() + } + self.item?.controllerInteraction.cancelInteractiveKeyboardGestures() + case .changed: + var translation = recognizer.translation(in: self.view) + translation.x = max(-80.0, min(0.0, translation.x)) + + if let item = self.item, self.swipeToReplyNode == nil { let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), backgroundNode: item.controllerInteraction.presentationContext.backgroundNode, action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) self.swipeToReplyNode = swipeToReplyNode - self.addSubnode(swipeToReplyNode) - animateReplyNodeIn = true + self.insertSubnode(swipeToReplyNode, at: 0) } - } - self.currentSwipeToReplyTranslation = translation.x - var bounds = self.bounds - bounds.origin.x = -translation.x - self.bounds = bounds + + self.currentSwipeToReplyTranslation = translation.x + var bounds = self.bounds + bounds.origin.x = -translation.x + self.bounds = bounds - self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) + self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) - if let swipeToReplyNode = self.swipeToReplyNode { - swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) - - if let (rect, containerSize) = self.absoluteRect { - let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) - swipeToReplyNode.updateAbsoluteRect(mappedRect, within: containerSize) + if let swipeToReplyNode = self.swipeToReplyNode { + swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width + offset, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) + + if let (rect, containerSize) = self.absoluteRect { + let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) + swipeToReplyNode.updateAbsoluteRect(mappedRect, within: containerSize) + } + + let progress = abs(translation.x) / swipeOffset + swipeToReplyNode.updateProgress(progress) + + if progress == 1.0 && !self.playedSwipeToReplyHaptic { + self.swipeToReplyFeedback?.impact(.heavy) + } } + case .cancelled, .ended: + self.swipeToReplyFeedback = nil - if animateReplyNodeIn { - swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) - swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) - } else { - swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0)) - } - } - case .cancelled, .ended: - self.swipeToReplyFeedback = nil - - let translation = recognizer.translation(in: self.view) - if case .ended = recognizer.state, translation.x < -45.0 { - if let item = self.item { - if let currentSwipeAction = currentSwipeAction { - switch currentSwipeAction { - case .none: - break - case .reply: - item.controllerInteraction.setupReply(item.message.id) + let translation = recognizer.translation(in: self.view) + if case .ended = recognizer.state, translation.x < -swipeOffset { + if let item = self.item { + if let currentSwipeAction = currentSwipeAction { + switch currentSwipeAction { + case .none: + break + case .reply: + item.controllerInteraction.setupReply(item.message.id) + } } } } - } - var bounds = self.bounds - let previousBounds = bounds - bounds.origin.x = 0.0 - self.bounds = bounds - self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.x = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.updateAttachedAvatarNodeOffset(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + self.updateAttachedAvatarNodeOffset(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) - if let swipeToReplyNode = self.swipeToReplyNode { - self.swipeToReplyNode = nil - swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in - swipeToReplyNode?.removeFromSupernode() - }) - swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } - default: - break + if let swipeToReplyNode = self.swipeToReplyNode { + self.swipeToReplyNode = nil + swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in + swipeToReplyNode?.removeFromSupernode() + }) + swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + default: + break } } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index b23bb31840..4495026442 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -3903,6 +3903,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } + private var playedSwipeToReplyHaptic = false @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 var swipeOffset: CGFloat = 45.0 @@ -3915,6 +3916,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode switch recognizer.state { case .began: + self.playedSwipeToReplyHaptic = false self.currentSwipeToReplyTranslation = 0.0 if self.swipeToReplyFeedback == nil { self.swipeToReplyFeedback = HapticFeedback() @@ -3925,7 +3927,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var translation = recognizer.translation(in: self.view) translation.x = max(-80.0, min(0.0, translation.x)) - if let item = self.item, self.swipeToReplyNode == nil { let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), backgroundNode: item.controllerInteraction.presentationContext.backgroundNode, action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) self.swipeToReplyNode = swipeToReplyNode @@ -3950,7 +3951,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode swipeToReplyNode.updateAbsoluteRect(mappedRect, within: containerSize) } - swipeToReplyNode.updateProgress(abs(translation.x) / swipeOffset) + let progress = abs(translation.x) / swipeOffset + swipeToReplyNode.updateProgress(progress) + + if progress == 1.0 && !self.playedSwipeToReplyHaptic { + self.swipeToReplyFeedback?.impact(.heavy) + } } case .cancelled, .ended: self.swipeToReplyFeedback = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift index facb0a018e..ae07c01bba 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift @@ -40,7 +40,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent } } - let title: String = item.presentationData.strings.Channel_AdminLog_MessagePreviousLink + let title: String = item.message.text.contains("\n") ? item.presentationData.strings.Channel_AdminLog_MessagePreviousLinks : item.presentationData.strings.Channel_AdminLog_MessagePreviousLink let text: String = item.message.text let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 9cfbbc2c6c..6b9346f53e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -983,84 +983,91 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } + private var playedSwipeToReplyHaptic = false @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + var offset: CGFloat = 0.0 + var swipeOffset: CGFloat = 45.0 + if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { + offset = -24.0 + } else { + offset = 10.0 + swipeOffset = 60.0 + } + switch recognizer.state { - case .began: - self.currentSwipeToReplyTranslation = 0.0 - if self.swipeToReplyFeedback == nil { - self.swipeToReplyFeedback = HapticFeedback() - self.swipeToReplyFeedback?.prepareImpact() - } - (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() - case .changed: - var translation = recognizer.translation(in: self.view) - translation.x = max(-80.0, min(0.0, translation.x)) - var animateReplyNodeIn = false - if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) { - if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { - self.swipeToReplyFeedback?.impact(.heavy) - + case .began: + self.playedSwipeToReplyHaptic = false + self.currentSwipeToReplyTranslation = 0.0 + if self.swipeToReplyFeedback == nil { + self.swipeToReplyFeedback = HapticFeedback() + self.swipeToReplyFeedback?.prepareImpact() + } + self.item?.controllerInteraction.cancelInteractiveKeyboardGestures() + case .changed: + var translation = recognizer.translation(in: self.view) + translation.x = max(-80.0, min(0.0, translation.x)) + + if let item = self.item, self.swipeToReplyNode == nil { let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), backgroundNode: item.controllerInteraction.presentationContext.backgroundNode, action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) self.swipeToReplyNode = swipeToReplyNode - self.addSubnode(swipeToReplyNode) - animateReplyNodeIn = true + self.insertSubnode(swipeToReplyNode, at: 0) } - } - self.currentSwipeToReplyTranslation = translation.x - var bounds = self.bounds - bounds.origin.x = -translation.x - self.bounds = bounds + + self.currentSwipeToReplyTranslation = translation.x + var bounds = self.bounds + bounds.origin.x = -translation.x + self.bounds = bounds - self.updateAttachedAvatarNodeOffset(offset: self.avatarOffset ?? translation.x, transition: .immediate) + self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) - if let swipeToReplyNode = self.swipeToReplyNode { - swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) - - if let (rect, containerSize) = self.absoluteRect { - let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) - swipeToReplyNode.updateAbsoluteRect(mappedRect, within: containerSize) + if let swipeToReplyNode = self.swipeToReplyNode { + swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width + offset, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) + + if let (rect, containerSize) = self.absoluteRect { + let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) + swipeToReplyNode.updateAbsoluteRect(mappedRect, within: containerSize) + } + + let progress = abs(translation.x) / swipeOffset + swipeToReplyNode.updateProgress(progress) + + if progress == 1.0 && !self.playedSwipeToReplyHaptic { + self.swipeToReplyFeedback?.impact(.heavy) + } } + case .cancelled, .ended: + self.swipeToReplyFeedback = nil - if animateReplyNodeIn { - swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) - swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) - } else { - swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0)) - } - } - case .cancelled, .ended: - self.swipeToReplyFeedback = nil - - let translation = recognizer.translation(in: self.view) - if case .ended = recognizer.state, translation.x < -45.0 { - if let item = self.item { - if let currentSwipeAction = currentSwipeAction { - switch currentSwipeAction { - case .none: - break - case .reply: - item.controllerInteraction.setupReply(item.message.id) + let translation = recognizer.translation(in: self.view) + if case .ended = recognizer.state, translation.x < -swipeOffset { + if let item = self.item { + if let currentSwipeAction = currentSwipeAction { + switch currentSwipeAction { + case .none: + break + case .reply: + item.controllerInteraction.setupReply(item.message.id) + } } } } - } - var bounds = self.bounds - let previousBounds = bounds - bounds.origin.x = 0.0 - self.bounds = bounds - self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.x = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.updateAttachedAvatarNodeOffset(offset: self.avatarOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .spring)) + self.updateAttachedAvatarNodeOffset(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) - if let swipeToReplyNode = self.swipeToReplyNode { - self.swipeToReplyNode = nil - swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in - swipeToReplyNode?.removeFromSupernode() - }) - swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } - default: - break + if let swipeToReplyNode = self.swipeToReplyNode { + self.swipeToReplyNode = nil + swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in + swipeToReplyNode?.removeFromSupernode() + }) + swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + default: + break } } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index c5d88c0204..aea7fe6b5f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -1235,56 +1235,63 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + private var playedSwipeToReplyHaptic = false @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + var offset: CGFloat = 0.0 + var swipeOffset: CGFloat = 45.0 + if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { + offset = -24.0 + } else { + offset = 10.0 + swipeOffset = 60.0 + } + switch recognizer.state { case .began: + self.playedSwipeToReplyHaptic = false self.currentSwipeToReplyTranslation = 0.0 if self.swipeToReplyFeedback == nil { self.swipeToReplyFeedback = HapticFeedback() self.swipeToReplyFeedback?.prepareImpact() } - (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + self.item?.controllerInteraction.cancelInteractiveKeyboardGestures() case .changed: var translation = recognizer.translation(in: self.view) translation.x = max(-80.0, min(0.0, translation.x)) - var animateReplyNodeIn = false - if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) { - if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { - self.swipeToReplyFeedback?.impact(.heavy) - - let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), backgroundNode: item.controllerInteraction.presentationContext.backgroundNode, action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) - self.swipeToReplyNode = swipeToReplyNode - self.addSubnode(swipeToReplyNode) - animateReplyNodeIn = true - } + + if let item = self.item, self.swipeToReplyNode == nil { + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), backgroundNode: item.controllerInteraction.presentationContext.backgroundNode, action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) + self.swipeToReplyNode = swipeToReplyNode + self.insertSubnode(swipeToReplyNode, at: 0) } + self.currentSwipeToReplyTranslation = translation.x var bounds = self.bounds bounds.origin.x = -translation.x self.bounds = bounds self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) - + if let swipeToReplyNode = self.swipeToReplyNode { - swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width + offset, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) if let (rect, containerSize) = self.absoluteRect { let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) swipeToReplyNode.updateAbsoluteRect(mappedRect, within: containerSize) } - if animateReplyNodeIn { - swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) - swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) - } else { - swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0)) + let progress = abs(translation.x) / swipeOffset + swipeToReplyNode.updateProgress(progress) + + if progress == 1.0 && !self.playedSwipeToReplyHaptic { + self.swipeToReplyFeedback?.impact(.heavy) } } case .cancelled, .ended: self.swipeToReplyFeedback = nil let translation = recognizer.translation(in: self.view) - if case .ended = recognizer.state, translation.x < -45.0 { + if case .ended = recognizer.state, translation.x < -swipeOffset { if let item = self.item { if let currentSwipeAction = currentSwipeAction { switch currentSwipeAction { diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 8394b9ac7e..da5ccba729 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -31,7 +31,7 @@ struct ChatRecentActionsEntryId: Hashable, Comparable { private func eventNeedsHeader(_ event: AdminLogEvent) -> Bool { switch event.action { - case .changeAbout, .changeUsername, .editMessage, .deleteMessage, .pollStopped, .sendMessage: + case .changeAbout, .changeUsername, .changeUsernames, .editMessage, .deleteMessage, .pollStopped, .sendMessage: return true case let .updatePinned(message): if message != nil { @@ -217,14 +217,14 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedChannelUsername(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedChannelUsernames(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in if index == 0, let author = author { return [.TextMention(peerId: author.id)] } return [] }, to: &text, entities: &entities) } else { - appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedGroupUsername(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedGroupUsernames(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in if index == 0, let author = author { return [.TextMention(peerId: author.id)] } @@ -237,14 +237,33 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { case .content: var previousAttributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = [] - - let prevText = "https://t.me/\(prev.first ?? "")" - previousAttributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< prevText.count, type: .Url)])) - - let text: String + + var prevTextEntities: [MessageTextEntity] = [] + var textEntities: [MessageTextEntity] = [] + + var prevText: String = "" + for username in prev { + let link = "https://t.me/\(username)" + prevTextEntities.append(MessageTextEntity(range: prevText.count ..< prevText.count + link.count, type: .Url)) + prevText.append(link) + prevText.append("\n") + } + prevText.removeLast() + if !prevTextEntities.isEmpty { + previousAttributes.append(TextEntitiesMessageAttribute(entities: prevTextEntities)) + } + var text: String = "" if !new.isEmpty { - text = "https://t.me/\(new.first ?? "")" - attributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< text.count, type: .Url)])) + for username in new { + let link = "https://t.me/\(username)" + textEntities.append(MessageTextEntity(range: text.count ..< text.count + link.count, type: .Url)) + text.append(link) + text.append("\n") + } + text.removeLast() + if !textEntities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: prevTextEntities)) + } } else { text = self.presentationData.strings.Channel_AdminLog_EmptyMessageText attributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< text.count, type: .Italic)])) diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index 57796fba8f..e716cad5c0 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -202,7 +202,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - interfaceInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + interfaceInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String @@ -211,7 +211,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont } else { text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string } - interfaceInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in + interfaceInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedGifs) interfaceInteraction?.getNavigationController()?.pushViewController(controller) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index a3f71c4922..bda75b9945 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3948,7 +3948,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: self.presentationData.strings.PeerInfo_TooltipUnmuted), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + ], title: nil, text: self.presentationData.strings.PeerInfo_TooltipUnmuted, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) } else { self.state = self.state.withHighlightedButton(.mute) if let (layout, navigationHeight) = self.validLayout { @@ -3990,7 +3990,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, muteInterval: value).start() - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_mute_for", scale: 0.066, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedFor(mutedForTimeIntervalString(strings: strongSelf.presentationData.strings, value: value)).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_mute_for", scale: 0.066, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedFor(mutedForTimeIntervalString(strings: strongSelf.presentationData.strings, value: value)).string, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }))) } @@ -4028,7 +4028,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, sound: .default).start() - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_sound_on", scale: 0.056, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipSoundEnabled), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_sound_on", scale: 0.056, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipSoundEnabled, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }))) } else { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.PeerInfo_DisableSound, icon: { theme in @@ -4041,7 +4041,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, sound: .none).start() - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_sound_off", scale: 0.056, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipSoundDisabled), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_sound_off", scale: 0.056, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipSoundDisabled, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }))) } @@ -4103,7 +4103,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedForever), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + ], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedForever, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) } }) }, updatePeerDisplayPreviews: { peerId, displayPreviews in @@ -4137,7 +4137,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedForever), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + ], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedForever, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }))) var tip: ContextController.Tip? @@ -4869,7 +4869,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let timeString = stringForPreciseRelativeTimestamp(strings: strongSelf.presentationData.strings, relativeTimestamp: Int32(Date().timeIntervalSince1970) + value, relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: strongSelf.presentationData.dateTimeFormat) - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_mute_for", scale: 0.056, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedUntil(timeString).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_mute_for", scale: 0.056, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedUntil(timeString).string, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) } }) self.controller?.view.endEditing(true) diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 0f38a3b402..eae12ddccf 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -41,7 +41,7 @@ public enum UndoOverlayContent { case inviteRequestSent(title: String, text: String) case image(image: UIImage, text: String) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) - case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String) + case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?) } public enum UndoOverlayAction { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 3f276511c5..370eba7436 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -824,7 +824,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } } } - case let .universal(animation, scale, colors, title, text): + case let .universal(animation, scale, colors, title, text, customUndoText): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -851,7 +851,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.originalRemainingSeconds = isUserInteractionEnabled ? 5 : 3 self.textNode.maximumNumberOfLines = 5 - displayUndo = false + + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } case let .image(image, text): self.avatarNode = nil self.iconNode = ASImageNode()