From 4b16494e2003930d8b49dcd33ec4a9ca91afac4a Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 28 Dec 2023 00:20:23 +0400 Subject: [PATCH] [WIP] Saved messages --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/ChatController.swift | 7 +- .../ChatListUI/Sources/ChatContextMenus.swift | 38 +++++ .../Sources/ChatListSearchContainerNode.swift | 33 ++++- .../Sources/Node/ChatListItem.swift | 47 +++--- .../Sources/Node/ChatListNode.swift | 133 ++++++++++------- .../DeleteChatPeerActionSheetItem/BUILD | 3 + .../DeleteChatPeerActionSheetItem.swift | 57 ++++++-- .../Display/Source/TooltipController.swift | 2 +- .../Sources/InstantPageControllerNode.swift | 16 +- .../FolderInviteLinkListController.swift | 20 ++- .../Sources/InviteLinkInviteController.swift | 16 +- .../Sources/InviteLinkListController.swift | 36 ++++- .../Sources/InviteLinkViewController.swift | 16 +- ...MessageHistorySavedMessagesIndexView.swift | 2 +- .../Sources/PremiumLimitScreen.swift | 17 +++ .../Sources/ChannelStatsController.swift | 13 +- submodules/TelegramApi/Sources/Api32.swift | 19 +++ .../TelegramCore/Sources/ForumChannels.swift | 78 ++++++---- ...gedCloudChatRemoveMessagesOperations.swift | 45 +++++- .../State/UserLimitsConfiguration.swift | 58 ++++---- .../Data/ConfigurationData.swift | 4 + .../TelegramEngine/Data/PeersData.swift | 1 + .../DeleteMessagesInteractively.swift | 13 +- .../Messages/TelegramEngineMessages.swift | 15 ++ .../Peers/TelegramEnginePeers.swift | 22 ++- .../TelegramNotices/Sources/Notices.swift | 31 ++++ .../Resources/PresentationResourceKey.swift | 1 + .../Resources/PresentationResourcesChat.swift | 22 +++ .../ChatMessageAnimatedStickerItemNode.swift | 4 +- .../Sources/ChatMessageBubbleItemNode.swift | 4 +- .../ChatMessageInstantVideoItemNode.swift | 4 +- .../Sources/ChatMessageItem.swift | 2 +- .../Sources/ChatMessageDateHeader.swift | 28 +++- .../Sources/ChatMessageItemImpl.swift | 23 ++- .../Sources/ChatMessageItemView.swift | 14 +- .../Sources/ChatMessageStickerItemNode.swift | 4 +- .../Sources/ChatControllerInteraction.swift | 1 + .../PeerInfo/PeerInfoChatListPaneNode/BUILD | 2 + .../Sources/PeerInfoChatListPaneNode.swift | 138 ++++++++++++++++++ .../Sources/PeerInfoChatPaneNode.swift | 10 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 77 ++++++++-- .../Sources/PeerInfoPaneContainerNode.swift | 6 + .../Sources/PeerInfoScreen.swift | 80 +++++++++- ...StoryItemSetContainerViewSendMessage.swift | 17 ++- .../TelegramUI/Sources/ChatController.swift | 82 ++++++++++- .../Sources/ChatControllerNode.swift | 43 +++++- .../Sources/ChatHistoryListNode.swift | 41 ++++-- .../ChatHistoryNavigationButtonNode.swift | 5 + .../ChatHistoryNavigationButtons.swift | 20 ++- .../OverlayAudioPlayerController.swift | 16 +- .../TooltipUI/Sources/TooltipScreen.swift | 3 + .../Sources/UndoOverlayControllerNode.swift | 11 +- 53 files changed, 1164 insertions(+), 238 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 41ec4bb65f..3b083c16a7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10839,3 +10839,5 @@ Sorry for the inconvenience."; "RequestPeer.SelectUsers.SearchPlaceholder" = "Search"; "RequestPeer.ReachedMaximum_1" = "You can select up to %@ user."; "RequestPeer.ReachedMaximum_any" = "You can select up to %@ users."; + +"ChatList.DeleteSavedPeerConfirmation" = "Are you sure you want to delete saved messages from %@?"; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 5e652529b5..5aef007cbd 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -758,10 +758,10 @@ public enum ChatControllerSubject: Equatable { } public enum ChatControllerPresentationMode: Equatable { - public enum StandardPresentation { + public enum StandardPresentation: Equatable { case `default` case previewing - case embedded + case embedded(invertDirection: Bool) } case standard(StandardPresentation) @@ -912,6 +912,9 @@ public protocol ChatController: ViewController { func cancelSelectingMessages() func activateSearch(domain: ChatSearchDomain, query: String) func beginClearHistory(type: InteractiveHistoryClearingType) + + func transferScrollingVelocity(_ velocity: CGFloat) + func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) } public protocol ChatMessagePreviewItemNode: AnyObject { diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 233e5db0fc..890c78ec2c 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -854,6 +854,44 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: } } +public func savedMessagesPeerMenuItems(context: AccountContext, threadId: Int64, parentController: ViewController) -> Signal<[ContextMenuItem], NoError> { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + let strings = presentationData.strings + + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: PeerId(threadId)) + ), + context.account.postbox.transaction { transaction -> [Int64] in + return transaction.getPeerPinnedThreads(peerId: context.account.peerId) + } + ) + |> mapToSignal { [weak parentController] peer, pinnedThreadIds -> Signal<[ContextMenuItem], NoError> in + var items: [ContextMenuItem] = [] + + let isPinned = pinnedThreadIds.contains(threadId) + + items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: context.account.peerId, threadId: threadId) + |> deliverOnMainQueue).startStandalone(error: { error in + switch error { + case let .limitReached(count): + let controller = PremiumLimitScreen(context: context, subject: .pinnedSavedPeers, count: Int32(count), action: { + return true + }) + parentController?.push(controller) + default: + break + } + }) + }))) + + return .single(items) + } +} + private func openCustomMute(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, baseController: ViewController) { let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak baseController] value in let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 86107ee921..ab3e61c6f6 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -1421,7 +1421,21 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } - (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) } peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer, threadId in let peerId = peer.id @@ -1432,7 +1446,22 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + + return false + }), in: .window(.root)) let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in return .forward(source: id, threadId: threadId, grouping: .auto, attributes: [], correlationId: nil) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index bc6907c832..0ffbd787c6 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -379,7 +379,13 @@ public struct ChatListItemFilterData: Equatable { private func revealOptions(strings: PresentationStrings, theme: PresentationTheme, isPinned: Bool, isMuted: Bool?, location: ChatListControllerLocation, peerId: EnginePeer.Id, accountPeerId: EnginePeer.Id, canDelete: Bool, isEditing: Bool, filterData: ChatListItemFilterData?) -> [ItemListRevealOption] { var options: [ItemListRevealOption] = [] if !isEditing { - if case .chatList(.archive) = location { + if case .savedMessagesChats = location { + if isPinned { + options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + } else { + options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + } + } else if case .chatList(.archive) = location { if isPinned { options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) } else { @@ -398,28 +404,31 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem if canDelete { options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor)) } - if !isEditing { - var canArchive = false - var canUnarchive = false - if let filterData = filterData { - if filterData.excludesArchived { - canArchive = true - } - } else { - if case let .chatList(groupId) = location { - if case .root = groupId { + if case .savedMessagesChats = location { + } else { + if !isEditing { + var canArchive = false + var canUnarchive = false + if let filterData = filterData { + if filterData.excludesArchived { canArchive = true - } else { - canUnarchive = true + } + } else { + if case let .chatList(groupId) = location { + if case .root = groupId { + canArchive = true + } else { + canUnarchive = true + } } } - } - if canArchive { - if canArchivePeer(id: peerId, accountPeerId: accountPeerId) { - options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) + if canArchive { + if canArchivePeer(id: peerId, accountPeerId: accountPeerId) { + options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) + } + } else if canUnarchive { + options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) } - } else if canUnarchive { - options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) } } return options diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 44aa7d5a20..9c74232736 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -22,6 +22,7 @@ import StoryContainerScreen import ChatListHeaderComponent import UndoUI import NewSessionInfoScreen +import PresentationDataUtils public enum ChatListNodeMode { case chatList(appendContacts: Bool) @@ -1420,70 +1421,90 @@ public final class ChatListNode: ListView { } } }, setItemPinned: { [weak self] itemId, _ in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let strongSelf = self else { - return - } - guard case let .chatList(groupId) = strongSelf.location else { - return - } - - let isPremium = peer?.isPremium ?? false - let location: TogglePeerChatPinnedLocation - if let chatListFilter = chatListFilter { - location = .filter(chatListFilter.id) - } else { - location = .group(groupId._asGroup()) - } - let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: itemId) - |> deliverOnMainQueue).startStandalone(next: { result in - if let strongSelf = self { - switch result { - case .done: + if case .savedMessagesChats = location { + if case let .peer(itemPeerId) = itemId { + let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: context.account.peerId, threadId: itemPeerId.toInt64()) + |> deliverOnMainQueue).start(error: { error in + guard let self else { + return + } + switch error { + case let .limitReached(count): + let controller = PremiumLimitScreen(context: context, subject: .pinnedSavedPeers, count: Int32(count), action: { + return true + }) + self.push?(controller) + default: break - case let .limitExceeded(count, _): - if isPremium { - if case .filter = location { - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - return true - }) - strongSelf.push?(controller) - } else { - let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { - return true - }) - strongSelf.push?(controller) - } - } else { - if case .filter = location { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) - replaceImpl?(premiumScreen) - return true - }) - strongSelf.push?(controller) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + } + }) + } + } else { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let strongSelf = self else { + return + } + guard case let .chatList(groupId) = strongSelf.location else { + return + } + + let isPremium = peer?.isPremium ?? false + let location: TogglePeerChatPinnedLocation + if let chatListFilter = chatListFilter { + location = .filter(chatListFilter.id) + } else { + location = .group(groupId._asGroup()) + } + let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: itemId) + |> deliverOnMainQueue).startStandalone(next: { result in + if let strongSelf = self { + switch result { + case .done: + break + case let .limitExceeded(count, _): + if isPremium { + if case .filter = location { + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + return true + }) + strongSelf.push?(controller) + } else { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { + return true + }) + strongSelf.push?(controller) } } else { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { - let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) - replaceImpl?(premiumScreen) - return true - }) - strongSelf.push?(controller) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + if case .filter = location { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) + replaceImpl?(premiumScreen) + return true + }) + strongSelf.push?(controller) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + } else { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { + let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) + replaceImpl?(premiumScreen) + return true + }) + strongSelf.push?(controller) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } } } } } - } + }) }) - }) + } }, setPeerMuted: { [weak self] peerId, _ in guard let strongSelf = self else { return diff --git a/submodules/DeleteChatPeerActionSheetItem/BUILD b/submodules/DeleteChatPeerActionSheetItem/BUILD index ca2b91ed1b..40f03aff21 100644 --- a/submodules/DeleteChatPeerActionSheetItem/BUILD +++ b/submodules/DeleteChatPeerActionSheetItem/BUILD @@ -16,6 +16,9 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/AvatarNode:AvatarNode", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift index 90cc87378e..8544a58f63 100644 --- a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift +++ b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift @@ -6,6 +6,9 @@ import TelegramPresentationData import TelegramUIPreferences import AvatarNode import AccountContext +import ComponentFlow +import BalancedTextComponent +import MultilineTextComponent public enum DeleteChatPeerAction { case delete @@ -15,6 +18,7 @@ public enum DeleteChatPeerAction { case clearCacheSuggestion case removeFromGroup case removeFromChannel + case deleteSavedPeer } private let avatarFont = avatarPlaceholderFont(size: 26.0) @@ -26,18 +30,20 @@ public final class DeleteChatPeerActionSheetItem: ActionSheetItem { let action: DeleteChatPeerAction let strings: PresentationStrings let nameDisplayOrder: PresentationPersonNameOrder + let balancedLayout: Bool - public init(context: AccountContext, peer: EnginePeer, chatPeer: EnginePeer, action: DeleteChatPeerAction, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { + public init(context: AccountContext, peer: EnginePeer, chatPeer: EnginePeer, action: DeleteChatPeerAction, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, balancedLayout: Bool = false) { self.context = context self.peer = peer self.chatPeer = chatPeer self.action = action self.strings = strings self.nameDisplayOrder = nameDisplayOrder + self.balancedLayout = balancedLayout } public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return DeleteChatPeerActionSheetItemNode(theme: theme, strings: self.strings, nameOrder: self.nameDisplayOrder, context: self.context, peer: self.peer, chatPeer: self.chatPeer, action: self.action) + return DeleteChatPeerActionSheetItemNode(theme: theme, strings: self.strings, nameOrder: self.nameDisplayOrder, context: self.context, peer: self.peer, chatPeer: self.chatPeer, action: self.action, balancedLayout: self.balancedLayout) } public func updateNode(_ node: ActionSheetItemNode) { @@ -47,15 +53,19 @@ public final class DeleteChatPeerActionSheetItem: ActionSheetItem { private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme private let strings: PresentationStrings + private let balancedLayout: Bool private let avatarNode: AvatarNode - private let textNode: ImmediateTextNode + + private var text: NSAttributedString? + private let textView = ComponentView() private let accessibilityArea: AccessibilityAreaNode - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, nameOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, chatPeer: EnginePeer, action: DeleteChatPeerAction) { + init(theme: ActionSheetControllerTheme, strings: PresentationStrings, nameOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, chatPeer: EnginePeer, action: DeleteChatPeerAction, balancedLayout: Bool) { self.theme = theme self.strings = strings + self.balancedLayout = balancedLayout let textFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0)) let boldFont = Font.semibold(floor(theme.baseFontSize * 14.0 / 17.0)) @@ -63,24 +73,19 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isAccessibilityElement = false - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.maximumNumberOfLines = 0 - self.textNode.textAlignment = .center - self.textNode.isAccessibilityElement = false - self.accessibilityArea = AccessibilityAreaNode() super.init(theme: theme) self.addSubnode(self.avatarNode) - self.addSubnode(self.textNode) self.addSubnode(self.accessibilityArea) if chatPeer.id == context.account.peerId { self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .savedMessagesIcon) } else if chatPeer.id.isReplies { self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .repliesIcon) + } else if chatPeer.id.isAnonymousSavedMessages { + self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .anonymousSavedMessagesIcon) } else { var overrideImage: AvatarNodeImageOverride? if chatPeer.isDeleted { @@ -127,6 +132,10 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } else { text = strings.ChatList_DeleteChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder)) } + case .deleteSavedPeer: + //TODO:localize + let peerTitle = peer.displayTitle(strings: strings, displayOrder: nameOrder) + text = strings.ChatList_DeleteSavedPeerConfirmation(peerTitle) case let .clearHistory(canClearCache): if peer.id == context.account.peerId { text = PresentationStrings.FormattedString(string: strings.ChatList_ClearSavedMessagesConfirmation, ranges: []) @@ -162,7 +171,7 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } if let attributedText = attributedText { - self.textNode.attributedText = attributedText + self.text = attributedText self.accessibilityArea.accessibilityLabel = attributedText.string self.accessibilityArea.accessibilityTraits = .staticText @@ -170,7 +179,21 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let textSize = self.textNode.updateLayout(CGSize(width: constrainedSize.width - 20.0, height: .greatestFiniteMagnitude)) + let textComponent: AnyComponent + if self.balancedLayout { + textComponent = AnyComponent(BalancedTextComponent( + text: .plain(self.text ?? NSAttributedString()), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )) + } else { + textComponent = AnyComponent(MultilineTextComponent( + text: .plain(self.text ?? NSAttributedString()), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )) + } + let textSize = self.textView.update(transition: .immediate, component: textComponent, environment: {}, containerSize: CGSize(width: constrainedSize.width - 20.0, height: 1000.0)) let topInset: CGFloat = 16.0 let avatarSize: CGFloat = 60.0 @@ -178,7 +201,13 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { let bottomInset: CGFloat = 15.0 self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - avatarSize) / 2.0), y: topInset), size: CGSize(width: avatarSize, height: avatarSize)) - self.textNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - textSize.width) / 2.0), y: topInset + avatarSize + textSpacing), size: textSize) + + if let textComponentView = self.textView.view { + if textComponentView.superview == nil { + self.view.addSubview(textComponentView) + } + textComponentView.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - textSize.width) / 2.0), y: topInset + avatarSize + textSpacing), size: textSize) + } let size = CGSize(width: constrainedSize.width, height: topInset + avatarSize + textSpacing + textSize.height + bottomInset) self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/Display/Source/TooltipController.swift b/submodules/Display/Source/TooltipController.swift index 3cd74e0f20..ca34cecec9 100644 --- a/submodules/Display/Source/TooltipController.swift +++ b/submodules/Display/Source/TooltipController.swift @@ -178,7 +178,7 @@ open class TooltipController: ViewController, StandalonePresentableController { override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - if self.layout != nil && self.layout! != layout { + if self.layout != nil && self.layout!.size != layout.size { if self.dismissImmediatelyOnLayoutUpdate { self.dismissImmediately() } else { diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 5f19df4094..885bc1ae4f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -184,7 +184,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { text = "" } } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.getNavigationController() else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), nil) } }) } diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index f2e72ec1f9..ab449349a4 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -311,6 +311,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)? + var navigationController: (() -> NavigationController?)? var dismissTooltipsImpl: (() -> Void)? @@ -385,7 +386,21 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + guard let navigationController = navigationController?() else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + }) + } + return false + }), nil) }) } shareController.actionCompleted = { @@ -746,6 +761,9 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese return true } } + navigationController = { [weak controller] in + return controller?.navigationController as? NavigationController + } pushControllerImpl = { [weak controller] c in if let controller = controller { (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index 59a906df9f..e6cc1b8796 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -466,7 +466,21 @@ public final class InviteLinkInviteController: ViewController { } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .window(.root)) } }) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 62f57045e6..85d7c95fff 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -397,6 +397,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)? + var navigationController: (() -> NavigationController?)? var dismissTooltipsImpl: (() -> Void)? @@ -463,7 +464,21 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio } } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + guard let navigationController = navigationController?() else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + }) + } + return false + }), nil) }) } shareController.actionCompleted = { @@ -665,7 +680,21 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio } } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + guard let navigationController = navigationController?() else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + }) + } + return false + }), nil) }) } shareController.actionCompleted = { @@ -925,6 +954,9 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) } } + navigationController = { [weak controller] in + return controller?.navigationController as? NavigationController + } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index ba8fe938f5..f05988d70d 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -536,7 +536,21 @@ public final class InviteLinkViewController: ViewController { } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .window(.root)) } }) } diff --git a/submodules/Postbox/Sources/MessageHistorySavedMessagesIndexView.swift b/submodules/Postbox/Sources/MessageHistorySavedMessagesIndexView.swift index bc96d4c7e9..61a46b3577 100644 --- a/submodules/Postbox/Sources/MessageHistorySavedMessagesIndexView.swift +++ b/submodules/Postbox/Sources/MessageHistorySavedMessagesIndexView.swift @@ -40,7 +40,7 @@ final class MutableMessageHistorySavedMessagesIndexView: MutablePostboxView { self.peer = postbox.peerTable.get(self.peerId) - let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: peerId)?.validIndexBoundary + let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: self.peerId)?.validIndexBoundary self.isLoading = validIndexBoundary == nil if let validIndexBoundary = validIndexBoundary { diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 29c1e1dfbc..a1d53d53f2 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -1021,6 +1021,22 @@ private final class LimitSheetContent: CombinedComponent { badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) badgeGraphPosition = badgePosition + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string + } + case .pinnedSavedPeers: + //TODO:localize + let limit = state.limits.maxPinnedSavedChatCount + let premiumLimit = state.premiumLimits.maxPinnedSavedChatCount + iconName = "Premium/Pin" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition + if isPremiumDisabled { badgeText = "\(limit)" string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string @@ -1771,6 +1787,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case folders case chatsPerFolder case pins + case pinnedSavedPeers case files case accounts case linksPerSharedFolder diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index d67b8c8879..00681b36b5 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1147,7 +1147,18 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } } - presentImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })) + presentImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + navigateToChatImpl?(peer) + }) + } + return false + })) }) } shareController.actionCompleted = { diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index ed20259b19..6983f44182 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -4583,6 +4583,25 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func deleteSavedHistory(flags: Int32, peer: Api.InputPeer, maxId: Int32, minDate: Int32?, maxDate: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1855459371) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(maxId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(minDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(maxDate!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.deleteSavedHistory", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("maxId", String(describing: maxId)), ("minDate", String(describing: minDate)), ("maxDate", String(describing: maxDate))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AffectedHistory? in + let reader = BufferReader(buffer) + var result: Api.messages.AffectedHistory? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.AffectedHistory + } + return result + }) + } +} public extension Api.functions.messages { static func deleteScheduledMessages(peer: Api.InputPeer, id: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index 2978d0ec2d..97993eb59b 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -458,33 +458,51 @@ public enum SetForumChannelTopicPinnedError { } func _internal_setForumChannelPinnedTopics(account: Account, id: EnginePeer.Id, threadIds: [Int64]) -> Signal { - return account.postbox.transaction { transaction -> Api.InputChannel? in - guard let inputChannel = transaction.getPeer(id).flatMap(apiInputChannel) else { - return nil - } - - transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds) - - return inputChannel - } - |> castError(SetForumChannelTopicPinnedError.self) - |> mapToSignal { inputChannel -> Signal in - guard let inputChannel = inputChannel else { - return .fail(.generic) - } - - return account.network.request(Api.functions.channels.reorderPinnedForumTopics( - flags: 1 << 0, - channel: inputChannel, - order: threadIds.map(Int32.init(clamping:)) - )) - |> mapError { _ -> SetForumChannelTopicPinnedError in - return .generic - } - |> mapToSignal { result -> Signal in - account.stateManager.addUpdates(result) + if id == account.peerId { + return account.postbox.transaction { transaction -> [Api.InputDialogPeer] in + transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds) - return .complete() + return threadIds.compactMap { transaction.getPeer(PeerId($0)).flatMap(apiInputPeer).flatMap({ .inputDialogPeer(peer: $0) }) } + } + |> castError(SetForumChannelTopicPinnedError.self) + |> mapToSignal { inputPeers -> Signal in + return account.network.request(Api.functions.messages.reorderPinnedSavedDialogs(flags: 1 << 0, order: inputPeers)) + |> mapError { _ -> SetForumChannelTopicPinnedError in + return .generic + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + } else { + return account.postbox.transaction { transaction -> Api.InputChannel? in + guard let inputChannel = transaction.getPeer(id).flatMap(apiInputChannel) else { + return nil + } + + transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds) + + return inputChannel + } + |> castError(SetForumChannelTopicPinnedError.self) + |> mapToSignal { inputChannel -> Signal in + guard let inputChannel = inputChannel else { + return .fail(.generic) + } + + return account.network.request(Api.functions.channels.reorderPinnedForumTopics( + flags: 1 << 0, + channel: inputChannel, + order: threadIds.map(Int32.init(clamping:)) + )) + |> mapError { _ -> SetForumChannelTopicPinnedError in + return .generic + } + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return .complete() + } } } } @@ -687,8 +705,12 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post limit: Int32(limit), hash: 0 )) - |> mapError { _ -> LoadMessageHistoryThreadsError in - return .generic + |> `catch` { error -> Signal in + if error.errorDescription == "SAVED_DIALOGS_UNSUPPORTED" { + return .never() + } else { + return .fail(.generic) + } } |> mapToSignal { result -> Signal in switch result { diff --git a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift index 8e7963bf1e..1e4572414b 100644 --- a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift @@ -387,7 +387,50 @@ private func requestClearHistory(postbox: Postbox, network: Network, stateManage private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatClearHistoryOperation) -> Signal { if peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser { if let inputPeer = apiInputPeer(peer) { - return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, minTimestamp: operation.minTimestamp, maxTimestamp: operation.maxTimestamp, type: operation.type) + if peer.id == stateManager.accountPeerId, let threadId = operation.threadId { + guard let inputSubPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) else { + return .complete() + } + + var flags: Int32 = 0 + var updatedMaxId = operation.topMessageId.id + if operation.minTimestamp != nil { + flags |= 1 << 2 + updatedMaxId = 0 + } + if operation.maxTimestamp != nil { + flags |= 1 << 3 + updatedMaxId = 0 + } + let signal = network.request(Api.functions.messages.deleteSavedHistory(flags: flags, peer: inputSubPeer, maxId: updatedMaxId, minDate: operation.minTimestamp, maxDate: operation.maxTimestamp)) + |> map { result -> Api.messages.AffectedHistory? in + return result + } + |> `catch` { _ -> Signal in + return .fail(true) + } + |> mapToSignal { result -> Signal in + if let result = result { + switch result { + case let .affectedHistory(pts, ptsCount, offset): + stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)]) + if offset == 0 { + return .fail(true) + } else { + return .complete() + } + } + } else { + return .fail(true) + } + } + return (signal |> restart) + |> `catch` { _ -> Signal in + return .complete() + } + } else { + return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, minTimestamp: operation.minTimestamp, maxTimestamp: operation.maxTimestamp, type: operation.type) + } } else { return .complete() } diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index d833c3e2a9..060f3d50f6 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -2,34 +2,36 @@ import Postbox import SwiftSignalKit public struct UserLimitsConfiguration: Equatable { - public let maxPinnedChatCount: Int32 - public let maxArchivedPinnedChatCount: Int32 - public let maxChannelsCount: Int32 - public let maxPublicLinksCount: Int32 - public let maxSavedGifCount: Int32 - public let maxFavedStickerCount: Int32 - public let maxFoldersCount: Int32 - public let maxFolderChatsCount: Int32 - public let maxCaptionLength: Int32 - public let maxUploadFileParts: Int32 - public let maxAboutLength: Int32 - public let maxAnimatedEmojisInText: Int32 - public let maxReactionsPerMessage: Int32 - public let maxSharedFolderInviteLinks: Int32 - public let maxSharedFolderJoin: Int32 - public let maxStoryCaptionLength: Int32 - public let maxExpiringStoriesCount: Int32 - public let maxStoriesWeeklyCount: Int32 - public let maxStoriesMonthlyCount: Int32 - public let maxStoriesSuggestedReactions: Int32 - public let maxGiveawayChannelsCount: Int32 - public let maxGiveawayCountriesCount: Int32 - public let maxGiveawayPeriodSeconds: Int32 - public let maxChannelRecommendationsCount: Int32 + public var maxPinnedChatCount: Int32 + public var maxPinnedSavedChatCount: Int32 + public var maxArchivedPinnedChatCount: Int32 + public var maxChannelsCount: Int32 + public var maxPublicLinksCount: Int32 + public var maxSavedGifCount: Int32 + public var maxFavedStickerCount: Int32 + public var maxFoldersCount: Int32 + public var maxFolderChatsCount: Int32 + public var maxCaptionLength: Int32 + public var maxUploadFileParts: Int32 + public var maxAboutLength: Int32 + public var maxAnimatedEmojisInText: Int32 + public var maxReactionsPerMessage: Int32 + public var maxSharedFolderInviteLinks: Int32 + public var maxSharedFolderJoin: Int32 + public var maxStoryCaptionLength: Int32 + public var maxExpiringStoriesCount: Int32 + public var maxStoriesWeeklyCount: Int32 + public var maxStoriesMonthlyCount: Int32 + public var maxStoriesSuggestedReactions: Int32 + public var maxGiveawayChannelsCount: Int32 + public var maxGiveawayCountriesCount: Int32 + public var maxGiveawayPeriodSeconds: Int32 + public var maxChannelRecommendationsCount: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( maxPinnedChatCount: 5, + maxPinnedSavedChatCount: 5, maxArchivedPinnedChatCount: 100, maxChannelsCount: 500, maxPublicLinksCount: 10, @@ -58,6 +60,7 @@ public struct UserLimitsConfiguration: Equatable { public init( maxPinnedChatCount: Int32, + maxPinnedSavedChatCount: Int32, maxArchivedPinnedChatCount: Int32, maxChannelsCount: Int32, maxPublicLinksCount: Int32, @@ -83,6 +86,7 @@ public struct UserLimitsConfiguration: Equatable { maxChannelRecommendationsCount: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount + self.maxPinnedSavedChatCount = maxPinnedSavedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount self.maxChannelsCount = maxChannelsCount self.maxPublicLinksCount = maxPublicLinksCount @@ -112,7 +116,10 @@ public struct UserLimitsConfiguration: Equatable { extension UserLimitsConfiguration { init(appConfiguration: AppConfiguration, isPremium: Bool) { let keySuffix = isPremium ? "_premium" : "_default" - let defaultValue = UserLimitsConfiguration.defaultValue + var defaultValue = UserLimitsConfiguration.defaultValue + if isPremium { + defaultValue.maxPinnedSavedChatCount = 100 + } func getValue(_ key: String, orElse defaultValue: Int32) -> Int32 { if let value = appConfiguration.data?[key + keySuffix] as? Double { @@ -131,6 +138,7 @@ extension UserLimitsConfiguration { } self.maxPinnedChatCount = getValue("dialogs_pinned_limit", orElse: defaultValue.maxPinnedChatCount) + self.maxPinnedSavedChatCount = getValue("saved_pinned_limit", orElse: defaultValue.maxPinnedSavedChatCount) self.maxArchivedPinnedChatCount = getValue("dialogs_folder_pinned_limit", orElse: defaultValue.maxArchivedPinnedChatCount) self.maxChannelsCount = getValue("channels_limit", orElse: defaultValue.maxChannelsCount) self.maxPublicLinksCount = getValue("channels_public_limit", orElse: defaultValue.maxPublicLinksCount) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 03845767f6..8107e653cd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -37,6 +37,7 @@ public enum EngineConfiguration { public struct UserLimits: Equatable { public let maxPinnedChatCount: Int32 + public let maxPinnedSavedChatCount: Int32 public let maxArchivedPinnedChatCount: Int32 public let maxChannelsCount: Int32 public let maxPublicLinksCount: Int32 @@ -67,6 +68,7 @@ public enum EngineConfiguration { public init( maxPinnedChatCount: Int32, + maxPinnedSavedChatCount: Int32, maxArchivedPinnedChatCount: Int32, maxChannelsCount: Int32, maxPublicLinksCount: Int32, @@ -92,6 +94,7 @@ public enum EngineConfiguration { maxChannelRecommendationsCount: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount + self.maxPinnedSavedChatCount = maxPinnedSavedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount self.maxChannelsCount = maxChannelsCount self.maxPublicLinksCount = maxPublicLinksCount @@ -153,6 +156,7 @@ public extension EngineConfiguration.UserLimits { init(_ userLimitsConfiguration: UserLimitsConfiguration) { self.init( maxPinnedChatCount: userLimitsConfiguration.maxPinnedChatCount, + maxPinnedSavedChatCount: userLimitsConfiguration.maxPinnedSavedChatCount, maxArchivedPinnedChatCount: userLimitsConfiguration.maxArchivedPinnedChatCount, maxChannelsCount: userLimitsConfiguration.maxChannelsCount, maxPublicLinksCount: userLimitsConfiguration.maxPublicLinksCount, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index fbe82f18c4..39b69aeb03 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1153,5 +1153,6 @@ public extension TelegramEngine.EngineData.Item { } } } + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift index 18a38af631..12e0fa696a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift @@ -148,7 +148,18 @@ func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threa } if let topIndex = topIndex { if peerId.namespace == Namespaces.Peer.CloudUser { - let _ = transaction.addMessages([StoreMessage(id: topIndex.id, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: topIndex.timestamp, flags: StoreMessageFlags(), tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: nil, text: "", attributes: [], media: [TelegramMediaAction(action: .historyCleared)])], location: .Random) + var addEmptyMessage = false + if threadId == nil { + addEmptyMessage = true + } else { + if transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) == nil { + addEmptyMessage = true + } + } + + if addEmptyMessage { + let _ = transaction.addMessages([StoreMessage(id: topIndex.id, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: topIndex.timestamp, flags: StoreMessageFlags(), tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: nil, text: "", attributes: [], media: [TelegramMediaAction(action: .historyCleared)])], location: .Random) + } } else { updatePeerChatInclusionWithMinTimestamp(transaction: transaction, id: peerId, minTimestamp: topIndex.timestamp, forceRootGroupIfNotExists: false) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 8db4bf2727..393f752d4c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1314,6 +1314,21 @@ public extension TelegramEngine { return self.account.stateManager.synchronouslyIsMessageDeletedInteractively(ids: ids) } + public func savedMessagesPeerListHead() -> Signal { + return self.account.postbox.combinedView(keys: [.savedMessagesIndex(peerId: self.account.peerId)]) + |> map { views -> EnginePeer.Id? in + //TODO:api optimize + guard let view = views.views[.savedMessagesIndex(peerId: self.account.peerId)] as? MessageHistorySavedMessagesIndexView else { + return nil + } + if view.isLoading { + return nil + } else { + return view.items.first?.peer?.id + } + } + } + public func savedMessagesPeersStats() -> Signal { return self.account.postbox.combinedView(keys: [.savedMessagesStats(peerId: self.account.peerId)]) |> map { views -> Int? in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 1d684e538c..a54c40408e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1063,13 +1063,23 @@ public extension TelegramEngine { public func toggleForumChannelTopicPinned(id: EnginePeer.Id, threadId: Int64) -> Signal { return self.account.postbox.transaction { transaction -> ([Int64], Int) in - var limit = 5 - let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue - if let data = appConfiguration.data, let value = data["topics_pinned_limit"] as? Double { - limit = Int(value) + if id == self.account.peerId { + var limit = 5 + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + if let data = appConfiguration.data, let value = data["saved_pinned_limit"] as? Double { + limit = Int(value) + } + + return (transaction.getPeerPinnedThreads(peerId: id), limit) + } else { + var limit = 5 + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + if let data = appConfiguration.data, let value = data["topics_pinned_limit"] as? Double { + limit = Int(value) + } + + return (transaction.getPeerPinnedThreads(peerId: id), limit) } - - return (transaction.getPeerPinnedThreads(peerId: id), limit) } |> castError(SetForumChannelTopicPinnedError.self) |> mapToSignal { threadIds, limit -> Signal in diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 96bb4d1af1..00c6499718 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -188,6 +188,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case dismissedPremiumWallpapersBadge = 54 case dismissedPremiumColorsBadge = 55 case multipleReactionsSuggestion = 56 + case savedMessagesChatsSuggestion = 57 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -465,6 +466,9 @@ private struct ApplicationSpecificNoticeKeys { static func multipleReactionsSuggestion() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.multipleReactionsSuggestion.key) } + static func savedMessagesChatsSuggestion() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.savedMessagesChatsSuggestion.key) + } } public struct ApplicationSpecificNotice { @@ -1852,4 +1856,31 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } + + public static func getSavedMessagesChatsSuggestion(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.savedMessagesChatsSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementSavedMessagesChatsSuggestion(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.savedMessagesChatsSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.savedMessagesChatsSuggestion(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 8fd981bdd9..dbaddd9023 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -213,6 +213,7 @@ public enum PresentationResourceKey: Int32 { case chatInputSearchPanelMembersImage case chatHistoryNavigationButtonImage + case chatHistoryNavigationUpButtonImage case chatHistoryMentionsButtonImage case chatHistoryReactionsButtonImage case chatHistoryNavigationButtonBadgeImage diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index bd8fdd22d6..561da4688d 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -606,6 +606,28 @@ public struct PresentationResourcesChat { }) } + public static func chatHistoryNavigationUpButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatHistoryNavigationUpButtonImage.rawValue, { theme in + return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setLineWidth(0.5) + context.setStrokeColor(theme.chat.historyNavigation.strokeColor.cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.25, y: 0.25), size: CGSize(width: size.width - 0.5, height: size.height - 0.5))) + context.setStrokeColor(theme.chat.historyNavigation.foregroundColor.cgColor) + context.setLineWidth(1.5) + + context.translateBy(x: size.width * 0.5, y: size.height * 0.5) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width * 0.5, y: -size.height * 0.5) + let position = CGPoint(x: 9.0 - 0.5, y: 24.0) + context.move(to: CGPoint(x: position.x + 1.0, y: position.y - 1.0)) + context.addLine(to: CGPoint(x: position.x + 10.0, y: position.y - 10.0)) + context.addLine(to: CGPoint(x: position.x + 19.0, y: position.y - 1.0)) + context.strokePath() + }) + }) + } + public static func chatHistoryMentionsButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatHistoryMentionsButtonImage.rawValue, { theme in return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 327955d2fa..f70e9ade84 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -141,7 +141,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var wasPending: Bool = false private var didChangeFromPendingToSent: Bool = false - required public init() { + required public init(rotated: Bool) { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.imageNode = TransformImageNode() @@ -156,7 +156,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.textNode.textNode.displaysAsynchronously = false self.textNode.textNode.isUserInteractionEnabled = false - super.init(layerBacked: false) + super.init(rotated: rotated) self.containerNode.shouldBegin = { [weak self] location in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index aae6ebc963..7def4f6902 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -636,7 +636,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } - required public init() { + required public init(rotated: Bool) { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() self.backgroundWallpaperNode = ChatMessageBubbleBackdrop() @@ -654,7 +654,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI //self.debugNode = ASDisplayNode() //self.debugNode.backgroundColor = .blue - super.init(layerBacked: false) + super.init(rotated: rotated) //self.addSubnode(self.debugNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 55a4c35309..62303af69c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -89,13 +89,13 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco fileprivate var wasPlaying = false - required public init() { + required public init(rotated: Bool) { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode() self.messageAccessibilityArea = AccessibilityAreaNode() - super.init(layerBacked: false) + super.init(rotated: rotated) self.interactiveVideoNode.shouldOpen = { [weak self] in if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift index e2d8837293..ac098658ff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift @@ -122,7 +122,7 @@ public protocol ChatMessageItem: ListViewItem { var sending: Bool { get } var failed: Bool { get } - func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) + func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) } public func hasCommentButton(item: ChatMessageItem) -> Bool { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index ae698993b6..63ae52c794 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -47,9 +47,13 @@ public final class ChatMessageDateHeader: ListViewItemHeader { self.action = action self.roundedTimestamp = dateHeaderTimestampId(timestamp: timestamp) self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.roundedTimestamp)) + + let isRotated = controllerInteraction?.chatIsRotated ?? true + + self.stickDirection = isRotated ? .bottom : .top } - public let stickDirection: ListViewItemHeaderStickDirection = .bottom + public let stickDirection: ListViewItemHeaderStickDirection public let stickOverInsets: Bool = true public let height: CGFloat = 34.0 @@ -191,9 +195,13 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } self.text = text - super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) + let isRotated = controllerInteraction?.chatIsRotated ?? true - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false) + + if isRotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) @@ -398,9 +406,13 @@ public final class ChatMessageAvatarHeader: ListViewItemHeader { self.controllerInteraction = controllerInteraction self.id = ListViewItemNode.HeaderId(space: 1, id: Id(peerId: peerId, timestampId: dateHeaderTimestampId(timestamp: timestamp))) self.storyStats = storyStats + + let isRotated = controllerInteraction?.chatIsRotated ?? true + + self.stickDirection = isRotated ? .top : .bottom } - public let stickDirection: ListViewItemHeaderStickDirection = .top + public let stickDirection: ListViewItemHeaderStickDirection public let stickOverInsets: Bool = false public let height: CGFloat = 38.0 @@ -484,9 +496,13 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.contentNode.displaysAsynchronously = !presentationData.isPreview - super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) + let isRotated = controllerInteraction?.chatIsRotated ?? true + + super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false) - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + if isRotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.avatarNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index f3e50cf870..f9883c0e9c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -259,7 +259,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible self.associatedData = associatedData self.controllerInteraction = controllerInteraction self.content = content - self.disableDate = disableDate + self.disableDate = disableDate || !controllerInteraction.chatIsRotated self.additionalContent = additionalContent var avatarHeader: ChatMessageAvatarHeader? @@ -369,6 +369,9 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible if case .messageOptions = associatedData.subject { headers = [] } + if !controllerInteraction.chatIsRotated { + headers = [] + } if let avatarHeader = self.avatarHeader { headers.append(avatarHeader) } @@ -450,11 +453,11 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } let configure = { - let node = (viewClassName as! ChatMessageItemView.Type).init() + let node = (viewClassName as! ChatMessageItemView.Type).init(rotated: self.controllerInteraction.chatIsRotated) node.setupItem(self, synchronousLoad: synchronousLoads) let nodeLayout = node.asyncLayout() - let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) + let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: self.controllerInteraction.chatIsRotated) var disableDate = self.disableDate if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { @@ -490,7 +493,15 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } } - public func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { + public func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { + var top = top + var bottom = bottom + if !isRotated { + let previousTop = top + top = bottom + bottom = previousTop + } + var mergedTop: ChatMessageMerge = .none var mergedBottom: ChatMessageMerge = .none var dateAtBottom = false @@ -530,8 +541,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible let nodeLayout = nodeValue.asyncLayout() + let isRotated = self.controllerInteraction.chatIsRotated + async { - let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) + let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: isRotated) var disableDate = self.disableDate if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 81957907ff..331bc890ed 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -653,13 +653,11 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { open var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)? - public required convenience init() { - self.init(layerBacked: false) - } - - public init(layerBacked: Bool) { - super.init(layerBacked: layerBacked, dynamicBounce: true, rotated: true) - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + public required init(rotated: Bool) { + super.init(layerBacked: false, dynamicBounce: true, rotated: rotated) + if rotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } } required public init?(coder aDecoder: NSCoder) { @@ -684,7 +682,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { override open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatMessageItem { let doLayout = self.asyncLayout() - let merged = item.mergedWithItems(top: previousItem, bottom: nextItem) + let merged = item.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: item.controllerInteraction.chatIsRotated) let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index e99ad854f4..f9d6593249 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -95,7 +95,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } - required public init() { + required public init(rotated: Bool) { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.imageNode = TransformImageNode() @@ -104,7 +104,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.messageAccessibilityArea = AccessibilityAreaNode() - super.init(layerBacked: false) + super.init(rotated: rotated) var firstTime = true self.imageNode.imageUpdated = { [weak self] image in diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 1f8978e8f8..1bc89dbb67 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -259,6 +259,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public var playNextOutgoingGift: Bool = false public var recommendedChannelsOpenUp: Bool = false public var enableFullTranslucency: Bool = true + public var chatIsRotated: Bool = true public init( openMessage: @escaping (Message, OpenMessageParams) -> Bool, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD index cfd1866200..586c694ff3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD @@ -22,6 +22,8 @@ swift_library( "//submodules/AppBundle", "//submodules/ChatListUI", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode", + "//submodules/DeleteChatPeerActionSheetItem", + "//submodules/UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index 4b7ba3d59a..6a96a5d862 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -14,6 +14,8 @@ import TelegramUIPreferences import AppBundle import PeerInfoPaneNode import ChatListUI +import DeleteChatPeerActionSheetItem +import UndoUI public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private let context: AccountContext @@ -174,6 +176,113 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI self.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: currentParams.size, insets: UIEdgeInsets(top: currentParams.topInset, left: currentParams.sideInset, bottom: currentParams.bottomInset, right: currentParams.sideInset), verticalOffset: offset + self.shimmerNodeOffset, transition: transition) } } + + self.chatListNode.push = { [weak self] c in + guard let self else { + return + } + self.parentController?.push(c) + } + + self.chatListNode.present = { [weak self] c in + guard let self else { + return + } + self.parentController?.present(c, in: .window(.root)) + } + + self.chatListNode.deletePeerChat = { [weak self] peerId, _ in + guard let self else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + + self.view.window?.endEditing(true) + + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: peer, chatPeer: peer, action: .deleteSavedPeer, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, balancedLayout: true)) + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self else { + return + } + + self.chatListNode.updateState({ state in + var state = state + state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil)) + return state + }) + self.parentController?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + } + return true + }) + + //TODO:localize + self.parentController?.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: "Saved messages deleted.", text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + guard let self else { + return false + } + if value == .commit { + let _ = self.context.engine.messages.clearHistoryInteractively(peerId: self.context.account.peerId, threadId: peer.id.toInt64(), type: .forLocalPeer).startStandalone(completed: { [weak self] in + guard let self else { + return + } + self.chatListNode.updateState({ state in + var state = state + state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil)) + return state + }) + }) + return true + } else if value == .undo { + self.chatListNode.updateState({ state in + var state = state + state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil)) + return state + }) + return true + } + return false + }), in: .current) + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.parentController?.present(actionSheet, in: .window(.root)) + }) + } + + self.chatListNode.activateChatPreview = { [weak self] item, _, node, gesture, location in + guard let self, let parentController = self.parentController else { + gesture?.cancel() + return + } + + if case let .peer(peerData) = item.content { + let threadId = peerData.peer.peerId.toInt64() + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( + peerId: self.context.account.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + )), subject: nil, botStart: nil, mode: .standard(.previewing)) + chatController.canReadHistory.set(false) + let source: ContextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: parentController.navigationController as? NavigationController)) + + let contextController = ContextController(presentationData: self.presentationData, source: source, items: savedMessagesPeerMenuItems(context: self.context, threadId: threadId, parentController: parentController) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + parentController.presentInGlobalOverlay(contextController) + } + } } deinit { @@ -268,3 +377,32 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI return result } } + +private final class ContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + + let navigationController: NavigationController? + + let passthroughTouches: Bool = true + + init(controller: ViewController, sourceNode: ASDisplayNode?, navigationController: NavigationController?) { + self.controller = controller + self.sourceNode = sourceNode + self.navigationController = navigationController + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceNode = self.sourceNode + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in + if let sourceNode = sourceNode { + return (sourceNode.view, sourceNode.bounds) + } else { + return nil + } + }) + } + + func animatedIn() { + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift index 2f17905d31..884ac92524 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift @@ -61,7 +61,7 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.navigationController = navigationController self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: .replyThread(message: ChatReplyThreadMessage(peerId: context.account.peerId, threadId: peerId.toInt64(), channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)), subject: nil, botStart: nil, mode: .standard(.embedded)) + self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: .replyThread(message: ChatReplyThreadMessage(peerId: context.account.peerId, threadId: peerId.toInt64(), channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: true))) super.init() @@ -105,6 +105,9 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } public func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { + self.chatController.transferScrollingVelocity(velocity) + } } public func cancelPreviewGestures() { @@ -142,9 +145,10 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) let chatFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height - topInset)) - let combinedBottomInset = max(0.0, size.height - visibleHeight) + bottomInset + let combinedBottomInset = bottomInset transition.updateFrame(node: self.chatController.displayNode, frame: chatFrame) - self.chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + self.chatController.updateIsScrollingLockedAtTop(isScrollingLockedAtTop: isScrollingLockedAtTop) + self.chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index cab6ac3ea0..08c124388d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -287,7 +287,7 @@ private enum PeerInfoScreenInputData: Equatable { public func hasAvailablePeerInfoMediaPanes(context: AccountContext, peerId: PeerId) -> Signal { let chatLocationContextHolder = Atomic(value: nil) - return peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: .peer(id: peerId), chatLocationContextHolder: chatLocationContextHolder) + let mediaPanes = peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: .peer(id: peerId), chatLocationContextHolder: chatLocationContextHolder) |> map { panes -> Bool in if let panes { return !panes.isEmpty @@ -295,6 +295,22 @@ public func hasAvailablePeerInfoMediaPanes(context: AccountContext, peerId: Peer return false } } + + let hasSavedMessagesChats: Signal + if peerId == context.account.peerId { + hasSavedMessagesChats = context.engine.messages.savedMessagesPeerListHead() + |> map { headPeerId -> Bool in + return headPeerId != nil + } + |> distinctUntilChanged + } else { + hasSavedMessagesChats = .single(false) + } + + return combineLatest(queue: .mainQueue(), [mediaPanes, hasSavedMessagesChats]) + |> map { values in + return values.contains(true) + } } private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic) -> Signal<[PeerInfoPaneKey]?, NoError> { @@ -807,6 +823,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } let hasSavedMessages: Signal + let hasSavedMessagesChats: Signal if case .peer = chatLocation { hasSavedMessages = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: context.account.peerId, threadId: peerId.toInt64(), tag: MessageTags())) |> map { count -> Bool in @@ -817,8 +834,15 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } } |> distinctUntilChanged + + hasSavedMessagesChats = context.engine.messages.savedMessagesPeerListHead() + |> map { headPeerId -> Bool in + return headPeerId != nil + } + |> distinctUntilChanged } else { hasSavedMessages = .single(false) + hasSavedMessagesChats = .single(false) } return combineLatest( @@ -830,9 +854,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasStories, accountIsPremium, savedMessagesPeer, + hasSavedMessagesChats, hasSavedMessages ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, accountIsPremium, savedMessagesPeer, hasSavedMessages -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { @@ -848,7 +873,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen if case .peer = chatLocation { if peerId == context.account.peerId { - availablePanes?.insert(.savedMessagesChats, at: 0) + if hasSavedMessagesChats { + availablePanes?.insert(.savedMessagesChats, at: 0) + } } else if hasSavedMessages { if var availablePanesValue = availablePanes { if let index = availablePanesValue.firstIndex(of: .media) { @@ -928,6 +955,21 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } |> distinctUntilChanged + let hasSavedMessages: Signal + if case .peer = chatLocation { + hasSavedMessages = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: context.account.peerId, threadId: peerId.toInt64(), tag: MessageTags())) + |> map { count -> Bool in + if let count, count != 0 { + return true + } else { + return false + } + } + |> distinctUntilChanged + } else { + hasSavedMessages = .single(false) + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), @@ -939,9 +981,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen requestsStatePromise.get(), hasStories, accountIsPremium, - context.engine.peers.recommendedChannels(peerId: peerId) + context.engine.peers.recommendedChannels(peerId: peerId), + hasSavedMessages ) - |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { if hasStories { @@ -952,7 +995,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } if case .peer = chatLocation { - if var availablePanesValue = availablePanes { + if hasSavedMessages, var availablePanesValue = availablePanes { if let index = availablePanesValue.firstIndex(of: .media) { availablePanesValue.insert(.savedMessages, at: index + 1) } else { @@ -1138,6 +1181,21 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } |> distinctUntilChanged + let hasSavedMessages: Signal + if case .peer = chatLocation { + hasSavedMessages = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: context.account.peerId, threadId: peerId.toInt64(), tag: MessageTags())) + |> map { count -> Bool in + if let count, count != 0 { + return true + } else { + return false + } + } + |> distinctUntilChanged + } else { + hasSavedMessages = .single(false) + } + return combineLatest(queue: .mainQueue(), context.account.viewTracker.peerView(groupId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: groupId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), @@ -1150,9 +1208,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen requestsStatePromise.get(), threadData, context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]), - accountIsPremium + accountIsPremium, + hasSavedMessages ) - |> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData, preferencesView, accountIsPremium -> Signal in + |> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData, preferencesView, accountIsPremium, hasSavedMessages -> Signal in var discussionPeer: Peer? if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { discussionPeer = peer @@ -1168,7 +1227,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } if case .peer = chatLocation { - if var availablePanesValue = availablePanes { + if hasSavedMessages, var availablePanesValue = availablePanes { if let index = availablePanesValue.firstIndex(of: .media) { availablePanesValue.insert(.savedMessages, at: index + 1) } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 81271aef90..623d2f72cc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -581,6 +581,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) { return [] } + if case .savedMessagesChats = currentPaneKey { + if index == 0 { + return .leftCenter + } + return [.leftCenter, .rightCenter] + } if index == 0 { return .left } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 9f01c2a7ce..48b4d3e486 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -5452,7 +5452,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) }) } } @@ -6334,7 +6348,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) }) } shareController.actionCompleted = { [weak self] in @@ -7153,7 +7181,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) }) } shareController.actionCompleted = { [weak self] in @@ -9095,7 +9137,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) } peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer, threadId in let peerId = peer.id @@ -9107,7 +9163,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messageIds.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messageIds.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index f9af36cf21..4fa7ef5151 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1090,13 +1090,28 @@ final class StoryItemSetContainerSendMessage { } if let controller = component.controller() { + let context = component.context let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } controller.present(UndoOverlayController( presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, - action: { _ in return false } + action: { [weak controller] _ in + if savedMessages { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let controller, let peer else { + return + } + guard let navigationController = controller.navigationController as? NavigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + }) + } + return false + } ), in: .current) } }) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 30fbd7b4da..64de20131d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -321,6 +321,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var preloadNextChatPeerId: PeerId? let preloadNextChatPeerIdDisposable = MetaDisposable() + var preloadSavedMessagesChatsDisposable: Disposable? + let botCallbackAlertMessage = Promise(nil) var botCallbackAlertMessageDisposable: Disposable? @@ -2513,7 +2515,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) }) } strongSelf.chatDisplayNode.dismissInput() @@ -5698,6 +5714,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } })) + + if peerId == context.account.peerId { + self.preloadSavedMessagesChatsDisposable = context.engine.messages.savedMessagesPeerListHead().start() + } } else if case let .replyThread(messagePromise) = self.chatLocationInfoData, let peerId = peerId { self.reportIrrelvantGeoNoticePromise.set(.single(nil)) @@ -5891,7 +5911,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let mappedPeerData = ChatTitleContent.PeerData( peerId: savedMessagesPeerId, peer: savedMessagesPeer?.peer?._asPeer(), - isContact: false, + isContact: true, notificationSettings: nil, peerPresences: [:], cachedData: nil @@ -6743,6 +6763,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.automaticMediaDownloadSettingsDisposable?.dispose() self.stickerSettingsDisposable?.dispose() self.searchQuerySuggestionState?.1.dispose() + self.preloadSavedMessagesChatsDisposable?.dispose() } deallocate() } @@ -7185,6 +7206,39 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasPlentyOfMessages(hasPlentyOfMessages) }) } } + if case .peer(self.context.account.peerId) = self.chatLocation { + var didDisplayTooltip = false + self.chatDisplayNode.historyNode.hasLotsOfMessagesUpdated = { [weak self] hasLotsOfMessages in + guard let self, hasLotsOfMessages else { + return + } + if didDisplayTooltip { + return + } + didDisplayTooltip = true + + let _ = (ApplicationSpecificNotice.getSavedMessagesChatsSuggestion(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self else { + return + } + if counter >= 3 { + return + } + guard let navigationBar = self.navigationBar else { + return + } + + //TODO:localize + let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Tap to view your Saved Messages organized by type or source"), location: .point(navigationBar.frame, .top), displayDuration: .manual, shouldDismissOnTouch: { point, _ in + return .ignore + }) + self.present(tooltipScreen, in: .current) + + let _ = ApplicationSpecificNotice.incrementSavedMessagesChatsSuggestion(accountManager: self.context.sharedContext.accountManager).startStandalone() + }) + } + } self.chatDisplayNode.historyNode.addContentOffset = { [weak self] offset, itemNode in guard let strongSelf = self else { @@ -11944,6 +11998,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil } + public func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) { + self.chatDisplayNode.isScrollingLockedAtTop = isScrollingLockedAtTop + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.suspendNavigationBarLayout = true super.containerLayoutUpdated(layout, transition: transition) @@ -16375,7 +16433,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) } switch mode { @@ -18910,6 +18982,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) self.push(controller) } + + public func transferScrollingVelocity(_ velocity: CGFloat) { + self.chatDisplayNode.historyNode.transferVelocity(velocity) + } } final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index ebefee58ca..587a58bad4 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -132,6 +132,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let loadingNode: ChatLoadingNode private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? + var isScrollingLockedAtTop: Bool = false + private var emptyNode: ChatEmptyNode? private(set) var emptyType: ChatHistoryNodeLoadState.EmptyType? private var didDisplayEmptyGreeting = false @@ -579,15 +581,23 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { source = .default } + + var historyNodeRotated = true + switch chatPresentationInterfaceState.mode { + case let .standard(standardMode): + if case .embedded(true) = standardMode { + historyNodeRotated = false + } + default: + break + } + + self.controllerInteraction.chatIsRotated = historyNodeRotated var getMessageTransitionNode: (() -> ChatMessageTransitionNodeImpl?)? - self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: nil, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), messageTransitionNode: { + self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: nil, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), rotated: historyNodeRotated, messageTransitionNode: { return getMessageTransitionNode?() }) - self.historyNode.rotated = true - - //self.historyScrollingArea = SparseDiscreteScrollingArea() - //self.historyNode.historyScrollingArea = self.historyScrollingArea self.historyNodeContainer = HistoryNodeContainer(isSecret: chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat) @@ -625,7 +635,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelBottomBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor self.inputPanelBottomBackgroundSeparatorNode.isLayerBacked = true - self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode) + self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode, isChatRotated: historyNodeRotated) self.navigateButtons.accessibilityElementsHidden = true super.init() @@ -1857,6 +1867,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + if !self.historyNode.rotated { + let current = listInsets + listInsets.top = current.bottom + listInsets.bottom = current.top + } + var displayTopDimNode = false let ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil var expandTopDimNode = false @@ -1923,6 +1939,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.notifyTransitionCompletionListeners(transition: transition) } }) + if self.isScrollingLockedAtTop { + switch self.historyNode.visibleContentOffset() { + case let .known(value) where value <= CGFloat.ulpOfOne: + break + case .none: + break + default: + self.historyNode.scrollToEndOfHistory() + } + } + self.historyNode.scrollEnabled = !self.isScrollingLockedAtTop let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) @@ -1946,6 +1973,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { apparentNavigateButtonsFrame.origin.y -= 16.0 } + if !self.historyNode.rotated { + apparentNavigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: 6.0), size: navigateButtonsSize) + } + var isInputExpansionEnabled = false if case .media = self.chatPresentationInterfaceState.inputMode { isInputExpansionEnabled = true diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 26fda0d0e0..44f0750b6c 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -621,6 +621,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto public private(set) var hasPlentyOfMessages: Bool = false public var hasPlentyOfMessagesUpdated: ((Bool) -> Void)? + public private(set) var hasLotsOfMessages: Bool = false + public var hasLotsOfMessagesUpdated: ((Bool) -> Void)? + private var loadedMessagesFromCachedDataDisposable: Disposable? let isTopReplyThreadMessageShown = ValuePromise(false, ignoreRepeated: true) @@ -683,7 +686,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private var allowDustEffect: Bool = true private var dustEffectLayer: DustEffectLayer? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, rotated: Bool = false, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) { var tagMask = tagMask if case .pinnedMessages = subject { tagMask = .pinned @@ -738,6 +741,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto nextClientId += 1 super.init() + + self.rotated = rotated + if rotated { + self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + } self.clipsToBounds = false @@ -809,12 +817,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } self.preloadPages = false - switch self.mode { - case .bubbles: - self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) - case .list: - break - } self.beginChatHistoryTransitions( selectedMessages: selectedMessages, @@ -3092,7 +3094,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: self.bounds.size) self.dustEffectLayer = dustEffectLayer dustEffectLayer.zPosition = 10.0 - dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + if self.rotated { + dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + } self.layer.addSublayer(dustEffectLayer) dustEffectLayer.becameEmpty = { [weak self] in guard let self else { @@ -3310,13 +3314,18 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } var hasPlentyOfMessages = false + var hasLotsOfMessages = false if let historyView = strongSelf.historyView { if historyView.originalView.holeEarlier || historyView.originalView.holeLater { hasPlentyOfMessages = true + hasLotsOfMessages = true } else if !historyView.originalView.holeEarlier && !historyView.originalView.holeLater { if historyView.filteredEntries.count >= 10 { hasPlentyOfMessages = true } + if historyView.filteredEntries.count >= 40 { + hasLotsOfMessages = true + } } } @@ -3324,6 +3333,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.hasPlentyOfMessages = hasPlentyOfMessages strongSelf.hasPlentyOfMessagesUpdated?(hasPlentyOfMessages) } + if strongSelf.hasLotsOfMessages != hasLotsOfMessages { + strongSelf.hasLotsOfMessages = hasLotsOfMessages + strongSelf.hasLotsOfMessagesUpdated?(hasLotsOfMessages) + } if let _ = visibleRange.loadedRange { if let visible = visibleRange.visibleRange { @@ -4170,7 +4183,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto overscrollView.frame = overscrollView.convert(overscrollView.bounds, to: self.view) snapshotView.addSubview(overscrollView) - overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + if self.rotated { + overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } } return SnapshotState( @@ -4195,13 +4210,17 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let snapshotParentView = UIView() snapshotParentView.addSubview(snapshotState.snapshotView) - snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + if self.rotated { + snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + } snapshotParentView.frame = self.view.frame snapshotState.snapshotView.frame = snapshotParentView.bounds snapshotState.snapshotView.clipsToBounds = true - snapshotState.snapshotView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + if self.rotated { + snapshotState.snapshotView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } self.view.superview?.insertSubview(snapshotParentView, belowSubview: self.view) diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift index 0c87bc5366..7dc96eea3e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift @@ -10,6 +10,7 @@ private let badgeFont = Font.with(size: 13.0, traits: [.monospacedNumbers]) enum ChatHistoryNavigationButtonType { case down + case up case mentions case reactions } @@ -60,6 +61,8 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { switch type { case .down: self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + case .up: + self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme) case .mentions: self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) case .reactions: @@ -113,6 +116,8 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { switch self.type { case .down: self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + case .up: + self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme) case .mentions: self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) case .reactions: diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift index 97053a85e2..513bc1b85d 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift @@ -8,6 +8,7 @@ import WallpaperBackgroundNode final class ChatHistoryNavigationButtons: ASDisplayNode { private var theme: PresentationTheme private var dateTimeFormat: PresentationDateTimeFormat + private let isChatRotated: Bool let reactionsButton: ChatHistoryNavigationButtonNode let mentionsButton: ChatHistoryNavigationButtonNode @@ -68,7 +69,8 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { } } - init(theme: PresentationTheme, dateTimeFormat: PresentationDateTimeFormat, backgroundNode: WallpaperBackgroundNode) { + init(theme: PresentationTheme, dateTimeFormat: PresentationDateTimeFormat, backgroundNode: WallpaperBackgroundNode, isChatRotated: Bool) { + self.isChatRotated = isChatRotated self.theme = theme self.dateTimeFormat = dateTimeFormat @@ -80,7 +82,7 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { self.reactionsButton.alpha = 0.0 self.reactionsButton.isHidden = true - self.downButton = ChatHistoryNavigationButtonNode(theme: theme, backgroundNode: backgroundNode, type: .down) + self.downButton = ChatHistoryNavigationButtonNode(theme: theme, backgroundNode: backgroundNode, type: isChatRotated ? .down : .up) self.downButton.alpha = 0.0 self.downButton.isHidden = true @@ -186,11 +188,15 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { transition.updateTransformScale(node: self.reactionsButton, scale: 0.2) } - transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height), size: buttonSize).center) - - transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize).center) - - transition.updatePosition(node: self.reactionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset - reactionsOffset), size: buttonSize).center) + if self.isChatRotated { + transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height), size: buttonSize).center) + transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize).center) + transition.updatePosition(node: self.reactionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset - reactionsOffset), size: buttonSize).center) + } else { + transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: buttonSize).center) + transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: mentionsOffset), size: buttonSize).center) + transition.updatePosition(node: self.reactionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: mentionsOffset + reactionsOffset), size: buttonSize).center) + } if let (rect, containerSize) = self.absoluteRect { self.update(rect: rect, within: containerSize, transition: transition) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index 5682f9d023..c28ceaa95c 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -104,7 +104,21 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer } } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + if savedMessages, let self { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) } }) } diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 40c9847038..f8d4f5234d 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -810,6 +810,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { if let _ = self.openActiveTextItem, let textComponentView = self.textView.view, let result = textComponentView.hitTest(self.view.convert(point, to: textComponentView), with: event) { return result } + if let closeButtonNode = self.closeButtonNode, let result = closeButtonNode.hitTest(self.view.convert(point, to: closeButtonNode.view), with: event) { + return result + } var eventIsPresses = false if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 865ca15d61..509cf77fe8 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -639,10 +639,19 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animatedStickerNode = nil let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) - let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let bold: MarkdownAttributeSet + if savedMessages { + bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0), additionalAttributes: ["URL": ""]) + } else { + bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + } let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 2 + + if savedMessages { + isUserInteractionEnabled = true + } displayUndo = false self.originalRemainingSeconds = 3