[WIP] Saved messages

This commit is contained in:
Isaac 2023-12-28 00:20:23 +04:00
parent ee90dd8332
commit 4b16494e20
53 changed files with 1164 additions and 238 deletions

View File

@ -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 %@?";

View File

@ -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 {

View File

@ -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 }

View File

@ -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)

View File

@ -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,6 +404,8 @@ 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 case .savedMessagesChats = location {
} else {
if !isEditing {
var canArchive = false
var canUnarchive = false
@ -422,6 +430,7 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem
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
}

View File

@ -22,6 +22,7 @@ import StoryContainerScreen
import ChatListHeaderComponent
import UndoUI
import NewSessionInfoScreen
import PresentationDataUtils
public enum ChatListNodeMode {
case chatList(appendContacts: Bool)
@ -1420,6 +1421,25 @@ public final class ChatListNode: ListView {
}
}
}, setItemPinned: { [weak self] itemId, _ in
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
}
})
}
} 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 {
@ -1484,6 +1504,7 @@ public final class ChatListNode: ListView {
}
})
})
}
}, setPeerMuted: { [weak self] peerId, _ in
guard let strongSelf = self else {
return

View File

@ -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",

View File

@ -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<Empty>()
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<Empty>
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)

View File

@ -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 {

View File

@ -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)
}
})
}

View File

@ -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)

View File

@ -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))
}
})
}

View File

@ -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)

View File

@ -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))
}
})
}

View File

@ -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 {

View File

@ -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

View File

@ -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 = {

View File

@ -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<Api.messages.AffectedHistory>) {
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<Api.Updates>) {
let buffer = Buffer()

View File

@ -458,6 +458,23 @@ public enum SetForumChannelTopicPinnedError {
}
func _internal_setForumChannelPinnedTopics(account: Account, id: EnginePeer.Id, threadIds: [Int64]) -> Signal<Never, SetForumChannelTopicPinnedError> {
if id == account.peerId {
return account.postbox.transaction { transaction -> [Api.InputDialogPeer] in
transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds)
return threadIds.compactMap { transaction.getPeer(PeerId($0)).flatMap(apiInputPeer).flatMap({ .inputDialogPeer(peer: $0) }) }
}
|> castError(SetForumChannelTopicPinnedError.self)
|> mapToSignal { inputPeers -> Signal<Never, SetForumChannelTopicPinnedError> in
return account.network.request(Api.functions.messages.reorderPinnedSavedDialogs(flags: 1 << 0, order: inputPeers))
|> mapError { _ -> SetForumChannelTopicPinnedError in
return .generic
}
|> mapToSignal { _ -> Signal<Never, SetForumChannelTopicPinnedError> in
return .complete()
}
}
} else {
return account.postbox.transaction { transaction -> Api.InputChannel? in
guard let inputChannel = transaction.getPeer(id).flatMap(apiInputChannel) else {
return nil
@ -487,6 +504,7 @@ func _internal_setForumChannelPinnedTopics(account: Account, id: EnginePeer.Id,
return .complete()
}
}
}
}
func _internal_setChannelForumMode(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, isForum: Bool) -> Signal<Never, NoError> {
@ -687,8 +705,12 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post
limit: Int32(limit),
hash: 0
))
|> mapError { _ -> LoadMessageHistoryThreadsError in
return .generic
|> `catch` { error -> Signal<Api.messages.SavedDialogs, LoadMessageHistoryThreadsError> in
if error.errorDescription == "SAVED_DIALOGS_UNSUPPORTED" {
return .never()
} else {
return .fail(.generic)
}
}
|> mapToSignal { result -> Signal<LoadMessageHistoryThreadsResult, LoadMessageHistoryThreadsError> in
switch result {

View File

@ -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<Void, NoError> {
if peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser {
if let inputPeer = apiInputPeer(peer) {
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<Api.messages.AffectedHistory?, Bool> in
return .fail(true)
}
|> mapToSignal { result -> Signal<Void, Bool> 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<Void, NoError> 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()
}

View File

@ -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)

View File

@ -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,

View File

@ -1153,5 +1153,6 @@ public extension TelegramEngine.EngineData.Item {
}
}
}
}
}

View File

@ -148,7 +148,18 @@ func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threa
}
if let topIndex = topIndex {
if peerId.namespace == Namespaces.Peer.CloudUser {
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)
}

View File

