mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
[WIP] Business
This commit is contained in:
@@ -13,34 +13,223 @@ import ChatControllerInteraction
|
||||
import ItemListUI
|
||||
import ChatContextQuery
|
||||
import ChatInputContextPanelNode
|
||||
import ChatListUI
|
||||
import ComponentFlow
|
||||
|
||||
private struct CommandChatInputContextPanelEntryStableId: Hashable {
|
||||
let command: PeerCommand
|
||||
private enum CommandChatInputContextPanelEntryStableId: Hashable {
|
||||
case editShortcuts
|
||||
case command(PeerCommand)
|
||||
case shortcut(Int32)
|
||||
}
|
||||
|
||||
private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
|
||||
let index: Int
|
||||
let command: PeerCommand
|
||||
let theme: PresentationTheme
|
||||
|
||||
var stableId: CommandChatInputContextPanelEntryStableId {
|
||||
return CommandChatInputContextPanelEntryStableId(command: self.command)
|
||||
struct Command: Equatable {
|
||||
let command: ChatInputTextCommand
|
||||
let accountPeer: EnginePeer?
|
||||
let searchQuery: String?
|
||||
|
||||
static func ==(lhs: Command, rhs: Command) -> Bool {
|
||||
return lhs.command == rhs.command && lhs.accountPeer == rhs.accountPeer && lhs.searchQuery == rhs.searchQuery
|
||||
}
|
||||
}
|
||||
|
||||
func withUpdatedTheme(_ theme: PresentationTheme) -> CommandChatInputContextPanelEntry {
|
||||
return CommandChatInputContextPanelEntry(index: self.index, command: self.command, theme: theme)
|
||||
enum Content: Equatable {
|
||||
case editShortcuts
|
||||
case command(Command)
|
||||
}
|
||||
|
||||
let content: Content
|
||||
let index: Int
|
||||
let theme: PresentationTheme
|
||||
|
||||
init(index: Int, content: Content, theme: PresentationTheme) {
|
||||
self.content = content
|
||||
self.index = index
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
static func ==(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool {
|
||||
return lhs.index == rhs.index && lhs.command == rhs.command && lhs.theme === rhs.theme
|
||||
return lhs.index == rhs.index && lhs.content == rhs.content && lhs.theme === rhs.theme
|
||||
}
|
||||
|
||||
static func <(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> ListViewItem {
|
||||
return CommandChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), command: self.command, commandSelected: commandSelected)
|
||||
var stableId: CommandChatInputContextPanelEntryStableId {
|
||||
switch self.content {
|
||||
case .editShortcuts:
|
||||
return .editShortcuts
|
||||
case let .command(command):
|
||||
switch command.command {
|
||||
case let .command(command):
|
||||
return .command(command)
|
||||
case let .shortcut(shortcut):
|
||||
return .shortcut(shortcut.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withUpdatedTheme(_ theme: PresentationTheme) -> CommandChatInputContextPanelEntry {
|
||||
return CommandChatInputContextPanelEntry(index: self.index, content: self.content, theme: theme)
|
||||
}
|
||||
|
||||
func item(context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (ChatInputTextCommand, Bool) -> Void, openEditShortcuts: @escaping () -> Void) -> ListViewItem {
|
||||
switch self.content {
|
||||
case .editShortcuts:
|
||||
//TODO:localzie
|
||||
return VerticalListContextResultsChatInputPanelButtonItem(theme: presentationData.theme, style: .round, title: "Edit Quick Replies", pressed: {
|
||||
openEditShortcuts()
|
||||
})
|
||||
case let .command(command):
|
||||
switch command.command {
|
||||
case let .command(command):
|
||||
return CommandChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), command: command, commandSelected: { value, sendImmediately in
|
||||
commandSelected(.command(value), sendImmediately)
|
||||
})
|
||||
case let .shortcut(shortcut):
|
||||
let chatListNodeInteraction = ChatListNodeInteraction(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
activateSearch: {
|
||||
},
|
||||
peerSelected: { _, _, _, _ in
|
||||
commandSelected(.shortcut(shortcut), true)
|
||||
},
|
||||
disabledPeerSelected: { _, _, _ in
|
||||
},
|
||||
togglePeerSelected: { _, _ in
|
||||
},
|
||||
togglePeersSelection: { _, _ in
|
||||
},
|
||||
additionalCategorySelected: { _ in
|
||||
},
|
||||
messageSelected: { _, _, _, _ in
|
||||
commandSelected(.shortcut(shortcut), true)
|
||||
},
|
||||
groupSelected: { _ in
|
||||
},
|
||||
addContact: { _ in
|
||||
},
|
||||
setPeerIdWithRevealedOptions: { _, _ in
|
||||
},
|
||||
setItemPinned: { _, _ in
|
||||
},
|
||||
setPeerMuted: { _, _ in
|
||||
},
|
||||
setPeerThreadMuted: { _, _, _ in
|
||||
},
|
||||
deletePeer: { _, _ in
|
||||
},
|
||||
deletePeerThread: { _, _ in
|
||||
},
|
||||
setPeerThreadStopped: { _, _, _ in
|
||||
},
|
||||
setPeerThreadPinned: { _, _, _ in
|
||||
},
|
||||
setPeerThreadHidden: { _, _, _ in
|
||||
},
|
||||
updatePeerGrouping: { _, _ in
|
||||
},
|
||||
togglePeerMarkedUnread: { _, _ in
|
||||
},
|
||||
toggleArchivedFolderHiddenByDefault: {
|
||||
},
|
||||
toggleThreadsSelection: { _, _ in
|
||||
},
|
||||
hidePsa: { _ in
|
||||
},
|
||||
activateChatPreview: { _, _, _, _, _ in
|
||||
},
|
||||
present: { _ in
|
||||
},
|
||||
openForumThread: { _, _ in
|
||||
},
|
||||
openStorageManagement: {
|
||||
},
|
||||
openPasswordSetup: {
|
||||
},
|
||||
openPremiumIntro: {
|
||||
},
|
||||
openPremiumGift: {
|
||||
},
|
||||
openActiveSessions: {
|
||||
},
|
||||
performActiveSessionAction: { _, _ in
|
||||
},
|
||||
openChatFolderUpdates: {
|
||||
},
|
||||
hideChatFolderUpdates: {
|
||||
},
|
||||
openStories: { _, _ in
|
||||
},
|
||||
dismissNotice: { _ in
|
||||
}
|
||||
)
|
||||
|
||||
let chatListPresentationData = ChatListPresentationData(
|
||||
theme: presentationData.theme,
|
||||
fontSize: presentationData.listsFontSize,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameSortOrder: presentationData.nameSortOrder,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
disableAnimations: false
|
||||
)
|
||||
|
||||
let renderedPeer: EngineRenderedPeer
|
||||
if let accountPeer = command.accountPeer {
|
||||
renderedPeer = EngineRenderedPeer(peer: accountPeer)
|
||||
} else {
|
||||
renderedPeer = EngineRenderedPeer(peerId: context.account.peerId, peers: [:], associatedMedia: [:])
|
||||
}
|
||||
|
||||
return ChatListItem(
|
||||
presentationData: chatListPresentationData,
|
||||
context: context,
|
||||
chatListLocation: .chatList(groupId: .root),
|
||||
filterData: nil,
|
||||
index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: context.account.peerId, namespace: 0, id: 0), timestamp: 0))),
|
||||
content: .peer(ChatListItemContent.PeerData(
|
||||
messages: shortcut.messages.first.flatMap({ [$0] }) ?? [],
|
||||
peer: renderedPeer,
|
||||
threadInfo: nil,
|
||||
combinedReadState: nil,
|
||||
isRemovedFromTotalUnreadCount: false,
|
||||
presence: nil,
|
||||
hasUnseenMentions: false,
|
||||
hasUnseenReactions: false,
|
||||
draftState: nil,
|
||||
mediaDraftContentType: nil,
|
||||
inputActivities: nil,
|
||||
promoInfo: nil,
|
||||
ignoreUnreadBadge: false,
|
||||
displayAsMessage: false,
|
||||
hasFailedMessages: false,
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
storyState: nil,
|
||||
requiresPremiumForMessaging: false,
|
||||
displayAsTopicList: false,
|
||||
tags: [],
|
||||
customMessageListData: ChatListItemContent.CustomMessageListData(
|
||||
commandPrefix: "/\(shortcut.shortcut)",
|
||||
searchQuery: command.searchQuery.flatMap { "/\($0)"},
|
||||
messageCount: nil
|
||||
)
|
||||
)),
|
||||
editing: false,
|
||||
hasActiveRevealControls: false,
|
||||
selected: false,
|
||||
header: nil,
|
||||
enableContextActions: false,
|
||||
hiddenOffset: false,
|
||||
interaction: chatListNodeInteraction
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,21 +237,33 @@ private struct CommandChatInputContextPanelTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let hasShortcuts: Bool
|
||||
let itemCountChanged: Bool
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> CommandChatInputContextPanelTransition {
|
||||
private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (ChatInputTextCommand, Bool) -> Void, openEditShortcuts: @escaping () -> Void) -> CommandChatInputContextPanelTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected), directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected, openEditShortcuts: openEditShortcuts), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected, openEditShortcuts: openEditShortcuts), directionHint: nil) }
|
||||
|
||||
return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
let itemCountChanged = fromEntries.count != toEntries.count
|
||||
|
||||
return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates, hasShortcuts: toEntries.contains(where: { entry in
|
||||
if case .editShortcuts = entry.content {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}), itemCountChanged: itemCountChanged)
|
||||
}
|
||||
|
||||
final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
private let listView: ListView
|
||||
private let listBackgroundView: UIView
|
||||
private var currentEntries: [CommandChatInputContextPanelEntry]?
|
||||
private var contentOffsetChangeTransition: Transition?
|
||||
private var isAnimatingOut: Bool = false
|
||||
|
||||
private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = []
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
|
||||
@@ -78,20 +279,54 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
return strings.VoiceOver_ScrollStatus(row, count).string
|
||||
}
|
||||
|
||||
self.listBackgroundView = UIView()
|
||||
self.listBackgroundView.backgroundColor = theme.list.plainBackgroundColor
|
||||
self.listBackgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
self.listBackgroundView.layer.cornerRadius = 10.0
|
||||
|
||||
super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext)
|
||||
|
||||
self.isOpaque = false
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.view.addSubview(self.listBackgroundView)
|
||||
self.addSubnode(self.listView)
|
||||
|
||||
self.listView.visibleContentOffsetChanged = { [weak self] offset in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.isAnimatingOut {
|
||||
return
|
||||
}
|
||||
|
||||
var topItemOffset: CGFloat = self.listView.bounds.height
|
||||
var isFirst = true
|
||||
self.listView.forEachItemNode { itemNode in
|
||||
if isFirst {
|
||||
isFirst = false
|
||||
topItemOffset = itemNode.frame.minY
|
||||
}
|
||||
}
|
||||
|
||||
let transition: Transition = self.contentOffsetChangeTransition ?? .immediate
|
||||
transition.setFrame(view: self.listBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: topItemOffset), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0)))
|
||||
}
|
||||
}
|
||||
|
||||
func updateResults(_ results: [PeerCommand]) {
|
||||
func updateResults(_ results: [ChatInputTextCommand], accountPeer: EnginePeer?, hasShortcuts: Bool, query: String?) {
|
||||
var entries: [CommandChatInputContextPanelEntry] = []
|
||||
var index = 0
|
||||
var stableIds = Set<CommandChatInputContextPanelEntryStableId>()
|
||||
if hasShortcuts {
|
||||
let entry = CommandChatInputContextPanelEntry(index: index, content: .editShortcuts, theme: self.theme)
|
||||
stableIds.insert(entry.stableId)
|
||||
entries.append(entry)
|
||||
index += 1
|
||||
}
|
||||
for command in results {
|
||||
let entry = CommandChatInputContextPanelEntry(index: index, command: command, theme: self.theme)
|
||||
let entry = CommandChatInputContextPanelEntry(index: index, content: .command(CommandChatInputContextPanelEntry.Command(command: command, accountPeer: accountPeer, searchQuery: query)), theme: self.theme)
|
||||
if stableIds.contains(entry.stableId) {
|
||||
continue
|
||||
}
|
||||
@@ -106,7 +341,11 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
let firstTime = self.currentEntries == nil
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let transition = preparedTransition(from: from ?? [], to: to, context: self.context, presentationData: presentationData, commandSelected: { [weak self] command, sendImmediately in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else {
|
||||
return
|
||||
}
|
||||
switch command {
|
||||
case let .command(command):
|
||||
if sendImmediately {
|
||||
interfaceInteraction.sendBotCommand(command.peer, "/" + command.command.text)
|
||||
} else {
|
||||
@@ -132,7 +371,14 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
return (textInputState, inputMode)
|
||||
}
|
||||
}
|
||||
case let .shortcut(shortcut):
|
||||
interfaceInteraction.sendShortcut(shortcut)
|
||||
}
|
||||
}, openEditShortcuts: { [weak self] in
|
||||
guard let self, let interfaceInteraction = self.interfaceInteraction else {
|
||||
return
|
||||
}
|
||||
interfaceInteraction.openEditShortcuts()
|
||||
})
|
||||
self.currentEntries = to
|
||||
self.enqueueTransition(transition, firstTime: firstTime)
|
||||
@@ -153,16 +399,23 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
if firstTime {
|
||||
//options.insert(.Synchronous)
|
||||
//options.insert(.LowLatency)
|
||||
self.contentOffsetChangeTransition = .spring(duration: 0.4)
|
||||
|
||||
self.listBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.listView.bounds.height), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0))
|
||||
} else {
|
||||
options.insert(.AnimateTopItemPosition)
|
||||
options.insert(.AnimateCrossfade)
|
||||
if transition.itemCountChanged {
|
||||
options.insert(.AnimateTopItemPosition)
|
||||
options.insert(.AnimateCrossfade)
|
||||
}
|
||||
|
||||
self.contentOffsetChangeTransition = .spring(duration: 0.4)
|
||||
}
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.top = topInsetForLayout(size: validLayout.0)
|
||||
insets.top = topInsetForLayout(size: validLayout.0, hasShortcuts: transition.hasShortcuts)
|
||||
insets.left = validLayout.1
|
||||
insets.right = validLayout.2
|
||||
|
||||
@@ -178,19 +431,28 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
}
|
||||
|
||||
if let topItemOffset = topItemOffset {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
|
||||
let position = strongSelf.listView.layer.position
|
||||
strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset))
|
||||
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView {
|
||||
transition.animateView {
|
||||
strongSelf.listView.position = position
|
||||
}
|
||||
//transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.contentOffsetChangeTransition = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func topInsetForLayout(size: CGSize) -> CGFloat {
|
||||
let minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
|
||||
private func topInsetForLayout(size: CGSize, hasShortcuts: Bool) -> CGFloat {
|
||||
var minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
|
||||
if hasShortcuts {
|
||||
minimumItemHeights += VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round)
|
||||
}
|
||||
|
||||
return max(size.height - minimumItemHeights, 0.0)
|
||||
}
|
||||
|
||||
@@ -199,7 +461,16 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
self.validLayout = (size, leftInset, rightInset, bottomInset)
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.top = self.topInsetForLayout(size: size)
|
||||
var hasShortcuts = false
|
||||
if let currentEntries = self.currentEntries {
|
||||
hasShortcuts = currentEntries.contains(where: { entry in
|
||||
if case .editShortcuts = entry.content {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
insets.top = self.topInsetForLayout(size: size, hasShortcuts: hasShortcuts)
|
||||
insets.left = leftInset
|
||||
insets.right = rightInset
|
||||
|
||||
@@ -226,6 +497,8 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
}
|
||||
|
||||
override func animateOut(completion: @escaping () -> Void) {
|
||||
self.isAnimatingOut = true
|
||||
|
||||
var topItemOffset: CGFloat?
|
||||
self.listView.forEachItemNode { itemNode in
|
||||
if topItemOffset == nil {
|
||||
@@ -235,9 +508,12 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
|
||||
if let topItemOffset = topItemOffset {
|
||||
let position = self.listView.layer.position
|
||||
self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + (self.listView.bounds.size.height - topItemOffset)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
let offset = (self.listView.bounds.size.height - topItemOffset)
|
||||
self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.listBackgroundView.layer.animatePosition(from: self.listBackgroundView.layer.position, to: CGPoint(x: self.listBackgroundView.layer.position.x, y: self.listBackgroundView.layer.position.y + offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
})
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user