Merge commit '85ac817816cb8ed5afdd3314aea99ee19cc325f5'

This commit is contained in:
Ali 2022-10-17 17:16:54 +04:00
commit dec8a63809
19 changed files with 291 additions and 201 deletions

View File

@ -8134,5 +8134,11 @@ Sorry for the inconvenience.";
"Channel.EditAdmin.PermissionCreateTopics" = "Create Topics"; "Channel.EditAdmin.PermissionCreateTopics" = "Create Topics";
"ChatList.Search.FilterTopics" = "Topics"; "ChatList.Search.FilterTopics" = "Topics";
"DialogList.SearchSectionTopics" = "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";

View File

@ -1792,6 +1792,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self?.askForFilterRemoval(id: id) 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 let tabContextGesture: (Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool, Bool) -> Void = { [weak self] id, sourceNode, gesture, keepInPlace, isDisabled in
guard let strongSelf = self else { guard let strongSelf = self else {

View File

@ -11,6 +11,7 @@ import AccountContext
import ItemListPeerActionItem import ItemListPeerActionItem
import ChatListFilterSettingsHeaderItem import ChatListFilterSettingsHeaderItem
import PremiumUI import PremiumUI
import UndoUI
private final class ChatListFilterPresetListControllerArguments { private final class ChatListFilterPresetListControllerArguments {
let context: AccountContext let context: AccountContext
@ -223,7 +224,7 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
var folderCount = 0 var folderCount = 0
for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { 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)) 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 { if case let .filter(_, title, _, _) = filter {
@ -512,6 +513,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|> afterDisposed { |> afterDisposed {
} }
var previousOrder: [Int32]?
let controller = ItemListController(context: context, state: signal) let controller = ItemListController(context: context, state: signal)
controller.isOpaqueWhenInOverlay = true controller.isOpaqueWhenInOverlay = true
controller.blocksBackgroundWhenInOverlay = 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 return controller
} }

View File

@ -224,9 +224,7 @@ private final class ItemNode: ASDisplayNode {
self.selectionFraction = selectionFraction self.selectionFraction = selectionFraction
self.unreadCount = unreadCount self.unreadCount = unreadCount
transition.updateAlpha(node: self.containerNode, alpha: (isReordering && isNoFilter && !canReorderAllChats) ? 0.5 : 1.0)
if isReordering && !isNoFilter { if isReordering && !isNoFilter {
if self.deleteButtonNode == nil { if self.deleteButtonNode == nil {
let deleteButtonNode = ItemNodeDeleteButtonNode(pressed: { [weak self] in let deleteButtonNode = ItemNodeDeleteButtonNode(pressed: { [weak self] in
@ -275,7 +273,7 @@ private final class ItemNode: ASDisplayNode {
if self.isReordering != isReordering { if self.isReordering != isReordering {
self.isReordering = isReordering self.isReordering = isReordering
if self.isReordering && (!isNoFilter || canReorderAllChats) { if self.isReordering {
self.startShaking() self.startShaking()
} else { } else {
self.layer.removeAnimation(forKey: "shaking_position") self.layer.removeAnimation(forKey: "shaking_position")
@ -476,11 +474,13 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)?
var addFilter: (() -> Void)? var addFilter: (() -> Void)?
var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)?
var presentPremiumTip: (() -> Void)?
private var reorderingGesture: ReorderingGestureRecognizer? private var reorderingGesture: ReorderingGestureRecognizer?
private var reorderingItem: ChatListFilterTabEntryId? private var reorderingItem: ChatListFilterTabEntryId?
private var reorderingItemPosition: (initial: CGFloat, offset: CGFloat)? private var reorderingItemPosition: (initial: CGFloat, offset: CGFloat)?
private var reorderingAutoScrollAnimator: ConstantDisplayLinkAnimator? private var reorderingAutoScrollAnimator: ConstantDisplayLinkAnimator?
private var initialReorderedItemIds: [ChatListFilterTabEntryId]?
private var reorderedItemIds: [ChatListFilterTabEntryId]? private var reorderedItemIds: [ChatListFilterTabEntryId]?
private lazy var hapticFeedback = { HapticFeedback() }() private lazy var hapticFeedback = { HapticFeedback() }()
@ -539,11 +539,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
guard let strongSelf = self else { guard let strongSelf = self else {
return false 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 itemNode.view.convert(itemNode.bounds, to: strongSelf.view).contains(point) {
if case .all = id, !(strongSelf.currentParams?.canReorderAllChats ?? false) {
return false
}
return true return true
} }
} }
@ -552,6 +549,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
guard let strongSelf = self, let _ = strongSelf.currentParams else { guard let strongSelf = self, let _ = strongSelf.currentParams else {
return return
} }
strongSelf.initialReorderedItemIds = strongSelf.reorderedItemIds
for (id, itemNode) in strongSelf.itemNodes { for (id, itemNode) in strongSelf.itemNodes {
let itemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.view) let itemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.view)
if itemFrame.contains(point) { if itemFrame.contains(point) {
@ -594,6 +592,11 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
strongSelf.scrollNode.addSubnode(itemNode) 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.reorderingItem = nil
strongSelf.reorderingItemPosition = nil strongSelf.reorderingItemPosition = nil
strongSelf.reorderingAutoScrollAnimator?.invalidate() strongSelf.reorderingAutoScrollAnimator?.invalidate()
@ -606,7 +609,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
return 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) { 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 { for (id, itemNode) in strongSelf.itemNodes {
@ -927,13 +930,6 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
} else { } else {
transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) 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 { if let previousSelectedFrame = self.previousSelectedFrame {
let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0))) let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0)))

View File

@ -1285,7 +1285,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { result in
switch result { switch result {
case .generic: 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): case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String let text: String
@ -1294,7 +1294,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} else { } else {
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string 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 { if case .info = action {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs)
controllerInteraction?.pushController(controller) controllerInteraction?.pushController(controller)
@ -1489,7 +1489,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { result in
switch result { switch result {
case .generic: 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): case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String let text: String
@ -1498,7 +1498,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} else { } else {
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string 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 { if case .info = action {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs)
controllerInteraction?.pushController(controller) controllerInteraction?.pushController(controller)

File diff suppressed because one or more lines are too long

View File

@ -1855,7 +1855,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} }
switch result { switch result {
case .generic: 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): case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String let text: String
@ -1864,7 +1864,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} else { } else {
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string 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 { guard let strongSelf = self else {
return false return false
} }

View File

@ -1391,7 +1391,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
Queue.mainQueue().after(0.2) { Queue.mainQueue().after(0.2) {
switch result { switch result {
case .generic: 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): case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String let text: String
@ -1400,7 +1400,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} else { } else {
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string 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 { if case .info = action {
let controller = PremiumIntroScreen(context: context, source: .savedGifs) let controller = PremiumIntroScreen(context: context, source: .savedGifs)
controllerInteraction.navigationController()?.pushViewController(controller) controllerInteraction.navigationController()?.pushViewController(controller)

View File

@ -1535,7 +1535,7 @@ final class ChatMediaInputNode: ChatInputNode {
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { result in
switch result { switch result {
case .generic: 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): case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String let text: String
@ -1544,7 +1544,7 @@ final class ChatMediaInputNode: ChatInputNode {
} else { } else {
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string 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 { if case .info = action {
let controller = PremiumIntroScreen(context: context, source: .savedGifs) let controller = PremiumIntroScreen(context: context, source: .savedGifs)
controllerInteraction.navigationController()?.pushViewController(controller) controllerInteraction.navigationController()?.pushViewController(controller)

View File

@ -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 { switch recognizer.state {
case .began: case .began:
self.currentSwipeToReplyTranslation = 0.0 self.playedSwipeToReplyHaptic = false
if self.swipeToReplyFeedback == nil { self.currentSwipeToReplyTranslation = 0.0
self.swipeToReplyFeedback = HapticFeedback() if self.swipeToReplyFeedback == nil {
self.swipeToReplyFeedback?.prepareImpact() self.swipeToReplyFeedback = HapticFeedback()
} self.swipeToReplyFeedback?.prepareImpact()
(self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() }
case .changed: self.item?.controllerInteraction.cancelInteractiveKeyboardGestures()
var translation = recognizer.translation(in: self.view) case .changed:
translation.x = max(-80.0, min(0.0, translation.x)) var translation = recognizer.translation(in: self.view)
var animateReplyNodeIn = false translation.x = max(-80.0, min(0.0, translation.x))
if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) {
if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { if let item = self.item, self.swipeToReplyNode == nil {
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)) 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.swipeToReplyNode = swipeToReplyNode
self.addSubnode(swipeToReplyNode) self.insertSubnode(swipeToReplyNode, at: 0)
animateReplyNodeIn = true
} }
}
self.currentSwipeToReplyTranslation = translation.x self.currentSwipeToReplyTranslation = translation.x
var bounds = self.bounds var bounds = self.bounds
bounds.origin.x = -translation.x bounds.origin.x = -translation.x
self.bounds = bounds self.bounds = bounds
self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate)
if let swipeToReplyNode = self.swipeToReplyNode { 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 { 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) 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) 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 { let translation = recognizer.translation(in: self.view)
swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) if case .ended = recognizer.state, translation.x < -swipeOffset {
swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) if let item = self.item {
} else { if let currentSwipeAction = currentSwipeAction {
swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0)) switch currentSwipeAction {
} case .none:
} break
case .cancelled, .ended: case .reply:
self.swipeToReplyFeedback = nil item.controllerInteraction.setupReply(item.message.id)
}
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)
} }
} }
} }
} var bounds = self.bounds
var bounds = self.bounds let previousBounds = bounds
let previousBounds = bounds bounds.origin.x = 0.0
bounds.origin.x = 0.0 self.bounds = bounds
self.bounds = bounds self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
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 { if let swipeToReplyNode = self.swipeToReplyNode {
self.swipeToReplyNode = nil self.swipeToReplyNode = nil
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
swipeToReplyNode?.removeFromSupernode() swipeToReplyNode?.removeFromSupernode()
}) })
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
} }
default: default:
break break
} }
} }

View File

@ -3903,6 +3903,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
} }
private var playedSwipeToReplyHaptic = false
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0 var offset: CGFloat = 0.0
var swipeOffset: CGFloat = 45.0 var swipeOffset: CGFloat = 45.0
@ -3915,6 +3916,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
switch recognizer.state { switch recognizer.state {
case .began: case .began:
self.playedSwipeToReplyHaptic = false
self.currentSwipeToReplyTranslation = 0.0 self.currentSwipeToReplyTranslation = 0.0
if self.swipeToReplyFeedback == nil { if self.swipeToReplyFeedback == nil {
self.swipeToReplyFeedback = HapticFeedback() self.swipeToReplyFeedback = HapticFeedback()
@ -3925,7 +3927,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
var translation = recognizer.translation(in: self.view) var translation = recognizer.translation(in: self.view)
translation.x = max(-80.0, min(0.0, translation.x)) translation.x = max(-80.0, min(0.0, translation.x))
if let item = self.item, self.swipeToReplyNode == nil { 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)) 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.swipeToReplyNode = swipeToReplyNode
@ -3950,7 +3951,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
swipeToReplyNode.updateAbsoluteRect(mappedRect, within: containerSize) 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: case .cancelled, .ended:
self.swipeToReplyFeedback = nil self.swipeToReplyFeedback = nil

View File

@ -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 text: String = item.message.text
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil

View File

@ -983,84 +983,91 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
} }
} }
private var playedSwipeToReplyHaptic = false
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @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 { switch recognizer.state {
case .began: case .began:
self.currentSwipeToReplyTranslation = 0.0 self.playedSwipeToReplyHaptic = false
if self.swipeToReplyFeedback == nil { self.currentSwipeToReplyTranslation = 0.0
self.swipeToReplyFeedback = HapticFeedback() if self.swipeToReplyFeedback == nil {
self.swipeToReplyFeedback?.prepareImpact() self.swipeToReplyFeedback = HapticFeedback()
} self.swipeToReplyFeedback?.prepareImpact()
(self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() }
case .changed: self.item?.controllerInteraction.cancelInteractiveKeyboardGestures()
var translation = recognizer.translation(in: self.view) case .changed:
translation.x = max(-80.0, min(0.0, translation.x)) var translation = recognizer.translation(in: self.view)
var animateReplyNodeIn = false translation.x = max(-80.0, min(0.0, translation.x))
if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) {
if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { if let item = self.item, self.swipeToReplyNode == nil {
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)) 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.swipeToReplyNode = swipeToReplyNode
self.addSubnode(swipeToReplyNode) self.insertSubnode(swipeToReplyNode, at: 0)
animateReplyNodeIn = true
} }
}
self.currentSwipeToReplyTranslation = translation.x self.currentSwipeToReplyTranslation = translation.x
var bounds = self.bounds var bounds = self.bounds
bounds.origin.x = -translation.x bounds.origin.x = -translation.x
self.bounds = bounds self.bounds = bounds
self.updateAttachedAvatarNodeOffset(offset: self.avatarOffset ?? translation.x, transition: .immediate) self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate)
if let swipeToReplyNode = self.swipeToReplyNode { 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 { 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) 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) 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 { let translation = recognizer.translation(in: self.view)
swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) if case .ended = recognizer.state, translation.x < -swipeOffset {
swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) if let item = self.item {
} else { if let currentSwipeAction = currentSwipeAction {
swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0)) switch currentSwipeAction {
} case .none:
} break
case .cancelled, .ended: case .reply:
self.swipeToReplyFeedback = nil item.controllerInteraction.setupReply(item.message.id)
}
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)
} }
} }
} }
} var bounds = self.bounds
var bounds = self.bounds let previousBounds = bounds
let previousBounds = bounds bounds.origin.x = 0.0
bounds.origin.x = 0.0 self.bounds = bounds
self.bounds = bounds self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
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 { if let swipeToReplyNode = self.swipeToReplyNode {
self.swipeToReplyNode = nil self.swipeToReplyNode = nil
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
swipeToReplyNode?.removeFromSupernode() swipeToReplyNode?.removeFromSupernode()
}) })
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
} }
default: default:
break break
} }
} }