@ -1314,6 +1314,21 @@ public extension TelegramEngine {
return self.account.stateManager.synchronouslyIsMessageDeletedInteractively(ids: ids)
}
public func savedMessagesPeerListHead() -> Signal<EnginePeer.Id?, NoError> {
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<Int?, NoError> {
return self.account.postbox.combinedView(keys: [.savedMessagesStats(peerId: self.account.peerId)])
|> map { views -> Int? in

View File

@ -1063,6 +1063,15 @@ public extension TelegramEngine {
public func toggleForumChannelTopicPinned(id: EnginePeer.Id, threadId: Int64) -> Signal<Never, SetForumChannelTopicPinnedError> {
return self.account.postbox.transaction { transaction -> ([Int64], Int) in
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 {
@ -1071,6 +1080,7 @@ public extension TelegramEngine {
return (transaction.getPeerPinnedThreads(peerId: id), limit)
}
}
|> castError(SetForumChannelTopicPinnedError.self)
|> mapToSignal { threadIds, limit -> Signal<Never, SetForumChannelTopicPinnedError> in
var threadIds = threadIds

View File

@ -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<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
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<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
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)
}
}
}

View File

@ -213,6 +213,7 @@ public enum PresentationResourceKey: Int32 {
case chatInputSearchPanelMembersImage
case chatHistoryNavigationButtonImage
case chatHistoryNavigationUpButtonImage
case chatHistoryMentionsButtonImage
case chatHistoryReactionsButtonImage
case chatHistoryNavigationButtonBadgeImage

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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
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)
if isRotated {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.avatarNode)

View File

@ -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 {

View File

@ -653,14 +653,12 @@ 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)
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) {
fatalError("init(coder:) has not been implemented")
@ -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

View File

@ -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

View File

@ -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,

View File

@ -22,6 +22,8 @@ swift_library(
"//submodules/AppBundle",
"//submodules/ChatListUI",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode",
"//submodules/DeleteChatPeerActionSheetItem",
"//submodules/UndoUI",
],
visibility = [
"//visibility:public",

View File

@ -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() {
}
}

View File

@ -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? {

View File

@ -287,7 +287,7 @@ private enum PeerInfoScreenInputData: Equatable {
public func hasAvailablePeerInfoMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<Bool, NoError> {
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(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<Bool, NoError>
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<ChatLocationContextHolder?>) -> Signal<[PeerInfoPaneKey]?, NoError> {
@ -807,6 +823,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
}
let hasSavedMessages: Signal<Bool, NoError>
let hasSavedMessagesChats: Signal<Bool, NoError>
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 {
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<Bool, NoError>
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<Bool, NoError>
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<PeerInfoScreenData, NoError> in
|> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData, preferencesView, accountIsPremium, hasSavedMessages -> Signal<PeerInfoScreenData, NoError> 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 {

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}
})

View File

@ -321,6 +321,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var preloadNextChatPeerId: PeerId?
let preloadNextChatPeerIdDisposable = MetaDisposable()
var preloadSavedMessagesChatsDisposable: Disposable?
let botCallbackAlertMessage = Promise<String?>(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 {

View File

@ -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
@ -580,14 +582,22 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
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

View File

@ -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<Bool>(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<PresentationData, NoError>), chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tagMask: MessageTags?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tagMask: MessageTags?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles, rotated: Bool = false, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) {
var tagMask = tagMask
if case .pinnedMessages = subject {
tagMask = .pinned
@ -739,6 +742,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
super.init()
self.rotated = rotated
if rotated {
self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
}
self.clipsToBounds = false
self.beginAdMessageManagement(adMessages: adMessages)
@ -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
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,8 +4183,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
overscrollView.frame = overscrollView.convert(overscrollView.bounds, to: self.view)
snapshotView.addSubview(overscrollView)
if self.rotated {
overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
}
return SnapshotState(
snapshotTopInset: snapshotTopInset,
@ -4195,13 +4210,17 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let snapshotParentView = UIView()
snapshotParentView.addSubview(snapshotState.snapshotView)
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
if self.rotated {
snapshotState.snapshotView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
self.view.superview?.insertSubview(snapshotParentView, belowSubview: self.view)

View File

@ -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:

View File

@ -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)
}
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)

View File

@ -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)
}
})
}

View File

@ -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, *) {

View File

@ -639,11 +639,20 @@ 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
case let .gigagroupConversion(text):