[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.SelectUsers.SearchPlaceholder" = "Search";
"RequestPeer.ReachedMaximum_1" = "You can select up to %@ user."; "RequestPeer.ReachedMaximum_1" = "You can select up to %@ user.";
"RequestPeer.ReachedMaximum_any" = "You can select up to %@ users."; "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 ChatControllerPresentationMode: Equatable {
public enum StandardPresentation { public enum StandardPresentation: Equatable {
case `default` case `default`
case previewing case previewing
case embedded case embedded(invertDirection: Bool)
} }
case standard(StandardPresentation) case standard(StandardPresentation)
@ -912,6 +912,9 @@ public protocol ChatController: ViewController {
func cancelSelectingMessages() func cancelSelectingMessages()
func activateSearch(domain: ChatSearchDomain, query: String) func activateSearch(domain: ChatSearchDomain, query: String)
func beginClearHistory(type: InteractiveHistoryClearingType) func beginClearHistory(type: InteractiveHistoryClearingType)
func transferScrollingVelocity(_ velocity: CGFloat)
func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool)
} }
public protocol ChatMessagePreviewItemNode: AnyObject { 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) { 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 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 } 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 peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer, threadId in
let peerId = peer.id let peerId = peer.id
@ -1432,7 +1446,22 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } 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 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) 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] { 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] = [] var options: [ItemListRevealOption] = []
if !isEditing { 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 { 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)) 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 { } else {
@ -398,28 +404,31 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem
if canDelete { 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)) options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor))
} }
if !isEditing { if case .savedMessagesChats = location {
var canArchive = false } else {
var canUnarchive = false if !isEditing {
if let filterData = filterData { var canArchive = false
if filterData.excludesArchived { var canUnarchive = false
canArchive = true if let filterData = filterData {
} if filterData.excludesArchived {
} else {
if case let .chatList(groupId) = location {
if case .root = groupId {
canArchive = true canArchive = true
} else { }
canUnarchive = true } else {
if case let .chatList(groupId) = location {
if case .root = groupId {
canArchive = true
} else {
canUnarchive = true
}
} }
} }
} if canArchive {
if canArchive { if canArchivePeer(id: peerId, accountPeerId: accountPeerId) {
if canArchivePeer(id: peerId, accountPeerId: accountPeerId) { options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) }
} else if canUnarchive {
options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor))
} }
} else if canUnarchive {
options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor))
} }
} }
return options return options

View File