View File

@ -1235,56 +1235,63 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
} }
} }
private var playedSwipeToReplyHaptic = false
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @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 { switch recognizer.state {
case .began: case .began:
self.playedSwipeToReplyHaptic = false
self.currentSwipeToReplyTranslation = 0.0 self.currentSwipeToReplyTranslation = 0.0
if self.swipeToReplyFeedback == nil { if self.swipeToReplyFeedback == nil {
self.swipeToReplyFeedback = HapticFeedback() self.swipeToReplyFeedback = HapticFeedback()
self.swipeToReplyFeedback?.prepareImpact() self.swipeToReplyFeedback?.prepareImpact()
} }
(self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() self.item?.controllerInteraction.cancelInteractiveKeyboardGestures()
case .changed: case .changed:
var translation = recognizer.translation(in: self.view) var translation = recognizer.translation(in: self.view)
translation.x = max(-80.0, min(0.0, translation.x)) translation.x = max(-80.0, min(0.0, translation.x))
var animateReplyNodeIn = false
if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) { if let item = self.item, self.swipeToReplyNode == nil {
if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { 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.swipeToReplyFeedback?.impact(.heavy) self.swipeToReplyNode = swipeToReplyNode
self.insertSubnode(swipeToReplyNode, at: 0)
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.currentSwipeToReplyTranslation = translation.x self.currentSwipeToReplyTranslation = translation.x
var bounds = self.bounds var bounds = self.bounds
bounds.origin.x = -translation.x bounds.origin.x = -translation.x
self.bounds = bounds self.bounds = bounds
self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate)
if let swipeToReplyNode = self.swipeToReplyNode { 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 { 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) 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) swipeToReplyNode.updateAbsoluteRect(mappedRect, within: containerSize)
} }
if animateReplyNodeIn { let progress = abs(translation.x) / swipeOffset
swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) swipeToReplyNode.updateProgress(progress)
swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
} else { if progress == 1.0 && !self.playedSwipeToReplyHaptic {
swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0)) self.swipeToReplyFeedback?.impact(.heavy)
} }
} }
case .cancelled, .ended: case .cancelled, .ended:
self.swipeToReplyFeedback = nil self.swipeToReplyFeedback = nil
let translation = recognizer.translation(in: self.view) 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 item = self.item {
if let currentSwipeAction = currentSwipeAction { if let currentSwipeAction = currentSwipeAction {
switch currentSwipeAction { switch currentSwipeAction {

View File

@ -31,7 +31,7 @@ struct ChatRecentActionsEntryId: Hashable, Comparable {
private func eventNeedsHeader(_ event: AdminLogEvent) -> Bool { private func eventNeedsHeader(_ event: AdminLogEvent) -> Bool {
switch event.action { switch event.action {
case .changeAbout, .changeUsername, .editMessage, .deleteMessage, .pollStopped, .sendMessage: case .changeAbout, .changeUsername, .changeUsernames, .editMessage, .deleteMessage, .pollStopped, .sendMessage:
return true return true
case let .updatePinned(message): case let .updatePinned(message):
if message != nil { if message != nil {
@ -217,14 +217,14 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var text: String = "" var text: String = ""
var entities: [MessageTextEntity] = [] var entities: [MessageTextEntity] = []
if let peer = peer as? TelegramChannel, case .broadcast = peer.info { 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 { if index == 0, let author = author {
return [.TextMention(peerId: author.id)] return [.TextMention(peerId: author.id)]
} }
return [] return []
}, to: &text, entities: &entities) }, to: &text, entities: &entities)
} else { } 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 { if index == 0, let author = author {
return [.TextMention(peerId: author.id)] return [.TextMention(peerId: author.id)]
} }
@ -237,14 +237,33 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
case .content: case .content:
var previousAttributes: [MessageAttribute] = [] var previousAttributes: [MessageAttribute] = []
var attributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = []
let prevText = "https://t.me/\(prev.first ?? "")" var prevTextEntities: [MessageTextEntity] = []
previousAttributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< prevText.count, type: .Url)])) var textEntities: [MessageTextEntity] = []
let text: String 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 { if !new.isEmpty {
text = "https://t.me/\(new.first ?? "")" for username in new {
attributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< text.count, type: .Url)])) 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 { } else {
text = self.presentationData.strings.Channel_AdminLog_EmptyMessageText text = self.presentationData.strings.Channel_AdminLog_EmptyMessageText
attributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< text.count, type: .Italic)])) attributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< text.count, type: .Italic)]))