@ -22,6 +22,7 @@ import StoryContainerScreen
import ChatListHeaderComponent import ChatListHeaderComponent
import UndoUI import UndoUI
import NewSessionInfoScreen import NewSessionInfoScreen
import PresentationDataUtils
public enum ChatListNodeMode { public enum ChatListNodeMode {
case chatList(appendContacts: Bool) case chatList(appendContacts: Bool)
@ -1420,70 +1421,90 @@ public final class ChatListNode: ListView {
} }
} }
}, setItemPinned: { [weak self] itemId, _ in }, setItemPinned: { [weak self] itemId, _ in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) if case .savedMessagesChats = location {
|> deliverOnMainQueue).startStandalone(next: { peer in if case let .peer(itemPeerId) = itemId {
guard let strongSelf = self else { let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: context.account.peerId, threadId: itemPeerId.toInt64())
return |> deliverOnMainQueue).start(error: { error in
} guard let self else {
guard case let .chatList(groupId) = strongSelf.location else { return
return }
} switch error {
case let .limitReached(count):
let isPremium = peer?.isPremium ?? false let controller = PremiumLimitScreen(context: context, subject: .pinnedSavedPeers, count: Int32(count), action: {
let location: TogglePeerChatPinnedLocation return true
if let chatListFilter = chatListFilter { })
location = .filter(chatListFilter.id) self.push?(controller)
} else { default:
location = .group(groupId._asGroup())
}
let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: itemId)
|> deliverOnMainQueue).startStandalone(next: { result in
if let strongSelf = self {
switch result {
case .done:
break break
case let .limitExceeded(count, _): }
if isPremium { })
if case .filter = location { }
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { } else {
return true let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
}) |> deliverOnMainQueue).startStandalone(next: { peer in
strongSelf.push?(controller) guard let strongSelf = self else {
} else { return
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { }
return true guard case let .chatList(groupId) = strongSelf.location else {
}) return
strongSelf.push?(controller) }
}
} else { let isPremium = peer?.isPremium ?? false
if case .filter = location { let location: TogglePeerChatPinnedLocation
var replaceImpl: ((ViewController) -> Void)? if let chatListFilter = chatListFilter {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { location = .filter(chatListFilter.id)
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) } else {
replaceImpl?(premiumScreen) location = .group(groupId._asGroup())
return true }
}) let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: itemId)
strongSelf.push?(controller) |> deliverOnMainQueue).startStandalone(next: { result in
replaceImpl = { [weak controller] c in if let strongSelf = self {
controller?.replace(with: c) switch result {
case .done:
break
case let .limitExceeded(count, _):
if isPremium {
if case .filter = location {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
return true
})
strongSelf.push?(controller)
} else {
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
return true
})
strongSelf.push?(controller)
} }
} else { } else {
var replaceImpl: ((ViewController) -> Void)? if case .filter = location {
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { var replaceImpl: ((ViewController) -> Void)?
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
replaceImpl?(premiumScreen) let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
return true replaceImpl?(premiumScreen)
}) return true
strongSelf.push?(controller) })
replaceImpl = { [weak controller] c in strongSelf.push?(controller)
controller?.replace(with: c) replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
} else {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
replaceImpl?(premiumScreen)
return true
})
strongSelf.push?(controller)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
} }
} }
} }
} }
} })
}) })
}) }
}, setPeerMuted: { [weak self] peerId, _ in }, setPeerMuted: { [weak self] peerId, _ in
guard let strongSelf = self else { guard let strongSelf = self else {
return return

View File

@ -16,6 +16,9 @@ swift_library(
"//submodules/AccountContext:AccountContext", "//submodules/AccountContext:AccountContext",
"//submodules/AvatarNode:AvatarNode", "//submodules/AvatarNode:AvatarNode",
"//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -6,6 +6,9 @@ import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import AvatarNode import AvatarNode
import AccountContext import AccountContext
import ComponentFlow
import BalancedTextComponent
import MultilineTextComponent
public enum DeleteChatPeerAction { public enum DeleteChatPeerAction {
case delete case delete
@ -15,6 +18,7 @@ public enum DeleteChatPeerAction {
case clearCacheSuggestion case clearCacheSuggestion
case removeFromGroup case removeFromGroup
case removeFromChannel case removeFromChannel
case deleteSavedPeer
} }
private let avatarFont = avatarPlaceholderFont(size: 26.0) private let avatarFont = avatarPlaceholderFont(size: 26.0)
@ -26,18 +30,20 @@ public final class DeleteChatPeerActionSheetItem: ActionSheetItem {
let action: DeleteChatPeerAction let action: DeleteChatPeerAction
let strings: PresentationStrings let strings: PresentationStrings
let nameDisplayOrder: PresentationPersonNameOrder 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.context = context
self.peer = peer self.peer = peer
self.chatPeer = chatPeer self.chatPeer = chatPeer
self.action = action self.action = action
self.strings = strings self.strings = strings
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
self.balancedLayout = balancedLayout
} }
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { 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) { public func updateNode(_ node: ActionSheetItemNode) {
@ -47,15 +53,19 @@ public final class DeleteChatPeerActionSheetItem: ActionSheetItem {
private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings private let strings: PresentationStrings
private let balancedLayout: Bool
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let textNode: ImmediateTextNode
private var text: NSAttributedString?
private let textView = ComponentView<Empty>()
private let accessibilityArea: AccessibilityAreaNode 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.theme = theme
self.strings = strings self.strings = strings
self.balancedLayout = balancedLayout
let textFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0)) let textFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0))
let boldFont = Font.semibold(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 = AvatarNode(font: avatarFont)
self.avatarNode.isAccessibilityElement = false 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() self.accessibilityArea = AccessibilityAreaNode()
super.init(theme: theme) super.init(theme: theme)
self.addSubnode(self.avatarNode) self.addSubnode(self.avatarNode)
self.addSubnode(self.textNode)
self.addSubnode(self.accessibilityArea) self.addSubnode(self.accessibilityArea)
if chatPeer.id == context.account.peerId { if chatPeer.id == context.account.peerId {
self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .savedMessagesIcon) self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .savedMessagesIcon)
} else if chatPeer.id.isReplies { } else if chatPeer.id.isReplies {
self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .repliesIcon) 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 { } else {
var overrideImage: AvatarNodeImageOverride? var overrideImage: AvatarNodeImageOverride?
if chatPeer.isDeleted { if chatPeer.isDeleted {
@ -127,6 +132,10 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
} else { } else {
text = strings.ChatList_DeleteChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder)) 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): case let .clearHistory(canClearCache):
if peer.id == context.account.peerId { if peer.id == context.account.peerId {
text = PresentationStrings.FormattedString(string: strings.ChatList_ClearSavedMessagesConfirmation, ranges: []) text = PresentationStrings.FormattedString(string: strings.ChatList_ClearSavedMessagesConfirmation, ranges: [])
@ -162,7 +171,7 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
} }
if let attributedText = attributedText { if let attributedText = attributedText {
self.textNode.attributedText = attributedText self.text = attributedText
self.accessibilityArea.accessibilityLabel = attributedText.string self.accessibilityArea.accessibilityLabel = attributedText.string
self.accessibilityArea.accessibilityTraits = .staticText self.accessibilityArea.accessibilityTraits = .staticText
@ -170,7 +179,21 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
} }
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { 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 topInset: CGFloat = 16.0
let avatarSize: CGFloat = 60.0 let avatarSize: CGFloat = 60.0
@ -178,7 +201,13 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
let bottomInset: CGFloat = 15.0 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.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) let size = CGSize(width: constrainedSize.width, height: topInset + avatarSize + textSpacing + textSize.height + bottomInset)
self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size) 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) { override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition) super.containerLayoutUpdated(layout, transition: transition)
if self.layout != nil && self.layout! != layout { if self.layout != nil && self.layout!.size != layout.size {
if self.dismissImmediatelyOnLayoutUpdate { if self.dismissImmediatelyOnLayoutUpdate {
self.dismissImmediately() self.dismissImmediately()
} else { } else {

View File

@ -184,7 +184,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
text = "" 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 presentInGlobalOverlayImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)? var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)?
var navigationController: (() -> NavigationController?)?
var dismissTooltipsImpl: (() -> Void)? 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 = { shareController.actionCompleted = {
@ -746,6 +761,9 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
return true return true
} }
} }
navigationController = { [weak controller] in
return controller?.navigationController as? NavigationController
}
pushControllerImpl = { [weak controller] c in pushControllerImpl = { [weak controller] c in
if let controller = controller { if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) (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 pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
var navigationController: (() -> NavigationController?)?
var dismissTooltipsImpl: (() -> Void)? 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 = { 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 = { shareController.actionCompleted = {
@ -925,6 +954,9 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
(controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true)
} }
} }
navigationController = { [weak controller] in
return controller?.navigationController as? NavigationController
}
presentControllerImpl = { [weak controller] c, p in presentControllerImpl = { [weak controller] c, p in
if let controller = controller { if let controller = controller {
controller.present(c, in: .window(.root), with: p) 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) 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 self.isLoading = validIndexBoundary == nil
if let validIndexBoundary = validIndexBoundary { if let validIndexBoundary = validIndexBoundary {

View File

@ -1021,6 +1021,22 @@ private final class LimitSheetContent: CombinedComponent {
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition 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 { if isPremiumDisabled {
badgeText = "\(limit)" badgeText = "\(limit)"
string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string
@ -1771,6 +1787,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
case folders case folders
case chatsPerFolder case chatsPerFolder
case pins case pins
case pinnedSavedPeers
case files case files
case accounts case accounts
case linksPerSharedFolder 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 = { 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 { public extension Api.functions.messages {
static func deleteScheduledMessages(peer: Api.InputPeer, id: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) { static func deleteScheduledMessages(peer: Api.InputPeer, id: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer() let buffer = Buffer()

View File

@ -458,33 +458,51 @@ public enum SetForumChannelTopicPinnedError {
} }
func _internal_setForumChannelPinnedTopics(account: Account, id: EnginePeer.Id, threadIds: [Int64]) -> Signal<Never, SetForumChannelTopicPinnedError> { func _internal_setForumChannelPinnedTopics(account: Account, id: EnginePeer.Id, threadIds: [Int64]) -> Signal<Never, SetForumChannelTopicPinnedError> {
return account.postbox.transaction { transaction -> Api.InputChannel? in if id == account.peerId {
guard let inputChannel = transaction.getPeer(id).flatMap(apiInputChannel) else { return account.postbox.transaction { transaction -> [Api.InputDialogPeer] in
return nil transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds)
}
transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds)
return inputChannel
}
|> castError(SetForumChannelTopicPinnedError.self)
|> mapToSignal { inputChannel -> Signal<Never, SetForumChannelTopicPinnedError> in
guard let inputChannel = inputChannel else {
return .fail(.generic)
}
return account.network.request(Api.functions.channels.reorderPinnedForumTopics(
flags: 1 << 0,
channel: inputChannel,
order: threadIds.map(Int32.init(clamping:))
))
|> mapError { _ -> SetForumChannelTopicPinnedError in
return .generic
}
|> mapToSignal { result -> Signal<Never, SetForumChannelTopicPinnedError> in
account.stateManager.addUpdates(result)
return .complete() 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
}
transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds)
return inputChannel
}
|> castError(SetForumChannelTopicPinnedError.self)
|> mapToSignal { inputChannel -> Signal<Never, SetForumChannelTopicPinnedError> in
guard let inputChannel = inputChannel else {
return .fail(.generic)
}
return account.network.request(Api.functions.channels.reorderPinnedForumTopics(
flags: 1 << 0,
channel: inputChannel,
order: threadIds.map(Int32.init(clamping:))
))
|> mapError { _ -> SetForumChannelTopicPinnedError in
return .generic
}
|> mapToSignal { result -> Signal<Never, SetForumChannelTopicPinnedError> in
account.stateManager.addUpdates(result)
return .complete()
}
} }
} }
} }
@ -687,8 +705,12 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post
limit: Int32(limit), limit: Int32(limit),
hash: 0 hash: 0
)) ))
|> mapError { _ -> LoadMessageHistoryThreadsError in |> `catch` { error -> Signal<Api.messages.SavedDialogs, LoadMessageHistoryThreadsError> in
return .generic if error.errorDescription == "SAVED_DIALOGS_UNSUPPORTED" {
return .never()
} else {
return .fail(.generic)
}
} }
|> mapToSignal { result -> Signal<LoadMessageHistoryThreadsResult, LoadMessageHistoryThreadsError> in |> mapToSignal { result -> Signal<LoadMessageHistoryThreadsResult, LoadMessageHistoryThreadsError> in
switch result { 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> { 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 peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser {
if let inputPeer = apiInputPeer(peer) { if let inputPeer = apiInputPeer(peer) {
return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, minTimestamp: operation.minTimestamp, maxTimestamp: operation.maxTimestamp, type: operation.type) if peer.id == stateManager.accountPeerId, let threadId = operation.threadId {
guard let inputSubPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) else {
return .complete()
}
var flags: Int32 = 0
var updatedMaxId = operation.topMessageId.id
if operation.minTimestamp != nil {
flags |= 1 << 2
updatedMaxId = 0
}
if operation.maxTimestamp != nil {
flags |= 1 << 3
updatedMaxId = 0
}
let signal = network.request(Api.functions.messages.deleteSavedHistory(flags: flags, peer: inputSubPeer, maxId: updatedMaxId, minDate: operation.minTimestamp, maxDate: operation.maxTimestamp))
|> map { result -> Api.messages.AffectedHistory? in
return result
}
|> `catch` { _ -> Signal<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 { } else {
return .complete() return .complete()
} }

View File

@ -2,34 +2,36 @@ import Postbox
import SwiftSignalKit import SwiftSignalKit
public struct UserLimitsConfiguration: Equatable { public struct UserLimitsConfiguration: Equatable {
public let maxPinnedChatCount: Int32 public var maxPinnedChatCount: Int32
public let maxArchivedPinnedChatCount: Int32 public var maxPinnedSavedChatCount: Int32
public let maxChannelsCount: Int32 public var maxArchivedPinnedChatCount: Int32
public let maxPublicLinksCount: Int32 public var maxChannelsCount: Int32
public let maxSavedGifCount: Int32 public var maxPublicLinksCount: Int32
public let maxFavedStickerCount: Int32 public var maxSavedGifCount: Int32
public let maxFoldersCount: Int32 public var maxFavedStickerCount: Int32
public let maxFolderChatsCount: Int32 public var maxFoldersCount: Int32
public let maxCaptionLength: Int32 public var maxFolderChatsCount: Int32
public let maxUploadFileParts: Int32 public var maxCaptionLength: Int32
public let maxAboutLength: Int32 public var maxUploadFileParts: Int32
public let maxAnimatedEmojisInText: Int32 public var maxAboutLength: Int32
public let maxReactionsPerMessage: Int32 public var maxAnimatedEmojisInText: Int32
public let maxSharedFolderInviteLinks: Int32 public var maxReactionsPerMessage: Int32
public let maxSharedFolderJoin: Int32 public var maxSharedFolderInviteLinks: Int32
public let maxStoryCaptionLength: Int32 public var maxSharedFolderJoin: Int32
public let maxExpiringStoriesCount: Int32 public var maxStoryCaptionLength: Int32
public let maxStoriesWeeklyCount: Int32 public var maxExpiringStoriesCount: Int32
public let maxStoriesMonthlyCount: Int32 public var maxStoriesWeeklyCount: Int32
public let maxStoriesSuggestedReactions: Int32 public var maxStoriesMonthlyCount: Int32
public let maxGiveawayChannelsCount: Int32 public var maxStoriesSuggestedReactions: Int32
public let maxGiveawayCountriesCount: Int32 public var maxGiveawayChannelsCount: Int32
public let maxGiveawayPeriodSeconds: Int32 public var maxGiveawayCountriesCount: Int32
public let maxChannelRecommendationsCount: Int32 public var maxGiveawayPeriodSeconds: Int32
public var maxChannelRecommendationsCount: Int32
public static var defaultValue: UserLimitsConfiguration { public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration( return UserLimitsConfiguration(
maxPinnedChatCount: 5, maxPinnedChatCount: 5,
maxPinnedSavedChatCount: 5,
maxArchivedPinnedChatCount: 100, maxArchivedPinnedChatCount: 100,
maxChannelsCount: 500, maxChannelsCount: 500,
maxPublicLinksCount: 10, maxPublicLinksCount: 10,
@ -58,6 +60,7 @@ public struct UserLimitsConfiguration: Equatable {
public init( public init(
maxPinnedChatCount: Int32, maxPinnedChatCount: Int32,
maxPinnedSavedChatCount: Int32,
maxArchivedPinnedChatCount: Int32, maxArchivedPinnedChatCount: Int32,
maxChannelsCount: Int32, maxChannelsCount: Int32,
maxPublicLinksCount: Int32, maxPublicLinksCount: Int32,
@ -83,6 +86,7 @@ public struct UserLimitsConfiguration: Equatable {
maxChannelRecommendationsCount: Int32 maxChannelRecommendationsCount: Int32
) { ) {
self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedChatCount = maxPinnedChatCount
self.maxPinnedSavedChatCount = maxPinnedSavedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
self.maxChannelsCount = maxChannelsCount self.maxChannelsCount = maxChannelsCount
self.maxPublicLinksCount = maxPublicLinksCount self.maxPublicLinksCount = maxPublicLinksCount
@ -112,7 +116,10 @@ public struct UserLimitsConfiguration: Equatable {
extension UserLimitsConfiguration { extension UserLimitsConfiguration {
init(appConfiguration: AppConfiguration, isPremium: Bool) { init(appConfiguration: AppConfiguration, isPremium: Bool) {
let keySuffix = isPremium ? "_premium" : "_default" 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 { func getValue(_ key: String, orElse defaultValue: Int32) -> Int32 {
if let value = appConfiguration.data?[key + keySuffix] as? Double { 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.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.maxArchivedPinnedChatCount = getValue("dialogs_folder_pinned_limit", orElse: defaultValue.maxArchivedPinnedChatCount)
self.maxChannelsCount = getValue("channels_limit", orElse: defaultValue.maxChannelsCount) self.maxChannelsCount = getValue("channels_limit", orElse: defaultValue.maxChannelsCount)
self.maxPublicLinksCount = getValue("channels_public_limit", orElse: defaultValue.maxPublicLinksCount) self.maxPublicLinksCount = getValue("channels_public_limit", orElse: defaultValue.maxPublicLinksCount)

View File

@ -37,6 +37,7 @@ public enum EngineConfiguration {
public struct UserLimits: Equatable { public struct UserLimits: Equatable {
public let maxPinnedChatCount: Int32 public let maxPinnedChatCount: Int32
public let maxPinnedSavedChatCount: Int32
public let maxArchivedPinnedChatCount: Int32 public let maxArchivedPinnedChatCount: Int32
public let maxChannelsCount: Int32 public let maxChannelsCount: Int32
public let maxPublicLinksCount: Int32 public let maxPublicLinksCount: Int32
@ -67,6 +68,7 @@ public enum EngineConfiguration {
public init( public init(
maxPinnedChatCount: Int32, maxPinnedChatCount: Int32,
maxPinnedSavedChatCount: Int32,
maxArchivedPinnedChatCount: Int32, maxArchivedPinnedChatCount: Int32,
maxChannelsCount: Int32, maxChannelsCount: Int32,
maxPublicLinksCount: Int32, maxPublicLinksCount: Int32,
@ -92,6 +94,7 @@ public enum EngineConfiguration {
maxChannelRecommendationsCount: Int32 maxChannelRecommendationsCount: Int32
) { ) {
self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedChatCount = maxPinnedChatCount
self.maxPinnedSavedChatCount = maxPinnedSavedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
self.maxChannelsCount = maxChannelsCount self.maxChannelsCount = maxChannelsCount
self.maxPublicLinksCount = maxPublicLinksCount self.maxPublicLinksCount = maxPublicLinksCount
@ -153,6 +156,7 @@ public extension EngineConfiguration.UserLimits {
init(_ userLimitsConfiguration: UserLimitsConfiguration) { init(_ userLimitsConfiguration: UserLimitsConfiguration) {
self.init( self.init(
maxPinnedChatCount: userLimitsConfiguration.maxPinnedChatCount, maxPinnedChatCount: userLimitsConfiguration.maxPinnedChatCount,
maxPinnedSavedChatCount: userLimitsConfiguration.maxPinnedSavedChatCount,
maxArchivedPinnedChatCount: userLimitsConfiguration.maxArchivedPinnedChatCount, maxArchivedPinnedChatCount: userLimitsConfiguration.maxArchivedPinnedChatCount,
maxChannelsCount: userLimitsConfiguration.maxChannelsCount, maxChannelsCount: userLimitsConfiguration.maxChannelsCount,
maxPublicLinksCount: userLimitsConfiguration.maxPublicLinksCount, 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 let topIndex = topIndex {
if peerId.namespace == Namespaces.Peer.CloudUser { if peerId.namespace == Namespaces.Peer.CloudUser {
let _ = transaction.addMessages([StoreMessage(id: topIndex.id, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: topIndex.timestamp, flags: StoreMessageFlags(), tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: nil, text: "", attributes: [], media: [TelegramMediaAction(action: .historyCleared)])], location: .Random) var addEmptyMessage = false
if threadId == nil {
addEmptyMessage = true
} else {
if transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) == nil {
addEmptyMessage = true
}
}
if addEmptyMessage {
let _ = transaction.addMessages([StoreMessage(id: topIndex.id, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: topIndex.timestamp, flags: StoreMessageFlags(), tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: nil, text: "", attributes: [], media: [TelegramMediaAction(action: .historyCleared)])], location: .Random)
}
} else { } else {
updatePeerChatInclusionWithMinTimestamp(transaction: transaction, id: peerId, minTimestamp: topIndex.timestamp, forceRootGroupIfNotExists: false) 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) 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> { public func savedMessagesPeersStats() -> Signal<Int?, NoError> {
return self.account.postbox.combinedView(keys: [.savedMessagesStats(peerId: self.account.peerId)]) return self.account.postbox.combinedView(keys: [.savedMessagesStats(peerId: self.account.peerId)])
|> map { views -> Int? in |> map { views -> Int? in

View File

@ -1063,13 +1063,23 @@ public extension TelegramEngine {
public func toggleForumChannelTopicPinned(id: EnginePeer.Id, threadId: Int64) -> Signal<Never, SetForumChannelTopicPinnedError> { public func toggleForumChannelTopicPinned(id: EnginePeer.Id, threadId: Int64) -> Signal<Never, SetForumChannelTopicPinnedError> {
return self.account.postbox.transaction { transaction -> ([Int64], Int) in return self.account.postbox.transaction { transaction -> ([Int64], Int) in
var limit = 5 if id == self.account.peerId {
let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue var limit = 5
if let data = appConfiguration.data, let value = data["topics_pinned_limit"] as? Double { let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
limit = Int(value) if let data = appConfiguration.data, let value = data["saved_pinned_limit"] as? Double {
limit = Int(value)
}
return (transaction.getPeerPinnedThreads(peerId: id), limit)
} else {
var limit = 5
let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
if let data = appConfiguration.data, let value = data["topics_pinned_limit"] as? Double {
limit = Int(value)
}
return (transaction.getPeerPinnedThreads(peerId: id), limit)
} }
return (transaction.getPeerPinnedThreads(peerId: id), limit)
} }
|> castError(SetForumChannelTopicPinnedError.self) |> castError(SetForumChannelTopicPinnedError.self)
|> mapToSignal { threadIds, limit -> Signal<Never, SetForumChannelTopicPinnedError> in |> mapToSignal { threadIds, limit -> Signal<Never, SetForumChannelTopicPinnedError> in

View File

@ -188,6 +188,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case dismissedPremiumWallpapersBadge = 54 case dismissedPremiumWallpapersBadge = 54
case dismissedPremiumColorsBadge = 55 case dismissedPremiumColorsBadge = 55
case multipleReactionsSuggestion = 56 case multipleReactionsSuggestion = 56
case savedMessagesChatsSuggestion = 57
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -465,6 +466,9 @@ private struct ApplicationSpecificNoticeKeys {
static func multipleReactionsSuggestion() -> NoticeEntryKey { static func multipleReactionsSuggestion() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.multipleReactionsSuggestion.key) 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 { public struct ApplicationSpecificNotice {
@ -1852,4 +1856,31 @@ public struct ApplicationSpecificNotice {
return Int(previousValue) 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 chatInputSearchPanelMembersImage
case chatHistoryNavigationButtonImage case chatHistoryNavigationButtonImage
case chatHistoryNavigationUpButtonImage
case chatHistoryMentionsButtonImage case chatHistoryMentionsButtonImage
case chatHistoryReactionsButtonImage case chatHistoryReactionsButtonImage
case chatHistoryNavigationButtonBadgeImage 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? { public static func chatHistoryMentionsButtonImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatHistoryMentionsButtonImage.rawValue, { theme in return theme.image(PresentationResourceKey.chatHistoryMentionsButtonImage.rawValue, { theme in
return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context 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 wasPending: Bool = false
private var didChangeFromPendingToSent: Bool = false private var didChangeFromPendingToSent: Bool = false
required public init() { required public init(rotated: Bool) {
self.contextSourceNode = ContextExtractedContentContainingNode() self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
@ -156,7 +156,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.textNode.textNode.displaysAsynchronously = false self.textNode.textNode.displaysAsynchronously = false
self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.isUserInteractionEnabled = false
super.init(layerBacked: false) super.init(rotated: rotated)
self.containerNode.shouldBegin = { [weak self] location in self.containerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self else { 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.mainContextSourceNode = ContextExtractedContentContainingNode()
self.mainContainerNode = ContextControllerSourceNode() self.mainContainerNode = ContextControllerSourceNode()
self.backgroundWallpaperNode = ChatMessageBubbleBackdrop() self.backgroundWallpaperNode = ChatMessageBubbleBackdrop()
@ -654,7 +654,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
//self.debugNode = ASDisplayNode() //self.debugNode = ASDisplayNode()
//self.debugNode.backgroundColor = .blue //self.debugNode.backgroundColor = .blue
super.init(layerBacked: false) super.init(rotated: rotated)
//self.addSubnode(self.debugNode) //self.addSubnode(self.debugNode)

View File

@ -89,13 +89,13 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
fileprivate var wasPlaying = false fileprivate var wasPlaying = false
required public init() { required public init(rotated: Bool) {
self.contextSourceNode = ContextExtractedContentContainingNode() self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode() self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode()
self.messageAccessibilityArea = AccessibilityAreaNode() self.messageAccessibilityArea = AccessibilityAreaNode()
super.init(layerBacked: false) super.init(rotated: rotated)
self.interactiveVideoNode.shouldOpen = { [weak self] in self.interactiveVideoNode.shouldOpen = { [weak self] in
if let strongSelf = self { if let strongSelf = self {

View File

@ -122,7 +122,7 @@ public protocol ChatMessageItem: ListViewItem {
var sending: Bool { get } var sending: Bool { get }
var failed: 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 { public func hasCommentButton(item: ChatMessageItem) -> Bool {

View File

@ -47,9 +47,13 @@ public final class ChatMessageDateHeader: ListViewItemHeader {
self.action = action self.action = action
self.roundedTimestamp = dateHeaderTimestampId(timestamp: timestamp) self.roundedTimestamp = dateHeaderTimestampId(timestamp: timestamp)
self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.roundedTimestamp)) 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 stickOverInsets: Bool = true
public let height: CGFloat = 34.0 public let height: CGFloat = 34.0
@ -191,9 +195,13 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode {
} }
self.text = text self.text = text
super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) let isRotated = controllerInteraction?.chatIsRotated ?? true
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false)
if isRotated {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) 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.controllerInteraction = controllerInteraction
self.id = ListViewItemNode.HeaderId(space: 1, id: Id(peerId: peerId, timestampId: dateHeaderTimestampId(timestamp: timestamp))) self.id = ListViewItemNode.HeaderId(space: 1, id: Id(peerId: peerId, timestampId: dateHeaderTimestampId(timestamp: timestamp)))
self.storyStats = storyStats 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 stickOverInsets: Bool = false
public let height: CGFloat = 38.0 public let height: CGFloat = 38.0
@ -484,9 +496,13 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat
self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.contentNode.displaysAsynchronously = !presentationData.isPreview self.avatarNode.contentNode.displaysAsynchronously = !presentationData.isPreview
super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) let isRotated = controllerInteraction?.chatIsRotated ?? true
super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) if isRotated {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
self.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.avatarNode) self.containerNode.addSubnode(self.avatarNode)

View File

@ -259,7 +259,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
self.associatedData = associatedData self.associatedData = associatedData
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction
self.content = content self.content = content
self.disableDate = disableDate self.disableDate = disableDate || !controllerInteraction.chatIsRotated
self.additionalContent = additionalContent self.additionalContent = additionalContent
var avatarHeader: ChatMessageAvatarHeader? var avatarHeader: ChatMessageAvatarHeader?
@ -369,6 +369,9 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
if case .messageOptions = associatedData.subject { if case .messageOptions = associatedData.subject {
headers = [] headers = []
} }
if !controllerInteraction.chatIsRotated {
headers = []
}
if let avatarHeader = self.avatarHeader { if let avatarHeader = self.avatarHeader {
headers.append(avatarHeader) headers.append(avatarHeader)
} }
@ -450,11 +453,11 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
} }
let configure = { 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) node.setupItem(self, synchronousLoad: synchronousLoads)
let nodeLayout = node.asyncLayout() 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 var disableDate = self.disableDate
if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { 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 mergedTop: ChatMessageMerge = .none
var mergedBottom: ChatMessageMerge = .none var mergedBottom: ChatMessageMerge = .none
var dateAtBottom = false var dateAtBottom = false
@ -530,8 +541,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
let nodeLayout = nodeValue.asyncLayout() let nodeLayout = nodeValue.asyncLayout()
let isRotated = self.controllerInteraction.chatIsRotated
async { 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 var disableDate = self.disableDate
if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject {

View File

@ -653,13 +653,11 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
open var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)? open var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)?
public required convenience init() { public required init(rotated: Bool) {
self.init(layerBacked: false) super.init(layerBacked: false, dynamicBounce: true, rotated: rotated)
} if rotated {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
public init(layerBacked: Bool) { }
super.init(layerBacked: layerBacked, dynamicBounce: true, rotated: true)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
} }
required public init?(coder aDecoder: NSCoder) { required public init?(coder aDecoder: NSCoder) {
@ -684,7 +682,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
override open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { override open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = item as? ChatMessageItem { if let item = item as? ChatMessageItem {
let doLayout = self.asyncLayout() 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) let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom)
self.contentSize = layout.contentSize self.contentSize = layout.contentSize
self.insets = layout.insets 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.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
@ -104,7 +104,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode()
self.messageAccessibilityArea = AccessibilityAreaNode() self.messageAccessibilityArea = AccessibilityAreaNode()
super.init(layerBacked: false) super.init(rotated: rotated)
var firstTime = true var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in 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 playNextOutgoingGift: Bool = false
public var recommendedChannelsOpenUp: Bool = false public var recommendedChannelsOpenUp: Bool = false
public var enableFullTranslucency: Bool = true public var enableFullTranslucency: Bool = true
public var chatIsRotated: Bool = true
public init( public init(
openMessage: @escaping (Message, OpenMessageParams) -> Bool, openMessage: @escaping (Message, OpenMessageParams) -> Bool,

View File

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

View File

@ -14,6 +14,8 @@ import TelegramUIPreferences
import AppBundle import AppBundle
import PeerInfoPaneNode import PeerInfoPaneNode
import ChatListUI import ChatListUI
import DeleteChatPeerActionSheetItem
import UndoUI
public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private let context: AccountContext 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.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 { deinit {
@ -268,3 +377,32 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI
return result 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.navigationController = navigationController
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } 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() super.init()
@ -105,6 +105,9 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
public func transferVelocity(_ velocity: CGFloat) { public func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
self.chatController.transferScrollingVelocity(velocity)
}
} }
public func cancelPreviewGestures() { public func cancelPreviewGestures() {
@ -142,9 +145,10 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) 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 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) 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? { 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> { public func hasAvailablePeerInfoMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<Bool, NoError> {
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil) 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 |> map { panes -> Bool in
if let panes { if let panes {
return !panes.isEmpty return !panes.isEmpty
@ -295,6 +295,22 @@ public func hasAvailablePeerInfoMediaPanes(context: AccountContext, peerId: Peer
return false 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> { 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 hasSavedMessages: Signal<Bool, NoError>
let hasSavedMessagesChats: Signal<Bool, NoError>
if case .peer = chatLocation { if case .peer = chatLocation {
hasSavedMessages = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: context.account.peerId, threadId: peerId.toInt64(), tag: MessageTags())) hasSavedMessages = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: context.account.peerId, threadId: peerId.toInt64(), tag: MessageTags()))
|> map { count -> Bool in |> map { count -> Bool in
@ -817,8 +834,15 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
} }
} }
|> distinctUntilChanged |> distinctUntilChanged
hasSavedMessagesChats = context.engine.messages.savedMessagesPeerListHead()
|> map { headPeerId -> Bool in
return headPeerId != nil
}
|> distinctUntilChanged
} else { } else {
hasSavedMessages = .single(false) hasSavedMessages = .single(false)
hasSavedMessagesChats = .single(false)
} }
return combineLatest( return combineLatest(
@ -830,9 +854,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasStories, hasStories,
accountIsPremium, accountIsPremium,
savedMessagesPeer, savedMessagesPeer,
hasSavedMessagesChats,
hasSavedMessages 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 var availablePanes = availablePanes
if let hasStories { if let hasStories {
@ -848,7 +873,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
if case .peer = chatLocation { if case .peer = chatLocation {
if peerId == context.account.peerId { if peerId == context.account.peerId {
availablePanes?.insert(.savedMessagesChats, at: 0) if hasSavedMessagesChats {
availablePanes?.insert(.savedMessagesChats, at: 0)
}
} else if hasSavedMessages { } else if hasSavedMessages {
if var availablePanesValue = availablePanes { if var availablePanesValue = availablePanes {
if let index = availablePanesValue.firstIndex(of: .media) { if let index = availablePanesValue.firstIndex(of: .media) {
@ -928,6 +955,21 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
} }
|> distinctUntilChanged |> 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( return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true), context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder),
@ -939,9 +981,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
requestsStatePromise.get(), requestsStatePromise.get(),
hasStories, hasStories,
accountIsPremium, 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 var availablePanes = availablePanes
if let hasStories { if let hasStories {
if hasStories { if hasStories {
@ -952,7 +995,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
} }
if case .peer = chatLocation { if case .peer = chatLocation {
if var availablePanesValue = availablePanes { if hasSavedMessages, var availablePanesValue = availablePanes {
if let index = availablePanesValue.firstIndex(of: .media) { if let index = availablePanesValue.firstIndex(of: .media) {
availablePanesValue.insert(.savedMessages, at: index + 1) availablePanesValue.insert(.savedMessages, at: index + 1)
} else { } else {
@ -1138,6 +1181,21 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
} }
|> distinctUntilChanged |> 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(), return combineLatest(queue: .mainQueue(),
context.account.viewTracker.peerView(groupId, updateData: true), context.account.viewTracker.peerView(groupId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: groupId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), peerInfoAvailableMediaPanes(context: context, peerId: groupId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder),
@ -1150,9 +1208,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
requestsStatePromise.get(), requestsStatePromise.get(),
threadData, threadData,
context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]), 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? var discussionPeer: Peer?
if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] {
discussionPeer = peer discussionPeer = peer
@ -1168,7 +1227,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
} }
if case .peer = chatLocation { if case .peer = chatLocation {
if var availablePanesValue = availablePanes { if hasSavedMessages, var availablePanesValue = availablePanes {
if let index = availablePanesValue.firstIndex(of: .media) { if let index = availablePanesValue.firstIndex(of: .media) {
availablePanesValue.insert(.savedMessages, at: index + 1) availablePanesValue.insert(.savedMessages, at: index + 1)
} else { } 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)) { if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) {
return [] return []
} }
if case .savedMessagesChats = currentPaneKey {
if index == 0 {
return .leftCenter
}
return [.leftCenter, .rightCenter]
}
if index == 0 { if index == 0 {
return .left 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 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 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 peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer, threadId in
let peerId = peer.id let peerId = peer.id
@ -9107,7 +9163,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } 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) strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil)

View File

@ -1090,13 +1090,28 @@ final class StoryItemSetContainerSendMessage {
} }
if let controller = component.controller() { if let controller = component.controller() {
let context = component.context
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController( controller.present(UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,
content: .forward(savedMessages: savedMessages, text: text), content: .forward(savedMessages: savedMessages, text: text),
elevatedLayout: false, elevatedLayout: false,
animateInAsReplacement: 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) ), in: .current)
} }
}) })

View File

@ -321,6 +321,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var preloadNextChatPeerId: PeerId? var preloadNextChatPeerId: PeerId?
let preloadNextChatPeerIdDisposable = MetaDisposable() let preloadNextChatPeerIdDisposable = MetaDisposable()
var preloadSavedMessagesChatsDisposable: Disposable?
let botCallbackAlertMessage = Promise<String?>(nil) let botCallbackAlertMessage = Promise<String?>(nil)
var botCallbackAlertMessageDisposable: Disposable? 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() 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 { } else if case let .replyThread(messagePromise) = self.chatLocationInfoData, let peerId = peerId {
self.reportIrrelvantGeoNoticePromise.set(.single(nil)) self.reportIrrelvantGeoNoticePromise.set(.single(nil))
@ -5891,7 +5911,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let mappedPeerData = ChatTitleContent.PeerData( let mappedPeerData = ChatTitleContent.PeerData(
peerId: savedMessagesPeerId, peerId: savedMessagesPeerId,
peer: savedMessagesPeer?.peer?._asPeer(), peer: savedMessagesPeer?.peer?._asPeer(),
isContact: false, isContact: true,
notificationSettings: nil, notificationSettings: nil,
peerPresences: [:], peerPresences: [:],
cachedData: nil cachedData: nil
@ -6743,6 +6763,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.automaticMediaDownloadSettingsDisposable?.dispose() self.automaticMediaDownloadSettingsDisposable?.dispose()
self.stickerSettingsDisposable?.dispose() self.stickerSettingsDisposable?.dispose()
self.searchQuerySuggestionState?.1.dispose() self.searchQuerySuggestionState?.1.dispose()
self.preloadSavedMessagesChatsDisposable?.dispose()
} }
deallocate() deallocate()
} }
@ -7185,6 +7206,39 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasPlentyOfMessages(hasPlentyOfMessages) }) 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 self.chatDisplayNode.historyNode.addContentOffset = { [weak self] offset, itemNode in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -11944,6 +11998,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return nil return nil
} }
public func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) {
self.chatDisplayNode.isScrollingLockedAtTop = isScrollingLockedAtTop
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.suspendNavigationBarLayout = true self.suspendNavigationBarLayout = true
super.containerLayoutUpdated(layout, transition: transition) 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 { switch mode {
@ -18910,6 +18982,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
) )
self.push(controller) self.push(controller)
} }
public func transferScrollingVelocity(_ velocity: CGFloat) {
self.chatDisplayNode.historyNode.transferVelocity(velocity)
}
} }
final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { final class ChatContextControllerContentSourceImpl: ContextControllerContentSource {

View File

@ -132,6 +132,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
let loadingNode: ChatLoadingNode let loadingNode: ChatLoadingNode
private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode?
var isScrollingLockedAtTop: Bool = false
private var emptyNode: ChatEmptyNode? private var emptyNode: ChatEmptyNode?
private(set) var emptyType: ChatHistoryNodeLoadState.EmptyType? private(set) var emptyType: ChatHistoryNodeLoadState.EmptyType?
private var didDisplayEmptyGreeting = false private var didDisplayEmptyGreeting = false
@ -579,15 +581,23 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
} else { } else {
source = .default 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?)? 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?() return getMessageTransitionNode?()
}) })
self.historyNode.rotated = true
//self.historyScrollingArea = SparseDiscreteScrollingArea()
//self.historyNode.historyScrollingArea = self.historyScrollingArea
self.historyNodeContainer = HistoryNodeContainer(isSecret: chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat) 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.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor
self.inputPanelBottomBackgroundSeparatorNode.isLayerBacked = true 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 self.navigateButtons.accessibilityElementsHidden = true
super.init() 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 var displayTopDimNode = false
let ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil let ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil
var expandTopDimNode = false var expandTopDimNode = false
@ -1923,6 +1939,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
strongSelf.notifyTransitionCompletionListeners(transition: transition) 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) 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) 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 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 var isInputExpansionEnabled = false
if case .media = self.chatPresentationInterfaceState.inputMode { if case .media = self.chatPresentationInterfaceState.inputMode {
isInputExpansionEnabled = true isInputExpansionEnabled = true

View File

@ -621,6 +621,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
public private(set) var hasPlentyOfMessages: Bool = false public private(set) var hasPlentyOfMessages: Bool = false
public var hasPlentyOfMessagesUpdated: ((Bool) -> Void)? public var hasPlentyOfMessagesUpdated: ((Bool) -> Void)?
public private(set) var hasLotsOfMessages: Bool = false
public var hasLotsOfMessagesUpdated: ((Bool) -> Void)?
private var loadedMessagesFromCachedDataDisposable: Disposable? private var loadedMessagesFromCachedDataDisposable: Disposable?
let isTopReplyThreadMessageShown = ValuePromise<Bool>(false, ignoreRepeated: true) 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 allowDustEffect: Bool = true
private var dustEffectLayer: DustEffectLayer? 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 var tagMask = tagMask
if case .pinnedMessages = subject { if case .pinnedMessages = subject {
tagMask = .pinned tagMask = .pinned
@ -738,6 +741,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
nextClientId += 1 nextClientId += 1
super.init() super.init()
self.rotated = rotated
if rotated {
self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
}
self.clipsToBounds = false self.clipsToBounds = false
@ -809,12 +817,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
} }
self.preloadPages = false 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( self.beginChatHistoryTransitions(
selectedMessages: selectedMessages, selectedMessages: selectedMessages,
@ -3092,7 +3094,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: self.bounds.size) dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: self.bounds.size)
self.dustEffectLayer = dustEffectLayer self.dustEffectLayer = dustEffectLayer
dustEffectLayer.zPosition = 10.0 dustEffectLayer.zPosition = 10.0
dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) if self.rotated {
dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
}
self.layer.addSublayer(dustEffectLayer) self.layer.addSublayer(dustEffectLayer)
dustEffectLayer.becameEmpty = { [weak self] in dustEffectLayer.becameEmpty = { [weak self] in
guard let self else { guard let self else {
@ -3310,13 +3314,18 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
} }
var hasPlentyOfMessages = false var hasPlentyOfMessages = false
var hasLotsOfMessages = false
if let historyView = strongSelf.historyView { if let historyView = strongSelf.historyView {
if historyView.originalView.holeEarlier || historyView.originalView.holeLater { if historyView.originalView.holeEarlier || historyView.originalView.holeLater {
hasPlentyOfMessages = true hasPlentyOfMessages = true
hasLotsOfMessages = true
} else if !historyView.originalView.holeEarlier && !historyView.originalView.holeLater { } else if !historyView.originalView.holeEarlier && !historyView.originalView.holeLater {
if historyView.filteredEntries.count >= 10 { if historyView.filteredEntries.count >= 10 {
hasPlentyOfMessages = true hasPlentyOfMessages = true
} }
if historyView.filteredEntries.count >= 40 {
hasLotsOfMessages = true
}
} }
} }
@ -3324,6 +3333,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
strongSelf.hasPlentyOfMessages = hasPlentyOfMessages strongSelf.hasPlentyOfMessages = hasPlentyOfMessages
strongSelf.hasPlentyOfMessagesUpdated?(hasPlentyOfMessages) strongSelf.hasPlentyOfMessagesUpdated?(hasPlentyOfMessages)
} }
if strongSelf.hasLotsOfMessages != hasLotsOfMessages {
strongSelf.hasLotsOfMessages = hasLotsOfMessages
strongSelf.hasLotsOfMessagesUpdated?(hasLotsOfMessages)
}
if let _ = visibleRange.loadedRange { if let _ = visibleRange.loadedRange {
if let visible = visibleRange.visibleRange { if let visible = visibleRange.visibleRange {
@ -4170,7 +4183,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
overscrollView.frame = overscrollView.convert(overscrollView.bounds, to: self.view) overscrollView.frame = overscrollView.convert(overscrollView.bounds, to: self.view)
snapshotView.addSubview(overscrollView) snapshotView.addSubview(overscrollView)
overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) if self.rotated {
overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
} }
return SnapshotState( return SnapshotState(
@ -4195,13 +4210,17 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let snapshotParentView = UIView() let snapshotParentView = UIView()
snapshotParentView.addSubview(snapshotState.snapshotView) snapshotParentView.addSubview(snapshotState.snapshotView)
snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) if self.rotated {
snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
}
snapshotParentView.frame = self.view.frame snapshotParentView.frame = self.view.frame
snapshotState.snapshotView.frame = snapshotParentView.bounds snapshotState.snapshotView.frame = snapshotParentView.bounds
snapshotState.snapshotView.clipsToBounds = true snapshotState.snapshotView.clipsToBounds = true
snapshotState.snapshotView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) if self.rotated {
snapshotState.snapshotView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
self.view.superview?.insertSubview(snapshotParentView, belowSubview: self.view) 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 { enum ChatHistoryNavigationButtonType {
case down case down
case up
case mentions case mentions
case reactions case reactions
} }
@ -60,6 +61,8 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode {
switch type { switch type {
case .down: case .down:
self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme)
case .up:
self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme)
case .mentions: case .mentions:
self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme)
case .reactions: case .reactions:
@ -113,6 +116,8 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode {
switch self.type { switch self.type {
case .down: case .down:
self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme)
case .up:
self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme)
case .mentions: case .mentions:
self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme)
case .reactions: case .reactions:

View File

@ -8,6 +8,7 @@ import WallpaperBackgroundNode
final class ChatHistoryNavigationButtons: ASDisplayNode { final class ChatHistoryNavigationButtons: ASDisplayNode {
private var theme: PresentationTheme private var theme: PresentationTheme
private var dateTimeFormat: PresentationDateTimeFormat private var dateTimeFormat: PresentationDateTimeFormat
private let isChatRotated: Bool
let reactionsButton: ChatHistoryNavigationButtonNode let reactionsButton: ChatHistoryNavigationButtonNode
let mentionsButton: 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.theme = theme
self.dateTimeFormat = dateTimeFormat self.dateTimeFormat = dateTimeFormat
@ -80,7 +82,7 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
self.reactionsButton.alpha = 0.0 self.reactionsButton.alpha = 0.0
self.reactionsButton.isHidden = true 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.alpha = 0.0
self.downButton.isHidden = true self.downButton.isHidden = true
@ -186,11 +188,15 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
transition.updateTransformScale(node: self.reactionsButton, scale: 0.2) transition.updateTransformScale(node: self.reactionsButton, scale: 0.2)
} }
transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height), size: buttonSize).center) 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.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)
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 { if let (rect, containerSize) = self.absoluteRect {
self.update(rect: rect, within: containerSize, transition: transition) 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) { if let _ = self.openActiveTextItem, let textComponentView = self.textView.view, let result = textComponentView.hitTest(self.view.convert(point, to: textComponentView), with: event) {
return result 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 var eventIsPresses = false
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {

View File

@ -639,10 +639,19 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.animatedStickerNode = nil self.animatedStickerNode = nil
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let 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) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2 self.textNode.maximumNumberOfLines = 2
if savedMessages {
isUserInteractionEnabled = true
}
displayUndo = false displayUndo = false
self.originalRemainingSeconds = 3 self.originalRemainingSeconds = 3