View File

@ -202,7 +202,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { result in
switch result { switch result {
case .generic: 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): case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String let text: String
@ -211,7 +211,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
} else { } else {
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string 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 { if case .info = action {
let controller = PremiumIntroScreen(context: context, source: .savedGifs) let controller = PremiumIntroScreen(context: context, source: .savedGifs)
interfaceInteraction?.getNavigationController()?.pushViewController(controller) interfaceInteraction?.getNavigationController()?.pushViewController(controller)

View File

@ -3948,7 +3948,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
"Bottom.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 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 { } else {
self.state = self.state.withHighlightedButton(.mute) self.state = self.state.withHighlightedButton(.mute)
if let (layout, navigationHeight) = self.validLayout { 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() 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() 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 { } else {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.PeerInfo_DisableSound, icon: { theme in 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() 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, "Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 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 }, updatePeerDisplayPreviews: { peerId, displayPreviews in
@ -4137,7 +4137,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
"Bottom.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 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? 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) 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) self.controller?.view.endEditing(true)

View File

@ -41,7 +41,7 @@ public enum UndoOverlayContent {
case inviteRequestSent(title: String, text: String) case inviteRequestSent(title: String, text: String)
case image(image: UIImage, text: String) case image(image: UIImage, text: String)
case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) 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 { public enum UndoOverlayAction {

View File

@ -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.avatarNode = nil
self.iconNode = nil self.iconNode = nil
self.iconCheckNode = nil self.iconCheckNode = nil
@ -851,7 +851,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.originalRemainingSeconds = isUserInteractionEnabled ? 5 : 3 self.originalRemainingSeconds = isUserInteractionEnabled ? 5 : 3
self.textNode.maximumNumberOfLines = 5 self.textNode.maximumNumberOfLines = 5
displayUndo = false
if let customUndoText = customUndoText {
undoText = customUndoText
displayUndo = true
} else {
displayUndo = false
}
case let .image(image, text): case let .image(image, text):
self.avatarNode = nil self.avatarNode = nil
self.iconNode = ASImageNode() self.iconNode = ASImageNode()