mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
3d709ba568
commit
3532108c30
@ -8150,3 +8150,8 @@ Sorry for the inconvenience.";
|
||||
"Channel.AdminLog.MessagePreviousLinks" = "Previous links";
|
||||
|
||||
"ShareMenu.SelectTopic" = "Select topic";
|
||||
|
||||
"ChatList.Context.Select" = "Select";
|
||||
|
||||
"ChatList.SelectedTopics_1" = "%@ Topic Selected";
|
||||
"ChatList.SelectedTopics_any" = "%@ Topics Selected";
|
||||
|
@ -738,6 +738,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
||||
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?) -> Signal<Never, NoError>
|
||||
func chatControllerForForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64) -> Signal<ChatController, NoError>
|
||||
func openStorageUsage(context: AccountContext)
|
||||
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController)
|
||||
func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void)
|
||||
|
@ -729,6 +729,13 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.separator)
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
|
||||
})))
|
||||
|
||||
return .single(items)
|
||||
}
|
||||
}
|
||||
|
@ -393,6 +393,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource)
|
||||
|
||||
self.tabBarItemContextActionType = .always
|
||||
self.automaticallyControlPresentationContextLayout = false
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
@ -489,14 +490,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
self.chatTitleDisposable = (combineLatest(queue: Queue.mainQueue(),
|
||||
peerView.get(),
|
||||
onlineMemberCount
|
||||
onlineMemberCount,
|
||||
self.chatListDisplayNode.containerNode.currentItemState
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, stateAndFilterId in
|
||||
guard let strongSelf = self, let chatTitleView = strongSelf.chatTitleView else {
|
||||
return
|
||||
}
|
||||
|
||||
chatTitleView.titleContent = .peer(peerView: peerView, customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil)
|
||||
if stateAndFilterId.state.editing && stateAndFilterId.state.selectedThreadIds.count > 0 {
|
||||
chatTitleView.titleContent = .custom(strongSelf.presentationData.strings.ChatList_SelectedTopics(Int32(stateAndFilterId.state.selectedThreadIds.count)), nil, false)
|
||||
} else {
|
||||
chatTitleView.titleContent = .peer(peerView: peerView, customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil)
|
||||
}
|
||||
|
||||
strongSelf.infoReady.set(.single(true))
|
||||
|
||||
if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, !channel.flags.contains(.isForum) {
|
||||
@ -1701,12 +1708,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
let peerIdsAndOptions: Signal<(ChatListSelectionOptions, Set<PeerId>)?, NoError> = self.chatListDisplayNode.containerNode.currentItemState
|
||||
|> map { state, filterId -> (Set<PeerId>, Int32?)? in
|
||||
let location = self.location
|
||||
let peerIdsAndOptions: Signal<(ChatListSelectionOptions, Set<PeerId>, Set<Int64>)?, NoError> = self.chatListDisplayNode.containerNode.currentItemState
|
||||
|> map { state, filterId -> (Set<PeerId>, Set<Int64>, Int32?)? in
|
||||
if !state.editing {
|
||||
return nil
|
||||
}
|
||||
return (state.selectedPeerIds, filterId)
|
||||
return (state.selectedPeerIds, state.selectedThreadIds, filterId)
|
||||
}
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||
if lhs?.0 != rhs?.0 {
|
||||
@ -1715,14 +1723,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if lhs?.1 != rhs?.1 {
|
||||
return false
|
||||
}
|
||||
if lhs?.2 != rhs?.2 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|> mapToSignal { selectedPeerIdsAndFilterId -> Signal<(ChatListSelectionOptions, Set<PeerId>)?, NoError> in
|
||||
if let (selectedPeerIds, filterId) = selectedPeerIdsAndFilterId {
|
||||
return chatListSelectionOptions(context: context, peerIds: selectedPeerIds, filterId: filterId)
|
||||
|> map { options -> (ChatListSelectionOptions, Set<PeerId>)? in
|
||||
return (options, selectedPeerIds)
|
||||
|> mapToSignal { selectedPeerIdsAndFilterId -> Signal<(ChatListSelectionOptions, Set<PeerId>, Set<Int64>)?, NoError> in
|
||||
if let (selectedPeerIds, selectedThreadIds, filterId) = selectedPeerIdsAndFilterId {
|
||||
switch location {
|
||||
case .chatList:
|
||||
return chatListSelectionOptions(context: context, peerIds: selectedPeerIds, filterId: filterId)
|
||||
|> map { options -> (ChatListSelectionOptions, Set<PeerId>, Set<Int64>)? in
|
||||
return (options, selectedPeerIds, selectedThreadIds)
|
||||
}
|
||||
case let .forum(peerId):
|
||||
return forumSelectionOptions(context: context, peerId: peerId, threadIds: selectedThreadIds, canDelete: false)
|
||||
|> map { options -> (ChatListSelectionOptions, Set<PeerId>, Set<Int64>)? in
|
||||
return (options, selectedPeerIds, selectedThreadIds)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
@ -1737,8 +1757,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
return
|
||||
}
|
||||
var toolbar: Toolbar?
|
||||
if case .chatList(.root) = strongSelf.location {
|
||||
if let (options, peerIds) = peerIdsAndOptions {
|
||||
if let (options, peerIds, _) = peerIdsAndOptions {
|
||||
if case .chatList(.root) = strongSelf.location {
|
||||
let leftAction: ToolbarAction
|
||||
switch options.read {
|
||||
case let .all(enabled):
|
||||
@ -1765,16 +1785,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: displayArchive ? ToolbarAction(title: presentationData.strings.ChatList_ArchiveAction, isEnabled: archiveEnabled) : nil)
|
||||
}
|
||||
} else {
|
||||
if let (options, peerIds) = peerIdsAndOptions {
|
||||
} else if case .forum = strongSelf.location {
|
||||
let leftAction: ToolbarAction
|
||||
switch options.read {
|
||||
case .all:
|
||||
leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: false)
|
||||
case let .selective(enabled):
|
||||
leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled)
|
||||
}
|
||||
toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: nil)
|
||||
} else {
|
||||
let middleAction = ToolbarAction(title: presentationData.strings.ChatList_UnarchiveAction, isEnabled: !peerIds.isEmpty)
|
||||
let leftAction: ToolbarAction
|
||||
switch options.read {
|
||||
case .all:
|
||||
leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: false)
|
||||
case let .selective(enabled):
|
||||
leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled)
|
||||
case .all:
|
||||
leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: false)
|
||||
case let .selective(enabled):
|
||||
leftAction = ToolbarAction(title: presentationData.strings.ChatList_Read, isEnabled: enabled)
|
||||
}
|
||||
toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: middleAction)
|
||||
}
|
||||
@ -1823,11 +1850,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.tabContainerNode.presentPremiumTip = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let context = strongSelf.context
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: nil, text: strongSelf.presentationData.strings.ChatListFolderSettings_SubscribeToMoveAll, customUndoText: strongSelf.presentationData.strings.ChatListFolderSettings_SubscribeToMoveAllAction), elevatedLayout: true, animateInAsReplacement: false, action: { action in
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: nil, text: strongSelf.presentationData.strings.ChatListFolderSettings_SubscribeToMoveAll, customUndoText: strongSelf.presentationData.strings.ChatListFolderSettings_SubscribeToMoveAllAction), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
strongSelf.push(PremiumIntroScreen(context: context, source: .folders))
|
||||
}
|
||||
return false }), in: .window(.root))
|
||||
return false }), in: .current)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2462,6 +2489,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
state.editing = false
|
||||
state.peerIdWithRevealedOptions = nil
|
||||
state.selectedPeerIds.removeAll()
|
||||
state.selectedThreadIds.removeAll()
|
||||
return state
|
||||
}
|
||||
self.chatListDisplayNode.isEditing = false
|
||||
|
@ -183,7 +183,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
|
||||
let timestamp1: Int32 = 100000
|
||||
let peers: [EnginePeer.Id: EnginePeer] = [:]
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in })
|
||||
|
||||
@ -1298,6 +1298,10 @@ final class ChatListControllerNode: ASDisplayNode {
|
||||
})
|
||||
}
|
||||
|
||||
var childrenLayout = layout
|
||||
childrenLayout.intrinsicInsets = UIEdgeInsets(top: visualNavigationHeight, left: childrenLayout.intrinsicInsets.left, bottom: childrenLayout.intrinsicInsets.bottom, right: childrenLayout.intrinsicInsets.right)
|
||||
self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
self.containerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, transition: transition)
|
||||
|
||||
|
@ -1845,6 +1845,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}, updatePeerGrouping: { _, _ in
|
||||
}, togglePeerMarkedUnread: { _, _ in
|
||||
}, toggleArchivedFolderHiddenByDefault: {
|
||||
}, toggleThreadsSelection: { _, _ in
|
||||
}, hidePsa: { _ in
|
||||
}, activateChatPreview: { item, node, gesture, location in
|
||||
guard let peerContextAction = interaction.peerContextAction else {
|
||||
@ -3053,7 +3054,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
var peers: [EnginePeer.Id: EnginePeer] = [:]
|
||||
peers[peer1.id] = peer1
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in })
|
||||
|
||||
|
@ -56,3 +56,20 @@ func chatListSelectionOptions(context: AccountContext, peerIds: Set<PeerId>, fil
|
||||
|> distinctUntilChanged
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func forumSelectionOptions(context: AccountContext, peerId: PeerId, threadIds: Set<Int64>, canDelete: Bool) -> Signal<ChatListSelectionOptions, NoError> {
|
||||
if threadIds.isEmpty {
|
||||
return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.PeerReadCounters(id: peerId))
|
||||
|> map { counters -> ChatListSelectionOptions in
|
||||
var hasUnread = false
|
||||
if counters.isUnread {
|
||||
hasUnread = true
|
||||
}
|
||||
return ChatListSelectionOptions(read: .all(enabled: hasUnread), delete: false)
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
} else {
|
||||
return .single(ChatListSelectionOptions(read: .selective(enabled: false), delete: canDelete))
|
||||
}
|
||||
}
|
||||
|
@ -333,13 +333,7 @@ private func groupReferenceRevealOptions(strings: PresentationStrings, theme: Pr
|
||||
private func forumRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isMuted: Bool?, isClosed: Bool, isPinned: Bool, isEditing: Bool, canManage: Bool) -> [ItemListRevealOption] {
|
||||
var options: [ItemListRevealOption] = []
|
||||
if !isEditing {
|
||||
if canManage {
|
||||
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 let isMuted = isMuted {
|
||||
if let isMuted = isMuted {
|
||||
if isMuted {
|
||||
options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
|
||||
} else {
|
||||
@ -1029,14 +1023,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
if case let .peer(_, peer, _, _, _, _, _, _, _, _, promoInfo, _, _, _, _) = item.content {
|
||||
if promoInfo == nil, let mainPeer = peer.peer {
|
||||
var threadId: Int64?
|
||||
switch item.index {
|
||||
case let .forum(_, _, threadIdValue, _, _):
|
||||
threadId = threadIdValue
|
||||
item.interaction.toggleThreadsSelection([threadIdValue], !item.selected)
|
||||
case .chatList:
|
||||
break
|
||||
item.interaction.togglePeerSelected(mainPeer, nil)
|
||||
}
|
||||
item.interaction.togglePeerSelected(mainPeer, threadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ public final class ChatListNodeInteraction {
|
||||
let updatePeerGrouping: (EnginePeer.Id, Bool) -> Void
|
||||
let togglePeerMarkedUnread: (EnginePeer.Id, Bool) -> Void
|
||||
let toggleArchivedFolderHiddenByDefault: () -> Void
|
||||
let toggleThreadsSelection: ([Int64], Bool) -> Void
|
||||
let hidePsa: (EnginePeer.Id) -> Void
|
||||
let activateChatPreview: (ChatListItem, ASDisplayNode, ContextGesture?, CGPoint?) -> Void
|
||||
let present: (ViewController) -> Void
|
||||
@ -109,6 +110,7 @@ public final class ChatListNodeInteraction {
|
||||
updatePeerGrouping: @escaping (EnginePeer.Id, Bool) -> Void,
|
||||
togglePeerMarkedUnread: @escaping (EnginePeer.Id, Bool) -> Void,
|
||||
toggleArchivedFolderHiddenByDefault: @escaping () -> Void,
|
||||
toggleThreadsSelection: @escaping ([Int64], Bool) -> Void,
|
||||
hidePsa: @escaping (EnginePeer.Id) -> Void,
|
||||
activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?, CGPoint?) -> Void,
|
||||
present: @escaping (ViewController) -> Void
|
||||
@ -133,6 +135,7 @@ public final class ChatListNodeInteraction {
|
||||
self.updatePeerGrouping = updatePeerGrouping
|
||||
self.togglePeerMarkedUnread = togglePeerMarkedUnread
|
||||
self.toggleArchivedFolderHiddenByDefault = toggleArchivedFolderHiddenByDefault
|
||||
self.toggleThreadsSelection = toggleThreadsSelection
|
||||
self.hidePsa = hidePsa
|
||||
self.activateChatPreview = activateChatPreview
|
||||
self.present = present
|
||||
@ -174,8 +177,9 @@ public struct ChatListNodeState: Equatable {
|
||||
public var hiddenPsaPeerId: EnginePeer.Id?
|
||||
public var foundPeers: [(EnginePeer, EnginePeer?)]
|
||||
public var selectedPeerMap: [EnginePeer.Id: EnginePeer]
|
||||
public var selectedThreadIds: Set<Int64>
|
||||
|
||||
public init(presentationData: ChatListPresentationData, editing: Bool, peerIdWithRevealedOptions: EnginePeer.Id?, selectedPeerIds: Set<EnginePeer.Id>, foundPeers: [(EnginePeer, EnginePeer?)], selectedPeerMap: [EnginePeer.Id: EnginePeer], selectedAdditionalCategoryIds: Set<Int>, peerInputActivities: ChatListNodePeerInputActivities?, pendingRemovalPeerIds: Set<EnginePeer.Id>, pendingClearHistoryPeerIds: Set<EnginePeer.Id>, archiveShouldBeTemporaryRevealed: Bool, hiddenPsaPeerId: EnginePeer.Id?) {
|
||||
public init(presentationData: ChatListPresentationData, editing: Bool, peerIdWithRevealedOptions: EnginePeer.Id?, selectedPeerIds: Set<EnginePeer.Id>, foundPeers: [(EnginePeer, EnginePeer?)], selectedPeerMap: [EnginePeer.Id: EnginePeer], selectedAdditionalCategoryIds: Set<Int>, peerInputActivities: ChatListNodePeerInputActivities?, pendingRemovalPeerIds: Set<EnginePeer.Id>, pendingClearHistoryPeerIds: Set<EnginePeer.Id>, archiveShouldBeTemporaryRevealed: Bool, hiddenPsaPeerId: EnginePeer.Id?, selectedThreadIds: Set<Int64>) {
|
||||
self.presentationData = presentationData
|
||||
self.editing = editing
|
||||
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
|
||||
@ -188,6 +192,7 @@ public struct ChatListNodeState: Equatable {
|
||||
self.pendingClearHistoryPeerIds = pendingClearHistoryPeerIds
|
||||
self.archiveShouldBeTemporaryRevealed = archiveShouldBeTemporaryRevealed
|
||||
self.hiddenPsaPeerId = hiddenPsaPeerId
|
||||
self.selectedThreadIds = selectedThreadIds
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool {
|
||||
@ -227,6 +232,9 @@ public struct ChatListNodeState: Equatable {
|
||||
if lhs.hiddenPsaPeerId != rhs.hiddenPsaPeerId {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedThreadIds != rhs.selectedThreadIds {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -396,6 +404,14 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
case .chatList:
|
||||
break
|
||||
}
|
||||
|
||||
var isForum = false
|
||||
if let peer = chatPeer, case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
isForum = true
|
||||
if editing {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
|
||||
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
|
||||
@ -406,7 +422,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
peer: peerContent,
|
||||
status: status,
|
||||
enabled: enabled,
|
||||
selection: editing ? .selectable(selected: selected) : .none,
|
||||
selection: editing && !isForum ? .selectable(selected: selected) : .none,
|
||||
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
|
||||
index: nil,
|
||||
header: header,
|
||||
@ -418,7 +434,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
nodeInteraction.peerSelected(chatPeer, nil, threadId, nil)
|
||||
}
|
||||
}
|
||||
}, disabledAction: { _ in
|
||||
}, disabledAction: isForum && editing ? nil : { _ in
|
||||
if let chatPeer = chatPeer {
|
||||
nodeInteraction.disabledPeerSelected(chatPeer, threadId)
|
||||
}
|
||||
@ -556,6 +572,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
break
|
||||
}
|
||||
|
||||
var isForum = false
|
||||
if let peer = chatPeer, case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
isForum = true
|
||||
if editing {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
|
||||
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
|
||||
sortOrder: presentationData.nameSortOrder,
|
||||
@ -565,7 +589,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
peer: peerContent,
|
||||
status: status,
|
||||
enabled: enabled,
|
||||
selection: editing ? .selectable(selected: selected) : .none,
|
||||
selection: editing && !isForum ? .selectable(selected: selected) : .none,
|
||||
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
|
||||
index: nil,
|
||||
header: header,
|
||||
@ -577,14 +601,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
nodeInteraction.peerSelected(chatPeer, nil, threadId, nil)
|
||||
}
|
||||
}
|
||||
}, disabledAction: { _ in
|
||||
}, disabledAction: isForum && editing ? nil : { _ in
|
||||
if let chatPeer = chatPeer {
|
||||
nodeInteraction.disabledPeerSelected(chatPeer, threadId)
|
||||
}
|
||||
},
|
||||
animationCache: nodeInteraction.animationCache,
|
||||
animationRenderer: nodeInteraction.animationRenderer
|
||||
), directionHint: entry.directionHint)
|
||||
), directionHint: entry.directionHint)
|
||||
}
|
||||
case let .HoleEntry(_, theme):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint)
|
||||
@ -809,7 +833,7 @@ public final class ChatListNode: ListView {
|
||||
isSelecting = true
|
||||
}
|
||||
|
||||
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil)
|
||||
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set())
|
||||
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
|
||||
|
||||
self.theme = theme
|
||||
@ -1033,6 +1057,23 @@ public final class ChatListNode: ListView {
|
||||
})
|
||||
}, toggleArchivedFolderHiddenByDefault: { [weak self] in
|
||||
self?.toggleArchivedFolderHiddenByDefault?()
|
||||
}, toggleThreadsSelection: { [weak self] threadIds, selected in
|
||||
self?.updateState { state in
|
||||
var state = state
|
||||
if selected {
|
||||
for threadId in threadIds {
|
||||
state.selectedThreadIds.insert(threadId)
|
||||
}
|
||||
} else {
|
||||
for threadId in threadIds {
|
||||
state.selectedThreadIds.remove(threadId)
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
if selected && !threadIds.isEmpty {
|
||||
self?.didBeginSelectingChats?()
|
||||
}
|
||||
}, hidePsa: { [weak self] id in
|
||||
self?.hidePsa?(id)
|
||||
}, activateChatPreview: { [weak self] item, node, gesture, location in
|
||||
@ -1065,7 +1106,7 @@ public final class ChatListNode: ListView {
|
||||
let currentRemovingPeerId = self.currentRemovingPeerId
|
||||
|
||||
let savedMessagesPeer: Signal<EnginePeer?, NoError>
|
||||
if case let .peers(filter, _, _, _) = mode, filter.contains(.onlyWriteable) {
|
||||
if case let .peers(filter, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location {
|
||||
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||
|> map(Optional.init)
|
||||
|> map { peer in
|
||||
@ -2326,7 +2367,25 @@ public final class ChatListNode: ListView {
|
||||
return resultPeer
|
||||
}
|
||||
|
||||
private func threadIdAtPoint(_ point: CGPoint) -> Int64? {
|
||||
var resultThreadId: Int64?
|
||||
self.forEachVisibleItemNode { itemNode in
|
||||
if resultThreadId == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) {
|
||||
if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item {
|
||||
switch item.content {
|
||||
case let .peer(_, _, threadInfo, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
resultThreadId = threadInfo?.id
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultThreadId
|
||||
}
|
||||
|
||||
private var selectionPanState: (selecting: Bool, initialPeerId: EnginePeer.Id, toggledPeerIds: [[EnginePeer.Id]])?
|
||||
private var threadSelectionPanState: (selecting: Bool, initialThreadId: Int64, toggledThreadIds: [[Int64]])?
|
||||
private var selectionScrollActivationTimer: SwiftSignalKit.Timer?
|
||||
private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator?
|
||||
private var selectionScrollDelta: CGFloat?
|
||||
@ -2336,15 +2395,25 @@ public final class ChatListNode: ListView {
|
||||
let location = recognizer.location(in: self.view)
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if let peer = self.peerAtPoint(location) {
|
||||
let selecting = !self.currentState.selectedPeerIds.contains(peer.id)
|
||||
self.selectionPanState = (selecting, peer.id, [])
|
||||
self.interaction?.togglePeersSelection([.peer(peer)], selecting)
|
||||
switch self .location {
|
||||
case .chatList:
|
||||
if let peer = self.peerAtPoint(location) {
|
||||
let selecting = !self.currentState.selectedPeerIds.contains(peer.id)
|
||||
self.selectionPanState = (selecting, peer.id, [])
|
||||
self.interaction?.togglePeersSelection([.peer(peer)], selecting)
|
||||
}
|
||||
case .forum:
|
||||
if let threadId = self.threadIdAtPoint(location) {
|
||||
let selecting = !self.currentState.selectedThreadIds.contains(threadId)
|
||||
self.threadSelectionPanState = (selecting, threadId, [])
|
||||
self.interaction?.toggleThreadsSelection([threadId], selecting)
|
||||
}
|
||||
}
|
||||
case .changed:
|
||||
self.handlePanSelection(location: location)
|
||||
self.selectionLastLocation = location
|
||||
case .ended, .failed, .cancelled:
|
||||
self.threadSelectionPanState = nil
|
||||
self.selectionPanState = nil
|
||||
self.selectionScrollDisplayLink = nil
|
||||
self.selectionScrollActivationTimer?.invalidate()
|
||||
@ -2367,66 +2436,111 @@ public final class ChatListNode: ListView {
|
||||
location.y = self.frame.height - self.insets.bottom - 5.0
|
||||
}
|
||||
|
||||
if let state = self.selectionPanState {
|
||||
if let peer = self.peerAtPoint(location) {
|
||||
if peer.id == state.initialPeerId {
|
||||
if !state.toggledPeerIds.isEmpty {
|
||||
self.interaction?.togglePeersSelection(state.toggledPeerIds.flatMap { $0.compactMap({ .peerId($0) }) }, !state.selecting)
|
||||
self.selectionPanState = (state.selecting, state.initialPeerId, [])
|
||||
}
|
||||
} else if state.toggledPeerIds.last?.first != peer.id {
|
||||
var updatedToggledPeerIds: [[EnginePeer.Id]] = []
|
||||
var previouslyToggled = false
|
||||
for i in (0 ..< state.toggledPeerIds.count) {
|
||||
if let peerId = state.toggledPeerIds[i].first {
|
||||
if peerId == peer.id {
|
||||
previouslyToggled = true
|
||||
updatedToggledPeerIds = Array(state.toggledPeerIds.prefix(i + 1))
|
||||
|
||||
let peerIdsToToggle = Array(state.toggledPeerIds.suffix(state.toggledPeerIds.count - i - 1)).flatMap { $0 }
|
||||
self.interaction?.togglePeersSelection(peerIdsToToggle.compactMap { .peerId($0) }, !state.selecting)
|
||||
break
|
||||
var hasState = false
|
||||
switch self.location {
|
||||
case .chatList:
|
||||
if let state = self.selectionPanState {
|
||||
hasState = true
|
||||
if let peer = self.peerAtPoint(location) {
|
||||
if peer.id == state.initialPeerId {
|
||||
if !state.toggledPeerIds.isEmpty {
|
||||
self.interaction?.togglePeersSelection(state.toggledPeerIds.flatMap { $0.compactMap({ .peerId($0) }) }, !state.selecting)
|
||||
self.selectionPanState = (state.selecting, state.initialPeerId, [])
|
||||
}
|
||||
} else if state.toggledPeerIds.last?.first != peer.id {
|
||||
var updatedToggledPeerIds: [[EnginePeer.Id]] = []
|
||||
var previouslyToggled = false
|
||||
for i in (0 ..< state.toggledPeerIds.count) {
|
||||
if let peerId = state.toggledPeerIds[i].first {
|
||||
if peerId == peer.id {
|
||||
previouslyToggled = true
|
||||
updatedToggledPeerIds = Array(state.toggledPeerIds.prefix(i + 1))
|
||||
|
||||
let peerIdsToToggle = Array(state.toggledPeerIds.suffix(state.toggledPeerIds.count - i - 1)).flatMap { $0 }
|
||||
self.interaction?.togglePeersSelection(peerIdsToToggle.compactMap { .peerId($0) }, !state.selecting)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !previouslyToggled {
|
||||
updatedToggledPeerIds = state.toggledPeerIds
|
||||
let isSelected = self.currentState.selectedPeerIds.contains(peer.id)
|
||||
if state.selecting != isSelected {
|
||||
updatedToggledPeerIds.append([peer.id])
|
||||
self.interaction?.togglePeersSelection([.peer(peer)], state.selecting)
|
||||
|
||||
if !previouslyToggled {
|
||||
updatedToggledPeerIds = state.toggledPeerIds
|
||||
let isSelected = self.currentState.selectedPeerIds.contains(peer.id)
|
||||
if state.selecting != isSelected {
|
||||
updatedToggledPeerIds.append([peer.id])
|
||||
self.interaction?.togglePeersSelection([.peer(peer)], state.selecting)
|
||||
}
|
||||
}
|
||||
|
||||
self.selectionPanState = (state.selecting, state.initialPeerId, updatedToggledPeerIds)
|
||||
}
|
||||
|
||||
self.selectionPanState = (state.selecting, state.initialPeerId, updatedToggledPeerIds)
|
||||
}
|
||||
}
|
||||
|
||||
let scrollingAreaHeight: CGFloat = 50.0
|
||||
if location.y < scrollingAreaHeight + self.insets.top || location.y > self.frame.height - scrollingAreaHeight - self.insets.bottom {
|
||||
if location.y < self.frame.height / 2.0 {
|
||||
self.selectionScrollDelta = (scrollingAreaHeight - (location.y - self.insets.top)) / scrollingAreaHeight
|
||||
} else {
|
||||
self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - self.insets.bottom - location.y)))) / scrollingAreaHeight
|
||||
}
|
||||
if let displayLink = self.selectionScrollDisplayLink {
|
||||
displayLink.isPaused = false
|
||||
} else {
|
||||
if let _ = self.selectionScrollActivationTimer {
|
||||
} else {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in
|
||||
self?.setupSelectionScrolling()
|
||||
}, queue: .mainQueue())
|
||||
timer.start()
|
||||
self.selectionScrollActivationTimer = timer
|
||||
case .forum:
|
||||
if let state = self.threadSelectionPanState {
|
||||
hasState = true
|
||||
if let threadId = self.threadIdAtPoint(location) {
|
||||
if threadId == state.initialThreadId {
|
||||
if !state.toggledThreadIds.isEmpty {
|
||||
self.interaction?.toggleThreadsSelection(Array(state.toggledThreadIds.joined()), !state.selecting)
|
||||
self.threadSelectionPanState = (state.selecting, state.initialThreadId, [])
|
||||
}
|
||||
} else if state.toggledThreadIds.last?.first != threadId {
|
||||
var updatedToggledThreadIds: [[Int64]] = []
|
||||
var previouslyToggled = false
|
||||
for i in (0 ..< state.toggledThreadIds.count) {
|
||||
if let toggledThreadId = state.toggledThreadIds[i].first {
|
||||
if toggledThreadId == threadId {
|
||||
previouslyToggled = true
|
||||
updatedToggledThreadIds = Array(state.toggledThreadIds.prefix(i + 1))
|
||||
|
||||
let threadIdsToToggle = Array(state.toggledThreadIds.suffix(state.toggledThreadIds.count - i - 1)).flatMap { $0 }
|
||||
self.interaction?.toggleThreadsSelection(threadIdsToToggle.compactMap { $0 }, !state.selecting)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !previouslyToggled {
|
||||
updatedToggledThreadIds = state.toggledThreadIds
|
||||
let isSelected = self.currentState.selectedThreadIds.contains(threadId)
|
||||
if state.selecting != isSelected {
|
||||
updatedToggledThreadIds.append([threadId])
|
||||
self.interaction?.toggleThreadsSelection([threadId], state.selecting)
|
||||
}
|
||||
}
|
||||
|
||||
self.threadSelectionPanState = (state.selecting, state.initialThreadId, updatedToggledThreadIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
guard hasState else {
|
||||
return
|
||||
}
|
||||
let scrollingAreaHeight: CGFloat = 50.0
|
||||
if location.y < scrollingAreaHeight + self.insets.top || location.y > self.frame.height - scrollingAreaHeight - self.insets.bottom {
|
||||
if location.y < self.frame.height / 2.0 {
|
||||
self.selectionScrollDelta = (scrollingAreaHeight - (location.y - self.insets.top)) / scrollingAreaHeight
|
||||
} else {
|
||||
self.selectionScrollDisplayLink?.isPaused = true
|
||||
self.selectionScrollActivationTimer?.invalidate()
|
||||
self.selectionScrollActivationTimer = nil
|
||||
self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - self.insets.bottom - location.y)))) / scrollingAreaHeight
|
||||
}
|
||||
if let displayLink = self.selectionScrollDisplayLink {
|
||||
displayLink.isPaused = false
|
||||
} else {
|
||||
if let _ = self.selectionScrollActivationTimer {
|
||||
} else {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in
|
||||
self?.setupSelectionScrolling()
|
||||
}, queue: .mainQueue())
|
||||
timer.start()
|
||||
self.selectionScrollActivationTimer = timer
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.selectionScrollDisplayLink?.isPaused = true
|
||||
self.selectionScrollActivationTimer?.invalidate()
|
||||
self.selectionScrollActivationTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -369,10 +369,6 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
||||
if let peerId {
|
||||
hasActiveRevealControls = peerId == state.peerIdWithRevealedOptions
|
||||
}
|
||||
var isSelected = false
|
||||
if let peerId {
|
||||
isSelected = state.selectedPeerIds.contains(peerId)
|
||||
}
|
||||
var inputActivities: [(EnginePeer, PeerInputActivity)]?
|
||||
if let peerId {
|
||||
inputActivities = state.peerInputActivities?.activities[peerId]
|
||||
@ -385,6 +381,13 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var isSelected = false
|
||||
if threadId != 0 {
|
||||
isSelected = state.selectedThreadIds.contains(threadId)
|
||||
} else if let peerId {
|
||||
isSelected = state.selectedPeerIds.contains(peerId)
|
||||
}
|
||||
|
||||
result.append(.PeerEntry(index: offsetPinnedIndex(entry.index, offset: pinnedIndexOffset), presentationData: state.presentationData, messages: updatedMessages, readState: updatedCombinedReadState, isRemovedFromTotalUnreadCount: entry.isMuted, draftState: draftState, peer: entry.renderedPeer, threadInfo: entry.threadData.flatMap { ChatListItemContent.ThreadInfo(id: threadId, info: $0.info, isOwner: $0.isOwnedByMe, isClosed: $0.isClosed) }, presence: entry.presence, hasUnseenMentions: entry.hasUnseenMentions, hasUnseenReactions: entry.hasUnseenReactions, editing: state.editing, hasActiveRevealControls: hasActiveRevealControls, selected: isSelected, inputActivities: inputActivities, promoInfo: nil, hasFailedMessages: entry.hasFailed, isContact: entry.isContact, forumTopicData: entry.forumTopicData))
|
||||
}
|
||||
|
@ -60,8 +60,8 @@ public enum ChatMessageBackgroundType: Equatable {
|
||||
}
|
||||
|
||||
public class ChatMessageBackground: ASDisplayNode {
|
||||
public weak var backdropNode: ASDisplayNode?
|
||||
|
||||
public weak var backdropNode: ChatMessageBubbleBackdrop?
|
||||
|
||||
public private(set) var type: ChatMessageBackgroundType?
|
||||
private var currentHighlighted: Bool?
|
||||
private var hasWallpaper: Bool?
|
||||
@ -85,7 +85,7 @@ public class ChatMessageBackground: ASDisplayNode {
|
||||
self.outlineImageNode.displayWithoutProcessing = true
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
self.addSubnode(self.outlineImageNode)
|
||||
self.addSubnode(self.imageNode)
|
||||
@ -259,9 +259,9 @@ public class ChatMessageBackground: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.imageNode.image = image
|
||||
if highlighted && maskMode, let backdropNode = self.backdropNode {
|
||||
if highlighted && maskMode, let backdropNode = self.backdropNode, backdropNode.hasImage {
|
||||
self.imageNode.layer.compositingFilter = "overlayBlendMode"
|
||||
self.imageNode.alpha = 1.0
|
||||
|
||||
@ -435,11 +435,17 @@ public final class ChatMessageBubbleBackdrop: ASDisplayNode {
|
||||
private var essentialGraphics: PrincipalThemeEssentialGraphics?
|
||||
private weak var backgroundNode: WallpaperBackgroundNode?
|
||||
|
||||
private var maskView: UIImageView?
|
||||
public var maskView: UIImageView?
|
||||
private var fixedMaskMode: Bool?
|
||||
|
||||
private var absolutePosition: (CGRect, CGSize)?
|
||||
|
||||
public var overrideMask: Bool = false {
|
||||
didSet {
|
||||
self.maskView?.image = nil
|
||||
}
|
||||
}
|
||||
|
||||
public var hasImage: Bool {
|
||||
return self.backgroundContent != nil
|
||||
}
|
||||
@ -475,7 +481,7 @@ public final class ChatMessageBubbleBackdrop: ASDisplayNode {
|
||||
self.setType(type: currentType, theme: theme, essentialGraphics: essentialGraphics, maskMode: maskMode, backgroundNode: backgroundNode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func setType(type: ChatMessageBackgroundType, theme: ChatPresentationThemeData, essentialGraphics: PrincipalThemeEssentialGraphics, maskMode inputMaskMode: Bool, backgroundNode: WallpaperBackgroundNode?) {
|
||||
let maskMode = self.fixedMaskMode ?? inputMaskMode
|
||||
|
||||
@ -555,11 +561,11 @@ public final class ChatMessageBubbleBackdrop: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let maskView = self.maskView {
|
||||
maskView.image = bubbleMaskForType(type, graphics: essentialGraphics)
|
||||
maskView.image = self.overrideMask ? nil : bubbleMaskForType(type, graphics: essentialGraphics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) {
|
||||
self.absolutePosition = (rect, containerSize)
|
||||
if let backgroundContent = self.backgroundContent {
|
||||
|
@ -970,7 +970,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
} else if peer.isDeleted {
|
||||
overrideImage = .deletedIcon
|
||||
}
|
||||
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
clipStyle = .roundedRect
|
||||
} else {
|
||||
clipStyle = .round
|
||||
}
|
||||
|
||||
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoads)
|
||||
}
|
||||
case let .deviceContact(_, contact):
|
||||
let letters: [String]
|
||||
|
@ -423,6 +423,21 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
}
|
||||
|
||||
func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
layer.animatePosition(from: fromValue, to: toValue, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animatePosition(node: ASDisplayNode, from position: CGPoint, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
@ -1455,6 +1470,31 @@ public extension ContainedViewLayoutTransition {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updatePath(layer: CAShapeLayer, path: CGPath, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
if layer.path == path {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
layer.removeAnimation(forKey: "path")
|
||||
layer.path = path
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let fromPath = layer.path
|
||||
layer.path = path
|
||||
layer.animate(from: fromPath, to: path, keyPath: "path", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CombinedTransition {
|
||||
@ -1600,6 +1640,7 @@ public protocol ControlledTransitionAnimator: AnyObject {
|
||||
func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?)
|
||||
func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?)
|
||||
func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?)
|
||||
func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, completion: ((Bool) -> Void)?)
|
||||
func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?)
|
||||
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?)
|
||||
func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?)
|
||||
@ -1752,14 +1793,14 @@ extension CGRect: AnyValueProviding {
|
||||
final class ControlledTransitionProperty {
|
||||
final class AnyValue: Equatable, CustomStringConvertible {
|
||||
let value: Any
|
||||
let nsValue: NSValue
|
||||
let nsValue: Any
|
||||
let stringValue: () -> String
|
||||
let isEqual: (AnyValue) -> Bool
|
||||
let interpolate: (AnyValue, CGFloat) -> AnyValue
|
||||
|
||||
init(
|
||||
value: Any,
|
||||
nsValue: NSValue,
|
||||
nsValue: Any,
|
||||
stringValue: @escaping () -> String,
|
||||
isEqual: @escaping (AnyValue) -> Bool,
|
||||
interpolate: @escaping (AnyValue, CGFloat) -> AnyValue
|
||||
@ -1951,6 +1992,16 @@ public final class ControlledTransition {
|
||||
))
|
||||
}
|
||||
|
||||
public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, completion: ((Bool) -> Void)?) {
|
||||
self.add(animation: ControlledTransitionProperty(
|
||||
layer: layer,
|
||||
path: "position",
|
||||
fromValue: fromValue,
|
||||
toValue: toValue,
|
||||
completion: completion
|
||||
))
|
||||
}
|
||||
|
||||
public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) {
|
||||
if layer.position == position {
|
||||
return
|
||||
@ -2059,6 +2110,10 @@ public final class ControlledTransition {
|
||||
self.transition.updatePosition(layer: layer, position: position, completion: completion)
|
||||
}
|
||||
|
||||
public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, completion: ((Bool) -> Void)?) {
|
||||
self.transition.animatePosition(layer: layer, from: fromValue, to: toValue, completion: completion)
|
||||
}
|
||||
|
||||
public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) {
|
||||
self.transition.updateBounds(layer: layer, bounds: bounds, completion: completion)
|
||||
}
|
||||
|
@ -147,6 +147,41 @@ public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, stroke
|
||||
})
|
||||
}
|
||||
|
||||
public func generateFilledRoundedRectImage(size: CGSize, cornerRadius: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
|
||||
return generateImage(CGSize(width: size.width, height: size.height), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
if let backgroundColor = backgroundColor {
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
|
||||
if let strokeColor = strokeColor, let strokeWidth = strokeWidth {
|
||||
context.setFillColor(strokeColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if let color = color {
|
||||
context.setFillColor(color.cgColor)
|
||||
} else {
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
}
|
||||
let path = CGPath(roundedRect: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0)), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
|
||||
context.addPath(path)
|
||||
context.fillPath()
|
||||
} else {
|
||||
if let color = color {
|
||||
context.setFillColor(color.cgColor)
|
||||
} else {
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
}
|
||||
let path = CGPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
|
||||
context.addPath(path)
|
||||
context.fillPath()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func generateAdjustedStretchableFilledCircleImage(diameter: CGFloat, color: UIColor) -> UIImage? {
|
||||
let corner: CGFloat = diameter / 2.0
|
||||
return generateImage(CGSize(width: diameter + 2.0, height: diameter + 2.0), contextGenerator: { size, context in
|
||||
|
@ -1600,9 +1600,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge)
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
}
|
||||
else
|
||||
{
|
||||
} else if self.itemNodes.isEmpty {
|
||||
self.scroller.contentSize = self.visibleSize
|
||||
if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero {
|
||||
self.scroller.contentOffset = .zero
|
||||
self.lastContentOffset = .zero
|
||||
}
|
||||
} else {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||
if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 {
|
||||
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize)
|
||||
|
@ -84,6 +84,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
}, updatePeerGrouping: { _, _ in
|
||||
}, togglePeerMarkedUnread: { _, _ in
|
||||
}, toggleArchivedFolderHiddenByDefault: {
|
||||
}, toggleThreadsSelection: { _, _ in
|
||||
}, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
|
@ -190,6 +190,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13
|
||||
}, initialValues: [:], queue: queue)
|
||||
}
|
||||
|
||||
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>, _ s14: Signal<T14, E>, _ s15: Signal<T15, E>, _ s16: Signal<T16, E>, _ s17: Signal<T17, E>, _ s18: Signal<T18, E>) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18), E> {
|
||||
return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18)], combine: { values in
|
||||
return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18)
|
||||
}, initialValues: [:], queue: queue)
|
||||
}
|
||||
|
||||
public func combineLatest<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
|
||||
if signals.count == 0 {
|
||||
return single([T](), E.self)
|
||||
|
@ -1216,13 +1216,21 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
}
|
||||
|
||||
private func updateIsEmpty(animated: Bool = false) {
|
||||
let isEmpty = (self.textField.text?.isEmpty ?? true) && self.tokens.isEmpty
|
||||
let textIsEmpty = (self.textField.text?.isEmpty ?? true)
|
||||
let isEmpty = textIsEmpty && self.tokens.isEmpty
|
||||
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate
|
||||
let placeholderTransition = !isEmpty ? .immediate : transition
|
||||
placeholderTransition.updateAlpha(node: self.textField.placeholderLabel, alpha: isEmpty ? 1.0 : 0.0)
|
||||
|
||||
let clearIsHidden = isEmpty && self.prefixString == nil
|
||||
var tokensEmpty = true
|
||||
for token in self.tokens {
|
||||
if !token.permanent {
|
||||
tokensEmpty = false
|
||||
}
|
||||
}
|
||||
|
||||
let clearIsHidden = (textIsEmpty && tokensEmpty) && self.prefixString == nil
|
||||
transition.updateAlpha(node: self.clearButton.imageNode, alpha: clearIsHidden ? 0.0 : 1.0)
|
||||
transition.updateTransformScale(node: self.clearButton, scale: clearIsHidden ? 0.2 : 1.0)
|
||||
self.clearButton.isUserInteractionEnabled = !clearIsHidden
|
||||
|
@ -219,7 +219,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
|
||||
var items: [ChatListItem] = []
|
||||
|
||||
let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in })
|
||||
|
@ -839,7 +839,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
||||
var items: [ChatListItem] = []
|
||||
|
||||
let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
|
@ -363,7 +363,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var items: [ChatListItem] = []
|
||||
|
||||
let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
|
@ -397,7 +397,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
|
||||
if let peersContentNode = strongSelf.peersContentNode {
|
||||
if let searchContentNode = strongSelf.contentNode as? ShareSearchContainerNode {
|
||||
searchContentNode.setContentOffsetUpdated(nil)
|
||||
let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - searchContentNode.contentGridNode.scrollView.contentOffset.y
|
||||
if let sourceFrame = searchContentNode.animateOut(peerId: peer.peerId, scrollDelta: scrollDelta) {
|
||||
topicsContentNode.animateIn(sourceFrame: sourceFrame, scrollDelta: scrollDelta)
|
||||
}
|
||||
} else if let peersContentNode = strongSelf.peersContentNode {
|
||||
peersContentNode.setContentOffsetUpdated(nil)
|
||||
let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - peersContentNode.contentGridNode.scrollView.contentOffset.y
|
||||
if let sourceFrame = peersContentNode.animateOut(peerId: peer.peerId, scrollDelta: scrollDelta) {
|
||||
@ -413,8 +419,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
func closePeerTopics(_ peerId: EnginePeer.Id) {
|
||||
if let topicsContentNode = self.topicsContentNode, let peersContentNode = self.peersContentNode {
|
||||
topicsContentNode.setContentOffsetUpdated(nil)
|
||||
guard let topicsContentNode = self.topicsContentNode else {
|
||||
return
|
||||
}
|
||||
topicsContentNode.setContentOffsetUpdated(nil)
|
||||
|
||||
if let searchContentNode = self.contentNode as? ShareSearchContainerNode {
|
||||
topicsContentNode.supernode?.insertSubnode(topicsContentNode, belowSubnode: searchContentNode)
|
||||
} else if let peersContentNode = self.peersContentNode {
|
||||
topicsContentNode.supernode?.insertSubnode(topicsContentNode, belowSubnode: peersContentNode)
|
||||
}
|
||||
|
||||
@ -422,14 +434,27 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
if let peersContentNode = self.peersContentNode {
|
||||
if let searchContentNode = self.contentNode as? ShareSearchContainerNode {
|
||||
searchContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in
|
||||
self?.contentNodeOffsetUpdated(contentOffset, transition: transition)
|
||||
})
|
||||
self.contentNodeOffsetUpdated(searchContentNode.contentGridNode.scrollView.contentOffset.y, transition: .animated(duration: 0.4, curve: .spring))
|
||||
|
||||
let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - searchContentNode.contentGridNode.scrollView.contentOffset.y
|
||||
if let targetFrame = searchContentNode.animateIn(peerId: peerId, scrollDelta: scrollDelta) {
|
||||
topicsContentNode.animateOut(targetFrame: targetFrame, scrollDelta: scrollDelta, completion: { [weak self] in
|
||||
if let topicsContentNode = self?.topicsContentNode {
|
||||
topicsContentNode.removeFromSupernode()
|
||||
self?.topicsContentNode = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if let peersContentNode = self.peersContentNode {
|
||||
peersContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in
|
||||
self?.contentNodeOffsetUpdated(contentOffset, transition: transition)
|
||||
})
|
||||
self.contentNodeOffsetUpdated(peersContentNode.contentGridNode.scrollView.contentOffset.y, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
if let peersContentNode = self.peersContentNode, let topicsContentNode = self.topicsContentNode {
|
||||
|
||||
let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - peersContentNode.contentGridNode.scrollView.contentOffset.y
|
||||
if let targetFrame = peersContentNode.animateIn(peerId: peerId, scrollDelta: scrollDelta) {
|
||||
topicsContentNode.animateOut(targetFrame: targetFrame, scrollDelta: scrollDelta, completion: { [weak self] in
|
||||
|
@ -17,7 +17,7 @@ import ContextUI
|
||||
|
||||
private let subtitleFont = Font.regular(12.0)
|
||||
|
||||
private extension CGPoint {
|
||||
extension CGPoint {
|
||||
func angle(to other: CGPoint) -> CGFloat {
|
||||
let originX = other.x - self.x
|
||||
let originY = other.y - self.y
|
||||
@ -377,22 +377,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func generateMaskImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 100.0, height: 100.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 16.0, dy: 16.0), cornerRadius: 16.0)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.setShadow(offset: .zero, blur: 40.0, color: UIColor.white.cgColor)
|
||||
|
||||
for _ in 0 ..< 10 {
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
}
|
||||
})?.stretchableImage(withLeftCapWidth: 49, topCapHeight: 49)
|
||||
}
|
||||
|
||||
|
||||
func animateIn(peerId: EnginePeer.Id, scrollDelta: CGFloat) -> CGRect? {
|
||||
self.headerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
@ -420,7 +405,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
let clippedNode = ASDisplayNode()
|
||||
clippedNode.clipsToBounds = true
|
||||
clippedNode.cornerRadius = 16.0
|
||||
clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.contentTitleNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset))
|
||||
clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.headerNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset + 15.0))
|
||||
self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view)
|
||||
|
||||
clippedNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
@ -429,7 +414,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
maskView.frame = clippedNode.bounds
|
||||
|
||||
let maskImageView = UIImageView()
|
||||
maskImageView.image = generateMaskImage()
|
||||
maskImageView.image = generatePeersMaskImage()
|
||||
maskImageView.frame = maskView.bounds.offsetBy(dx: 0.0, dy: 36.0)
|
||||
maskView.addSubview(maskImageView)
|
||||
clippedNode.view.mask = maskView
|
||||
@ -493,7 +478,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
let clippedNode = ASDisplayNode()
|
||||
clippedNode.clipsToBounds = true
|
||||
clippedNode.cornerRadius = 16.0
|
||||
clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.contentTitleNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset))
|
||||
clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.headerNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset + 15.0))
|
||||
self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view)
|
||||
|
||||
clippedNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
@ -502,7 +487,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
maskView.frame = clippedNode.bounds
|
||||
|
||||
let maskImageView = UIImageView()
|
||||
maskImageView.image = generateMaskImage()
|
||||
maskImageView.image = generatePeersMaskImage()
|
||||
maskImageView.frame = maskView.bounds.offsetBy(dx: 0.0, dy: 36.0)
|
||||
maskView.addSubview(maskImageView)
|
||||
clippedNode.view.mask = maskView
|
||||
@ -727,3 +712,18 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generatePeersMaskImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 100.0, height: 100.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 16.0, dy: 16.0), cornerRadius: 16.0)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.setShadow(offset: .zero, blur: 40.0, color: UIColor.white.cgColor)
|
||||
|
||||
for _ in 0 ..< 10 {
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
}
|
||||
})?.stretchableImage(withLeftCapWidth: 49, topCapHeight: 49)
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
private var enqueuedTransitions: [(ShareSearchGridTransaction, Bool)] = []
|
||||
private var enqueuedRecentTransitions: [(ShareSearchGridTransaction, Bool)] = []
|
||||
|
||||
private let contentGridNode: GridNode
|
||||
let contentGridNode: GridNode
|
||||
private let recentGridNode: GridNode
|
||||
|
||||
private let contentSeparatorNode: ASDisplayNode
|
||||
@ -632,4 +632,146 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
self.recentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
func frameForPeerId(_ peerId: EnginePeer.Id) -> CGRect? {
|
||||
var node: ASDisplayNode?
|
||||
if !self.recentGridNode.isHidden {
|
||||
self.recentGridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId {
|
||||
node = itemNode
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.contentGridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId {
|
||||
node = itemNode
|
||||
}
|
||||
}
|
||||
}
|
||||
if let node = node {
|
||||
return node.frame.offsetBy(dx: 0.0, dy: -10.0)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(peerId: EnginePeer.Id, scrollDelta: CGFloat) -> CGRect? {
|
||||
self.searchNode.alpha = 1.0
|
||||
self.searchNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.searchNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
self.cancelButtonNode.alpha = 1.0
|
||||
self.cancelButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.cancelButtonNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
self.contentGridNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
if let targetFrame = self.frameForPeerId(peerId), let (size, bottomInset) = self.validLayout {
|
||||
let clippedNode = ASDisplayNode()
|
||||
clippedNode.clipsToBounds = true
|
||||
clippedNode.cornerRadius = 16.0
|
||||
clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.searchNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset))
|
||||
self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view)
|
||||
|
||||
clippedNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
let maskView = UIView()
|
||||
maskView.frame = clippedNode.bounds
|
||||
|
||||
let maskImageView = UIImageView()
|
||||
maskImageView.image = generatePeersMaskImage()
|
||||
maskImageView.frame = maskView.bounds.offsetBy(dx: 0.0, dy: 36.0)
|
||||
maskView.addSubview(maskImageView)
|
||||
clippedNode.view.mask = maskView
|
||||
|
||||
|
||||
self.contentGridNode.alpha = 1.0
|
||||
self.contentGridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId {
|
||||
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false)
|
||||
itemNode.layer.animateScale(from: 1.35, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak clippedNode] _ in
|
||||
clippedNode?.view.removeFromSuperview()
|
||||
})
|
||||
} else if let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.frame = itemNode.view.convert(itemNode.bounds, to: clippedNode.view)
|
||||
|
||||
clippedNode.view.addSubview(snapshotView)
|
||||
|
||||
itemNode.alpha = 0.0
|
||||
let angle = targetFrame.center.angle(to: itemNode.position)
|
||||
let distance = targetFrame.center.distance(to: itemNode.position)
|
||||
let newDistance = distance * 2.8
|
||||
let newPosition = snapshotView.center.offsetBy(distance: newDistance, inDirection: angle)
|
||||
snapshotView.layer.animatePosition(from: newPosition, to: snapshotView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
snapshotView.layer.animateScale(from: 1.35, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak itemNode] _ in
|
||||
itemNode?.alpha = 1.0
|
||||
})
|
||||
snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
return targetFrame
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(peerId: EnginePeer.Id, scrollDelta: CGFloat) -> CGRect? {
|
||||
self.searchNode.alpha = 0.0
|
||||
self.searchNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
self.searchNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
self.cancelButtonNode.alpha = 0.0
|
||||
self.cancelButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
self.cancelButtonNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
self.contentGridNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
if let sourceFrame = self.frameForPeerId(peerId), let (size, bottomInset) = self.validLayout {
|
||||
let clippedNode = ASDisplayNode()
|
||||
clippedNode.clipsToBounds = true
|
||||
clippedNode.cornerRadius = 16.0
|
||||
clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.searchNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset))
|
||||
self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view)
|
||||
|
||||
clippedNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
let maskView = UIView()
|
||||
maskView.frame = clippedNode.bounds
|
||||
|
||||
let maskImageView = UIImageView()
|
||||
maskImageView.image = generatePeersMaskImage()
|
||||
maskImageView.frame = maskView.bounds.offsetBy(dx: 0.0, dy: 36.0)
|
||||
maskView.addSubview(maskImageView)
|
||||
clippedNode.view.mask = maskView
|
||||
|
||||
self.contentGridNode.forEachItemNode { itemNode in
|
||||
if let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.frame = itemNode.view.convert(itemNode.bounds, to: clippedNode.view)
|
||||
clippedNode.view.addSubview(snapshotView)
|
||||
|
||||
if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId {
|
||||
|
||||
} else {
|
||||
let angle = sourceFrame.center.angle(to: itemNode.position)
|
||||
let distance = sourceFrame.center.distance(to: itemNode.position)
|
||||
let newDistance = distance * 2.8
|
||||
let newPosition = snapshotView.center.offsetBy(distance: newDistance, inDirection: angle)
|
||||
snapshotView.layer.animatePosition(from: snapshotView.center, to: newPosition, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
snapshotView.layer.animateScale(from: 1.0, to: 1.35, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
||||
clippedNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak clippedNode] _ in
|
||||
clippedNode?.view.removeFromSuperview()
|
||||
})
|
||||
|
||||
self.contentGridNode.alpha = 0.0
|
||||
|
||||
return sourceFrame
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3501,7 +3501,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}, displaySwipeToReplyHint: { [weak self] in
|
||||
if let strongSelf = self, let validLayout = strongSelf.validLayout, min(validLayout.size.width, validLayout.size.height) > 320.0 {
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
|
||||
}
|
||||
}, dismissReplyMarkupMessage: { [weak self] message in
|
||||
guard let strongSelf = self, strongSelf.presentationInterfaceState.keyboardButtonsMessage?.id == message.id else {
|
||||
@ -4028,6 +4028,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self?.chatDisplayNode.cancelInteractiveKeyboardGestures()
|
||||
}, dismissTextInput: { [weak self] in
|
||||
self?.chatDisplayNode.dismissTextInput()
|
||||
}, scrollToMessageId: { [weak self] index in
|
||||
self?.chatDisplayNode.historyNode.scrollToMessage(index: index)
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
|
||||
|
||||
self.controllerInteraction = controllerInteraction
|
||||
@ -9916,7 +9918,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
self.tempVoicePlaylistItemChanged = { [weak self] previousItem, currentItem in
|
||||
guard let strongSelf = self, case .peer = strongSelf.chatLocation else {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -15078,7 +15080,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})
|
||||
}
|
||||
}
|
||||
controller.peerSelected = { [weak self, weak controller] peer, _ in
|
||||
controller.peerSelected = { [weak self, weak controller] peer, threadId in
|
||||
guard let strongSelf = self, let strongController = controller else {
|
||||
return
|
||||
}
|
||||
@ -15165,34 +15167,46 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
|
||||
let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: nil, { currentState in
|
||||
let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in
|
||||
return currentState.withUpdatedForwardMessageIds(messages.map { $0.id }).withUpdatedForwardOptionsState(ChatInterfaceForwardOptionsState(hideNames: !hasNotOwnMessages, hideCaptions: false, unhideNamesOnCaptionChange: false))
|
||||
})
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||||
|
||||
let navigationController: NavigationController?
|
||||
if let parentController = strongSelf.parentController {
|
||||
navigationController = (parentController.navigationController as? NavigationController)
|
||||
} else {
|
||||
navigationController = strongSelf.effectiveNavigationController
|
||||
}
|
||||
|
||||
if let navigationController = navigationController {
|
||||
let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId))
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers.insert(chatController, at: viewControllers.count - 1)
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
let proceed: (ChatController) -> Void = { chatController in
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||||
|
||||
strongSelf.controllerNavigationDisposable.set((chatController.ready.get()
|
||||
|> SwiftSignalKit.filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
let navigationController: NavigationController?
|
||||
if let parentController = strongSelf.parentController {
|
||||
navigationController = (parentController.navigationController as? NavigationController)
|
||||
} else {
|
||||
navigationController = strongSelf.effectiveNavigationController
|
||||
}
|
||||
|
||||
if let navigationController = navigationController {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
if threadId != nil {
|
||||
viewControllers.insert(chatController, at: viewControllers.count - 2)
|
||||
} else {
|
||||
viewControllers.insert(chatController, at: viewControllers.count - 1)
|
||||
}
|
||||
}))
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
|
||||
strongSelf.controllerNavigationDisposable.set((chatController.ready.get()
|
||||
|> SwiftSignalKit.filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak navigationController] _ in
|
||||
viewControllers.removeAll(where: { $0 is PeerSelectionController })
|
||||
navigationController?.setViewControllers(viewControllers, animated: true)
|
||||
}))
|
||||
}
|
||||
}
|
||||
if let threadId = threadId {
|
||||
let _ = (strongSelf.context.sharedContext.chatControllerForForumThread(context: strongSelf.context, peerId: peerId, threadId: threadId)
|
||||
|> deliverOnMainQueue).start(next: { chatController in
|
||||
proceed(chatController)
|
||||
})
|
||||
} else {
|
||||
proceed(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId)))
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -15295,7 +15309,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
case let .chat(textInputState, _, _):
|
||||
if let textInputState = textInputState {
|
||||
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData))
|
||||
controller.peerSelected = { [weak self, weak controller] peer, _ in
|
||||
controller.peerSelected = { [weak self, weak controller] peer, threadId in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongSelf = self, let strongController = controller {
|
||||
@ -15309,7 +15323,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})
|
||||
strongController.dismiss()
|
||||
} else {
|
||||
let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: nil, { currentState in
|
||||
let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in
|
||||
return currentState.withUpdatedComposeInputState(textInputState)
|
||||
})
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
|
@ -145,6 +145,7 @@ public final class ChatControllerInteraction {
|
||||
let requestMessageUpdate: (MessageId) -> Void
|
||||
let cancelInteractiveKeyboardGestures: () -> Void
|
||||
let dismissTextInput: () -> Void
|
||||
let scrollToMessageId: (MessageIndex) -> Void
|
||||
|
||||
var canPlayMedia: Bool = false
|
||||
var hiddenMedia: [MessageId: [Media]] = [:]
|
||||
@ -250,6 +251,7 @@ public final class ChatControllerInteraction {
|
||||
requestMessageUpdate: @escaping (MessageId) -> Void,
|
||||
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
||||
dismissTextInput: @escaping () -> Void,
|
||||
scrollToMessageId: @escaping (MessageIndex) -> Void,
|
||||
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
|
||||
pollActionState: ChatInterfacePollActionState,
|
||||
stickerSettings: ChatInterfaceStickerSettings,
|
||||
@ -339,6 +341,7 @@ public final class ChatControllerInteraction {
|
||||
self.requestMessageUpdate = requestMessageUpdate
|
||||
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
|
||||
self.dismissTextInput = dismissTextInput
|
||||
self.scrollToMessageId = scrollToMessageId
|
||||
|
||||
self.automaticMediaDownloadSettings = automaticMediaDownloadSettings
|
||||
|
||||
|
@ -221,16 +221,28 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var derivedLayoutState: ChatControllerNodeDerivedLayoutState?
|
||||
|
||||
private var isLoadingValue: Bool = false
|
||||
private func updateIsLoading(isLoading: Bool, animated: Bool) {
|
||||
private var isLoadingEarlier: Bool = false
|
||||
private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) {
|
||||
let useLoadingPlaceholder = self.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser
|
||||
|
||||
if isLoading != self.isLoadingValue {
|
||||
let updated = isLoading != self.isLoadingValue || (isLoading && earlier && !self.isLoadingEarlier)
|
||||
|
||||
if updated {
|
||||
let updatedIsLoading = self.isLoadingValue != isLoading
|
||||
self.isLoadingValue = isLoading
|
||||
|
||||
let updatedIsEarlier = self.isLoadingEarlier != earlier && !updatedIsLoading
|
||||
self.isLoadingEarlier = earlier
|
||||
|
||||
if isLoading {
|
||||
if useLoadingPlaceholder {
|
||||
let loadingPlaceholderNode: ChatLoadingPlaceholderNode
|
||||
if let current = self.loadingPlaceholderNode {
|
||||
loadingPlaceholderNode = current
|
||||
|
||||
if updatedIsEarlier {
|
||||
loadingPlaceholderNode.setup(self.historyNode, updating: true)
|
||||
}
|
||||
} else {
|
||||
loadingPlaceholderNode = ChatLoadingPlaceholderNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners, backgroundNode: self.backgroundNode)
|
||||
loadingPlaceholderNode.updatePresentationInterfaceState(self.chatPresentationInterfaceState)
|
||||
@ -238,7 +250,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
self.loadingPlaceholderNode = loadingPlaceholderNode
|
||||
|
||||
loadingPlaceholderNode.setup(self.historyNode)
|
||||
loadingPlaceholderNode.setup(self.historyNode, updating: false)
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate, listViewTransaction: { _, _, _, _ in
|
||||
@ -509,10 +521,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.historyNode.setLoadStateUpdated { [weak self] loadState, animated in
|
||||
if let strongSelf = self {
|
||||
let wasLoading = strongSelf.isLoadingValue
|
||||
if case .loading = loadState {
|
||||
strongSelf.updateIsLoading(isLoading: true, animated: animated)
|
||||
if case let .loading(earlier) = loadState {
|
||||
strongSelf.updateIsLoading(isLoading: true, earlier: earlier, animated: animated)
|
||||
} else {
|
||||
strongSelf.updateIsLoading(isLoading: false, animated: animated)
|
||||
strongSelf.updateIsLoading(isLoading: false, earlier: false, animated: animated)
|
||||
}
|
||||
|
||||
var emptyType: ChatHistoryNodeLoadState.EmptyType?
|
||||
@ -3190,8 +3202,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var attributes: [MessageAttribute] = []
|
||||
attributes.append(ForwardOptionsMessageAttribute(hideNames: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideNames == true, hideCaptions: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideCaptions == true))
|
||||
|
||||
var replyThreadId: Int64?
|
||||
if case let .replyThread(replyThreadMessage) = self.chatPresentationInterfaceState.chatLocation {
|
||||
replyThreadId = Int64(replyThreadMessage.messageId.id)
|
||||
}
|
||||
|
||||
for id in forwardMessageIds.sorted() {
|
||||
messages.append(.forward(source: id, threadId: nil, grouping: .auto, attributes: attributes, correlationId: nil))
|
||||
messages.append(.forward(source: id, threadId: replyThreadId, grouping: .auto, attributes: attributes, correlationId: nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -555,6 +555,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
}
|
||||
|
||||
private var appliedScrollToMessageId: MessageIndex? = nil
|
||||
private let scrollToMessageIdPromise = Promise<MessageIndex?>(nil)
|
||||
|
||||
private let currentlyPlayingMessageIdPromise = Promise<(MessageIndex, Bool)?>(nil)
|
||||
private var appliedPlayingMessageId: MessageIndex? = nil
|
||||
|
||||
@ -1043,11 +1046,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
customChannelDiscussionReadState,
|
||||
customThreadOutgoingReadState,
|
||||
self.currentlyPlayingMessageIdPromise.get(),
|
||||
self.scrollToMessageIdPromise.get(),
|
||||
adMessages,
|
||||
availableReactions,
|
||||
defaultReaction,
|
||||
accountPeer
|
||||
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageIdAndType, adMessages, availableReactions, defaultReaction, accountPeer in
|
||||
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageIdAndType, scrollToMessageId, adMessages, availableReactions, defaultReaction, accountPeer in
|
||||
let currentlyPlayingMessageId = currentlyPlayingMessageIdAndType?.0
|
||||
|
||||
func applyHole() {
|
||||
@ -1139,7 +1143,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|
||||
strongSelf._cachedPeerDataAndMessages.set(.single((cachedData, cachedDataMessages)))
|
||||
|
||||
let loadState: ChatHistoryNodeLoadState = .loading
|
||||
let loadState: ChatHistoryNodeLoadState = .loading(false)
|
||||
if strongSelf.loadState != loadState {
|
||||
strongSelf.loadState = loadState
|
||||
strongSelf.loadStateUpdated?(loadState, false)
|
||||
@ -1258,15 +1262,20 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|
||||
var scrollAnimationCurve: ListViewAnimationCurve? = nil
|
||||
if let strongSelf = self, case .default = source {
|
||||
let wasPlaying = strongSelf.appliedPlayingMessageId != nil
|
||||
|
||||
if strongSelf.appliedPlayingMessageId != currentlyPlayingMessageId, let (currentlyPlayingMessageId, currentlyPlayingVideo) = currentlyPlayingMessageIdAndType {
|
||||
if isFirstTime {
|
||||
} else if case let .peer(peerId) = chatLocation, currentlyPlayingMessageId.id.peerId != peerId {
|
||||
} else {
|
||||
if wasPlaying || currentlyPlayingVideo {
|
||||
updatedScrollPosition = .index(index: .message(currentlyPlayingMessageId), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true)
|
||||
scrollAnimationCurve = .Spring(duration: 0.4)
|
||||
if strongSelf.appliedScrollToMessageId == nil, let scrollToMessageId = scrollToMessageId {
|
||||
updatedScrollPosition = .index(index: .message(scrollToMessageId), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true)
|
||||
scrollAnimationCurve = .Spring(duration: 0.4)
|
||||
} else {
|
||||
let wasPlaying = strongSelf.appliedPlayingMessageId != nil
|
||||
|
||||
if strongSelf.appliedPlayingMessageId != currentlyPlayingMessageId, let (currentlyPlayingMessageId, currentlyPlayingVideo) = currentlyPlayingMessageIdAndType {
|
||||
if isFirstTime {
|
||||
} else if case let .peer(peerId) = chatLocation, currentlyPlayingMessageId.id.peerId != peerId {
|
||||
} else {
|
||||
if wasPlaying || currentlyPlayingVideo {
|
||||
updatedScrollPosition = .index(index: .message(currentlyPlayingMessageId), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true)
|
||||
scrollAnimationCurve = .Spring(duration: 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1337,6 +1346,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
if strongSelf.appliedPlayingMessageId != currentlyPlayingMessageId {
|
||||
strongSelf.appliedPlayingMessageId = currentlyPlayingMessageId
|
||||
}
|
||||
if strongSelf.appliedScrollToMessageId != scrollToMessageId {
|
||||
strongSelf.appliedScrollToMessageId = scrollToMessageId
|
||||
}
|
||||
strongSelf.enqueueHistoryViewTransition(mappedTransition)
|
||||
}
|
||||
}
|
||||
@ -2560,13 +2572,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
} else {
|
||||
if historyView.originalView.isLoadingEarlier && strongSelf.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser {
|
||||
loadState = .loading
|
||||
loadState = .loading(true)
|
||||
} else {
|
||||
loadState = .messages
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadState = .loading
|
||||
loadState = .loading(false)
|
||||
}
|
||||
|
||||
var animateIn = false
|
||||
@ -3343,6 +3355,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
self.currentlyPlayingMessageIdPromise.set(.single(nil))
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToMessage(index: MessageIndex) {
|
||||
self.appliedScrollToMessageId = nil
|
||||
self.scrollToMessageIdPromise.set(.single(index))
|
||||
}
|
||||
|
||||
private var currentSendAnimationCorrelationIds: Set<Int64>?
|
||||
func setCurrentSendAnimationCorrelationIds(_ value: Set<Int64>?) {
|
||||
|
@ -14,7 +14,7 @@ public enum ChatHistoryNodeLoadState: Equatable {
|
||||
case topic
|
||||
}
|
||||
|
||||
case loading
|
||||
case loading(Bool)
|
||||
case empty(EmptyType)
|
||||
case messages
|
||||
}
|
||||
|
@ -98,21 +98,7 @@ final class ChatLoadingPlaceholderMessageContainer {
|
||||
videoItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func animateBackgroundFrame(to frame: CGRect, transition: ContainedViewLayoutTransition) {
|
||||
// let targetFrame = CGRect(origin: CGPoint(x: self.bubbleNode.frame.minX, y: frame.minY), size: frame.size)
|
||||
//
|
||||
// transition.updateFrame(node: self.bubbleNode, frame: targetFrame)
|
||||
// transition.updateFrame(node: self.bubbleBorderNode, frame: targetFrame)
|
||||
//
|
||||
// if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode {
|
||||
// let avatarFrame = CGRect(origin: CGPoint(x: 3.0, y: frame.maxY + 1.0 - avatarSize.height), size: avatarSize)
|
||||
//
|
||||
// transition.updateFrame(node: avatarNode, frame: avatarFrame)
|
||||
// transition.updateFrame(node: avatarBorderNode, frame: avatarFrame)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
func update(size: CGSize, hasAvatar: Bool, rect: CGRect, transition: ContainedViewLayoutTransition) {
|
||||
var avatarOffset: CGFloat = 0.0
|
||||
|
||||
@ -151,7 +137,6 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode {
|
||||
private let maskNode: ASDisplayNode
|
||||
private let borderMaskNode: ASDisplayNode
|
||||
|
||||
private let scrollingContainer: ASDisplayNode
|
||||
private let containerNode: ASDisplayNode
|
||||
private var backgroundContent: WallpaperBubbleBackgroundNode?
|
||||
private let backgroundColorNode: ASDisplayNode
|
||||
@ -169,7 +154,6 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode {
|
||||
init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, backgroundNode: WallpaperBackgroundNode) {
|
||||
self.backgroundNode = backgroundNode
|
||||
|
||||
self.scrollingContainer = ASDisplayNode()
|
||||
self.maskNode = ASDisplayNode()
|
||||
self.borderMaskNode = ASDisplayNode()
|
||||
|
||||
@ -198,13 +182,11 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.scrollingContainer)
|
||||
|
||||
self.scrollingContainer.addSubnode(self.containerNode)
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.backgroundColorNode)
|
||||
self.containerNode.addSubnode(self.effectNode)
|
||||
|
||||
self.scrollingContainer.addSubnode(self.borderNode)
|
||||
self.addSubnode(self.borderNode)
|
||||
self.borderNode.addSubnode(self.borderEffectNode)
|
||||
}
|
||||
|
||||
@ -218,25 +200,73 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private var bottomInset: (Int, CGFloat)?
|
||||
func setup(_ historyNode: ChatHistoryNode) {
|
||||
func setup(_ historyNode: ChatHistoryNode, updating: Bool = false) {
|
||||
guard let listNode = historyNode as? ListView else {
|
||||
return
|
||||
}
|
||||
|
||||
var listItemNodes: [ASDisplayNode] = []
|
||||
var count = 0
|
||||
var inset: CGFloat = 0.0
|
||||
listNode.forEachVisibleItemNode { itemNode in
|
||||
inset += itemNode.frame.height
|
||||
count += 1
|
||||
|
||||
listItemNodes.append(itemNode)
|
||||
}
|
||||
|
||||
|
||||
if updating {
|
||||
let heightNorm = listNode.bounds.height - listNode.insets.top
|
||||
listNode.forEachItemHeaderNode { itemNode in
|
||||
var animateScale = true
|
||||
if itemNode is ChatMessageAvatarHeaderNode {
|
||||
animateScale = false
|
||||
}
|
||||
|
||||
let delayFactor = itemNode.frame.minY / heightNorm
|
||||
let delay = Double(delayFactor * 0.2)
|
||||
|
||||
itemNode.allowsGroupOpacity = true
|
||||
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay, completion: { [weak itemNode] _ in
|
||||
itemNode?.allowsGroupOpacity = false
|
||||
})
|
||||
if animateScale {
|
||||
itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
self.bottomInset = (count, inset)
|
||||
}
|
||||
|
||||
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: inset)
|
||||
if updating {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
|
||||
transition.animateOffsetAdditive(node: self.maskNode, offset: -inset)
|
||||
transition.animateOffsetAdditive(node: self.borderMaskNode, offset: -inset)
|
||||
|
||||
for listItemNode in listItemNodes {
|
||||
var incoming = false
|
||||
if let itemNode = listItemNode as? ChatMessageItemView, let item = itemNode.item, item.message.effectivelyIncoming(item.context.account.peerId) {
|
||||
incoming = true
|
||||
}
|
||||
|
||||
transition.animatePositionAdditive(node: listItemNode, offset: CGPoint(x: incoming ? 30.0 : -30.0, y: -30.0))
|
||||
transition.animateTransformScale(node: listItemNode, from: CGPoint(x: 0.85, y: 0.85))
|
||||
|
||||
listItemNode.allowsGroupOpacity = true
|
||||
listItemNode.alpha = 1.0
|
||||
listItemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in
|
||||
listItemNode.allowsGroupOpacity = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.maskNode.bounds = self.maskNode.bounds.offsetBy(dx: 0.0, dy: inset)
|
||||
self.borderMaskNode.bounds = self.borderMaskNode.bounds.offsetBy(dx: 0.0, dy: inset)
|
||||
}
|
||||
|
||||
|
||||
func animateOut(_ historyNode: ChatHistoryNode, completion: @escaping () -> Void = {}) {
|
||||
guard let listNode = historyNode as? ListView, let (size, _) = self.validLayout else {
|
||||
return
|
||||
@ -325,8 +355,10 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func addContentOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset)
|
||||
transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset)
|
||||
self.maskNode.bounds = self.maskNode.bounds.offsetBy(dx: 0.0, dy: -offset)
|
||||
self.borderMaskNode.bounds = self.borderMaskNode.bounds.offsetBy(dx: 0.0, dy: -offset)
|
||||
transition.animateOffsetAdditive(node: self.maskNode, offset: offset)
|
||||
transition.animateOffsetAdditive(node: self.borderMaskNode, offset: offset)
|
||||
if let (rect, containerSize) = self.absolutePosition {
|
||||
self.update(rect: rect, within: containerSize)
|
||||
}
|
||||
@ -337,7 +369,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode {
|
||||
if let backgroundContent = self.backgroundContent {
|
||||
var backgroundFrame = backgroundContent.frame
|
||||
backgroundFrame.origin.x += rect.minX
|
||||
backgroundFrame.origin.y += rect.minY - self.scrollingContainer.bounds.minY
|
||||
backgroundFrame.origin.y += rect.minY
|
||||
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition)
|
||||
}
|
||||
}
|
||||
@ -365,9 +397,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode {
|
||||
self.validLayout = (size, insets)
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
|
||||
transition.updateFrame(node: self.scrollingContainer, frame: bounds)
|
||||
|
||||
|
||||
transition.updateFrame(node: self.maskNode, frame: bounds)
|
||||
transition.updateFrame(node: self.borderMaskNode, frame: bounds)
|
||||
transition.updateFrame(node: self.containerNode, frame: bounds)
|
||||
@ -401,13 +431,13 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode {
|
||||
|
||||
var offset: CGFloat = 5.0
|
||||
var index = 0
|
||||
if let (insetCount, _) = self.bottomInset {
|
||||
index += insetCount
|
||||
}
|
||||
// if let (insetCount, _) = self.bottomInset {
|
||||
// index += insetCount
|
||||
// }
|
||||
|
||||
for messageContainer in self.messageContainers {
|
||||
let messageSize = dimensions[index % 11]
|
||||
messageContainer.update(size: size, hasAvatar: self.isGroup, rect: CGRect(origin: CGPoint(x: 0.0, y: size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition)
|
||||
messageContainer.update(size: bounds.size, hasAvatar: self.isGroup, rect: CGRect(origin: CGPoint(x: 0.0, y: bounds.size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition)
|
||||
offset += messageSize.height
|
||||
index += 1
|
||||
}
|
||||
|
@ -26,6 +26,23 @@ struct ChatMessageBubbleContentProperties {
|
||||
let hidesBackground: ChatMessageBubbleContentBackgroundHiding
|
||||
let forceFullCorners: Bool
|
||||
let forceAlignment: ChatMessageBubbleContentAlignment
|
||||
let shareButtonOffset: CGPoint?
|
||||
|
||||
init(
|
||||
hidesSimpleAuthorHeader: Bool,
|
||||
headerSpacing: CGFloat,
|
||||
hidesBackground: ChatMessageBubbleContentBackgroundHiding,
|
||||
forceFullCorners: Bool,
|
||||
forceAlignment: ChatMessageBubbleContentAlignment,
|
||||
shareButtonOffset: CGPoint? = nil
|
||||
) {
|
||||
self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader
|
||||
self.headerSpacing = headerSpacing
|
||||
self.hidesBackground = hidesBackground
|
||||
self.forceFullCorners = forceFullCorners
|
||||
self.forceAlignment = forceAlignment
|
||||
self.shareButtonOffset = shareButtonOffset
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatMessageBubbleNoneMergeStatus {
|
||||
@ -136,6 +153,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
weak var bubbleBackgroundNode: ChatMessageBackground?
|
||||
weak var bubbleBackdropNode: ChatMessageBubbleBackdrop?
|
||||
|
||||
var visibility: ListViewItemNodeVisibility = .none
|
||||
|
||||
@ -143,6 +161,10 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
||||
|
||||
var updateIsTextSelectionActive: ((Bool) -> Void)?
|
||||
|
||||
var disablesClipping: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
required override init() {
|
||||
super.init()
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
let isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil)
|
||||
if isVideo {
|
||||
if file.isInstantVideo {
|
||||
// result.append((message, ChatMessageInstantVideoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
result.append((message, ChatMessageInstantVideoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
} else {
|
||||
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty {
|
||||
messageWithCaptionToAdd = (message, itemAttributes)
|
||||
@ -1213,10 +1213,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
}
|
||||
|
||||
var isInstantVideo = false
|
||||
if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.source == nil, forwardInfo.author?.id.namespace == Namespaces.Peer.CloudUser {
|
||||
for media in item.content.firstMessage.media {
|
||||
if let file = media as? TelegramMediaFile, file.isMusic {
|
||||
ignoreForward = true
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if file.isMusic {
|
||||
ignoreForward = true
|
||||
} else if file.isInstantVideo {
|
||||
isInstantVideo = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -1329,12 +1334,22 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
tmpWidth -= deliveryFailedInset
|
||||
|
||||
let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset * 3.0 - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
|
||||
let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item)
|
||||
|
||||
var maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset * 3.0 - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
|
||||
|
||||
var hasInstantVideo = false
|
||||
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
||||
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
||||
if contentNodeItem.type == ChatMessageInstantVideoBubbleContentNode.self, !contentNodeItem.bubbleAttributes.isAttachment {
|
||||
maximumContentWidth = baseWidth - 20.0
|
||||
hasInstantVideo = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var contentPropertiesAndPrepareLayouts: [(Message, Bool, ChatMessageEntryAttributes, BubbleItemAttributes, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = []
|
||||
var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?
|
||||
|
||||
let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item)
|
||||
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
||||
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
||||
|
||||
@ -1505,6 +1520,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
}
|
||||
|
||||
var shareButtonOffset: CGPoint?
|
||||
var index = 0
|
||||
for (message, _, attributes, bubbleAttributes, prepareLayout) in contentPropertiesAndPrepareLayouts {
|
||||
let topPosition: ChatMessageBubbleRelativePosition
|
||||
@ -1560,6 +1576,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(contentItem, layoutConstants, prepareContentPosition, itemSelection, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth)
|
||||
|
||||
if let offset = properties.shareButtonOffset {
|
||||
shareButtonOffset = offset
|
||||
}
|
||||
|
||||
contentPropertiesAndLayouts.append((unboundSize, properties, prepareContentPosition, bubbleAttributes, nodeLayout, needSeparateContainers && !bubbleAttributes.isAttachment ? message.stableId : nil, itemSelection))
|
||||
|
||||
switch properties.hidesBackground {
|
||||
@ -1856,7 +1876,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
headerSize.height += nameNodeSizeApply.0.height
|
||||
}
|
||||
|
||||
if !ignoreForward, let forwardInfo = firstMessage.forwardInfo {
|
||||
if !ignoreForward && !isInstantVideo, let forwardInfo = firstMessage.forwardInfo {
|
||||
if headerSize.height.isZero {
|
||||
headerSize.height += 5.0
|
||||
}
|
||||
@ -1889,7 +1909,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
headerSize.height += forwardInfoSizeApply.0.height
|
||||
}
|
||||
|
||||
if let replyMessage = replyMessage {
|
||||
if !isInstantVideo, let replyMessage = replyMessage {
|
||||
if headerSize.height.isZero {
|
||||
headerSize.height += 6.0
|
||||
} else {
|
||||
@ -2234,6 +2254,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?
|
||||
if let reactionButtonsFinalize = reactionButtonsFinalize {
|
||||
var maxContentWidth = maxContentWidth
|
||||
if hasInstantVideo {
|
||||
maxContentWidth += 64.0
|
||||
}
|
||||
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
@ -2346,7 +2370,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
contentContainerNodeFrames: contentContainerNodeFrames,
|
||||
mosaicStatusOrigin: mosaicStatusOrigin,
|
||||
mosaicStatusSizeAndApply: mosaicStatusSizeAndApply,
|
||||
needsShareButton: needsShareButton
|
||||
needsShareButton: needsShareButton,
|
||||
shareButtonOffset: shareButtonOffset
|
||||
)
|
||||
})
|
||||
}
|
||||
@ -2390,7 +2415,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
contentContainerNodeFrames: [(UInt32, CGRect, Bool?, CGFloat)],
|
||||
mosaicStatusOrigin: CGPoint?,
|
||||
mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?,
|
||||
needsShareButton: Bool
|
||||
needsShareButton: Bool,
|
||||
shareButtonOffset: CGPoint?
|
||||
) -> Void {
|
||||
guard let strongSelf = selfReference.value else {
|
||||
return
|
||||
@ -2826,6 +2852,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
containerSupernode.addSubnode(contentNode)
|
||||
|
||||
contentNode.bubbleBackgroundNode = strongSelf.backgroundNode
|
||||
contentNode.bubbleBackdropNode = strongSelf.backgroundWallpaperNode
|
||||
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
|
||||
contextSourceNode?.updateDistractionFreeMode?(value)
|
||||
}
|
||||
@ -2858,6 +2885,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.contentNodes = sortedContentNodes
|
||||
}
|
||||
|
||||
var shouldClipOnTransitions = true
|
||||
var contentNodeIndex = 0
|
||||
for (relativeFrame, _, useContentOrigin, apply) in contentNodeFramesPropertiesAndApply {
|
||||
apply(animation, synchronousLoads, applyInfo)
|
||||
@ -2867,9 +2895,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
|
||||
let contentNode = strongSelf.contentNodes[contentNodeIndex]
|
||||
|
||||
if contentNode.disablesClipping {
|
||||
shouldClipOnTransitions = false
|
||||
}
|
||||
|
||||
let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: useContentOrigin ? contentOrigin.y : 0.0)
|
||||
let previousContentNodeFrame = contentNode.frame
|
||||
|
||||
|
||||
if case let .System(duration, _) = animation {
|
||||
var animateFrame = false
|
||||
var animateAlpha = false
|
||||
@ -3034,6 +3067,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
}
|
||||
|
||||
var isCurrentlyPlayingMedia = false
|
||||
if item.associatedData.currentlyPlayingMessageId == item.message.index {
|
||||
isCurrentlyPlayingMedia = true
|
||||
}
|
||||
|
||||
if case let .System(duration, _) = animation/*, !strongSelf.mainContextSourceNode.isExtractedToContextPreview*/ {
|
||||
if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) {
|
||||
if useDisplayLinkAnimations {
|
||||
@ -3052,7 +3090,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil)
|
||||
animation.animator.updatePosition(layer: strongSelf.clippingNode.layer, position: backgroundFrame.center, completion: nil)
|
||||
strongSelf.clippingNode.clipsToBounds = true
|
||||
strongSelf.clippingNode.clipsToBounds = shouldClipOnTransitions
|
||||
animation.animator.updateBounds(layer: strongSelf.clippingNode.layer, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size), completion: { [weak strongSelf] _ in
|
||||
strongSelf?.clippingNode.clipsToBounds = false
|
||||
})
|
||||
@ -3075,7 +3113,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let currentBackgroundFrame = strongSelf.backgroundNode.frame
|
||||
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true)
|
||||
animation.animator.updateFrame(layer: shareButtonNode.layer, frame: CGRect(origin: CGPoint(x: currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize), completion: nil)
|
||||
|
||||
var buttonFrame = CGRect(origin: CGPoint(x: currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize)
|
||||
if let shareButtonOffset = shareButtonOffset {
|
||||
buttonFrame = buttonFrame.offsetBy(dx: shareButtonOffset.x, dy: shareButtonOffset.y)
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: shareButtonNode.layer, frame: buttonFrame, completion: nil)
|
||||
animation.animator.updateAlpha(layer: shareButtonNode.layer, alpha: isCurrentlyPlayingMedia ? 0.0 : 1.0, completion: nil)
|
||||
|
||||
}
|
||||
} else {
|
||||
/*if let _ = strongSelf.backgroundFrameTransition {
|
||||
@ -3085,7 +3131,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true)
|
||||
shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize)
|
||||
|
||||
var buttonFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize)
|
||||
if let shareButtonOffset = shareButtonOffset {
|
||||
buttonFrame = buttonFrame.offsetBy(dx: shareButtonOffset.x, dy: shareButtonOffset.y)
|
||||
}
|
||||
shareButtonNode.frame = buttonFrame
|
||||
shareButtonNode.alpha = isCurrentlyPlayingMedia ? 0.0 : 1.0
|
||||
}
|
||||
|
||||
if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview {
|
||||
|
@ -6,6 +6,8 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import ComponentFlow
|
||||
import AudioTranscriptionButtonComponent
|
||||
|
||||
class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let interactiveFileNode: ChatMessageInteractiveFileNode
|
||||
@ -77,7 +79,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
if let item = self.item {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
|
@ -0,0 +1,408 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import ComponentFlow
|
||||
import AudioTranscriptionButtonComponent
|
||||
|
||||
class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let interactiveFileNode: ChatMessageInteractiveFileNode
|
||||
let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode
|
||||
|
||||
private let maskLayer = SimpleLayer()
|
||||
private let maskForeground = SimpleLayer()
|
||||
|
||||
private let backdropMaskLayer = SimpleLayer()
|
||||
private let backdropMaskForeground = SimpleShapeLayer()
|
||||
|
||||
private var isExpanded = false
|
||||
|
||||
private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
var wasVisible = false
|
||||
if case .visible = oldValue {
|
||||
wasVisible = true
|
||||
}
|
||||
var isVisible = false
|
||||
if case .visible = self.visibility {
|
||||
isVisible = true
|
||||
}
|
||||
if wasVisible != isVisible {
|
||||
self.interactiveVideoNode.visibility = isVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init() {
|
||||
self.interactiveFileNode = ChatMessageInteractiveFileNode()
|
||||
self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.maskForeground.backgroundColor = UIColor.white.cgColor
|
||||
self.maskForeground.masksToBounds = true
|
||||
self.maskLayer.addSublayer(self.maskForeground)
|
||||
|
||||
self.backdropMaskForeground.fillColor = UIColor.white.cgColor
|
||||
self.backdropMaskForeground.masksToBounds = true
|
||||
|
||||
self.addSubnode(self.interactiveFileNode)
|
||||
self.addSubnode(self.interactiveVideoNode)
|
||||
|
||||
self.interactiveVideoNode.requestUpdateLayout = { [weak self] _ in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||
}
|
||||
}
|
||||
self.interactiveVideoNode.updateTranscribeExpanded = { [weak self] state, text in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
strongSelf.audioTranscriptionState = state
|
||||
strongSelf.interactiveFileNode.audioTranscriptionState = state
|
||||
strongSelf.interactiveFileNode.forcedAudioTranscriptionText = text
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||
}
|
||||
}
|
||||
self.interactiveFileNode.updateTranscribeExpanded = { [weak self] state in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
strongSelf.audioTranscriptionState = state
|
||||
strongSelf.interactiveVideoNode.audioTranscriptionState = state
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||
}
|
||||
}
|
||||
|
||||
self.interactiveFileNode.toggleSelection = { [weak self] value in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
item.controllerInteraction.toggleMessagesSelection([item.message.id], value)
|
||||
}
|
||||
}
|
||||
|
||||
self.interactiveFileNode.activateLocalContent = { [weak self] in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
}
|
||||
}
|
||||
|
||||
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||
}
|
||||
}
|
||||
|
||||
self.interactiveFileNode.displayImportedTooltip = { [weak self] sourceNode in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
let _ = item.controllerInteraction.displayImportedMessageTooltip(sourceNode)
|
||||
}
|
||||
}
|
||||
|
||||
self.interactiveFileNode.dateAndStatusNode.reactionSelected = { [weak self] value in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
|
||||
self.interactiveFileNode.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
gesture?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value)
|
||||
}
|
||||
|
||||
self.interactiveFileNode.updateIsTextSelectionActive = { [weak self] value in
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
if let item = self.item {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
||||
let interactiveVideoLayout = self.interactiveVideoNode.asyncLayout()
|
||||
let interactiveFileLayout = self.interactiveFileNode.asyncLayout()
|
||||
|
||||
let currentExpanded = self.isExpanded
|
||||
let audioTranscriptionState = self.audioTranscriptionState
|
||||
let didSetupFileNode = self.item != nil
|
||||
|
||||
return { item, layoutConstants, preparePosition, selection, constrainedSize in
|
||||
var selectedFile: TelegramMediaFile?
|
||||
for media in item.message.media {
|
||||
if let telegramFile = media as? TelegramMediaFile {
|
||||
selectedFile = telegramFile
|
||||
}
|
||||
}
|
||||
|
||||
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch preparePosition {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
} else {
|
||||
if item.message.flags.contains(.Failed) {
|
||||
statusType = .BubbleOutgoing(.Failed)
|
||||
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
|
||||
statusType = .BubbleOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
|
||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
|
||||
|
||||
let (_, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
message: item.message,
|
||||
topMessage: item.topMessage,
|
||||
associatedData: item.associatedData,
|
||||
chatLocation: item.chatLocation,
|
||||
attributes: item.attributes,
|
||||
isPinned: item.isItemPinned,
|
||||
forcedIsEdited: item.isItemEdited,
|
||||
file: selectedFile!,
|
||||
automaticDownload: automaticDownload,
|
||||
incoming: item.message.effectivelyIncoming(item.context.account.peerId),
|
||||
isRecentActions: item.associatedData.isRecentActions,
|
||||
forcedResourceStatus: item.associatedData.forcedResourceStatus,
|
||||
dateAndStatusType: statusType,
|
||||
displayReactions: false,
|
||||
messageSelection: item.message.groupingKey != nil ? selection : nil,
|
||||
layoutConstants: layoutConstants,
|
||||
constrainedSize: CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height),
|
||||
controllerInteraction: item.controllerInteraction
|
||||
))
|
||||
|
||||
var isReplyThread = false
|
||||
if case .replyThread = item.chatLocation {
|
||||
isReplyThread = true
|
||||
}
|
||||
|
||||
let avatarInset: CGFloat = 0.0
|
||||
|
||||
var isExpanded = false
|
||||
if case .expanded = audioTranscriptionState {
|
||||
isExpanded = true
|
||||
}
|
||||
|
||||
var isPlaying = false
|
||||
let normalDisplaySize = layoutConstants.instantVideo.dimensions
|
||||
var displaySize = normalDisplaySize
|
||||
let maximumDisplaySize = CGSize(width: min(404, constrainedSize.width - 2.0), height: min(404, constrainedSize.width - 2.0))
|
||||
// var effectiveAvatarInset = avatarInset
|
||||
if item.associatedData.currentlyPlayingMessageId == item.message.index {
|
||||
isPlaying = true
|
||||
if !isExpanded {
|
||||
displaySize = maximumDisplaySize
|
||||
}
|
||||
// effectiveAvatarInset = 0.0
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 0.0
|
||||
let rightInset: CGFloat = 0.0
|
||||
|
||||
|
||||
|
||||
let (videoLayout, videoApply) = interactiveVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.attributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - leftInset - rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload)
|
||||
|
||||
let videoFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: videoLayout.contentSize)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none, shareButtonOffset: isExpanded ? .zero : CGPoint(x: -16.0, y: -24.0))
|
||||
|
||||
let width = videoFrame.width + 2.0
|
||||
// if isExpanded {
|
||||
// width = normalDisplaySize.width + 32.0
|
||||
// }
|
||||
|
||||
return (contentProperties, nil, width, { constrainedSize, position in
|
||||
var refinedWidth = videoFrame.width + 2.0
|
||||
var finishLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void))?
|
||||
|
||||
if isExpanded || !didSetupFileNode {
|
||||
(refinedWidth, finishLayout) = refineLayout(CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height))
|
||||
// refinedWidth = refinedWidth//max(refinedWidth, normalDisplaySize.width)
|
||||
refinedWidth += layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right
|
||||
}
|
||||
|
||||
if !isExpanded {
|
||||
refinedWidth = videoFrame.width + 2.0
|
||||
}
|
||||
|
||||
return (refinedWidth, { boundingWidth in
|
||||
var finalSize: CGSize
|
||||
var finalFileSize: CGSize?
|
||||
var finalFileApply: ((Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void)?
|
||||
if let finishLayout = finishLayout {
|
||||
let (fileSize, fileApply) = finishLayout(boundingWidth - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right)
|
||||
if isExpanded {
|
||||
finalSize = CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + layoutConstants.file.bubbleInsets.bottom)
|
||||
} else {
|
||||
finalSize = CGSize(width: boundingWidth, height: videoFrame.height + 2.0)
|
||||
}
|
||||
finalFileSize = fileSize
|
||||
finalFileApply = fileApply
|
||||
} else {
|
||||
finalSize = CGSize(width: boundingWidth, height: videoFrame.height + 2.0)
|
||||
}
|
||||
|
||||
return (finalSize, { [weak self] animation, synchronousLoads, applyInfo in
|
||||
if let strongSelf = self {
|
||||
let firstTime = strongSelf.item == nil
|
||||
strongSelf.item = item
|
||||
strongSelf.isExpanded = isExpanded
|
||||
|
||||
if currentExpanded != isExpanded {
|
||||
item.controllerInteraction.scrollToMessageId(item.message.index)
|
||||
}
|
||||
|
||||
if firstTime {
|
||||
strongSelf.interactiveFileNode.isHidden = true
|
||||
}
|
||||
|
||||
strongSelf.bubbleBackgroundNode?.layer.mask = strongSelf.maskLayer
|
||||
if let bubbleBackdropNode = strongSelf.bubbleBackdropNode, bubbleBackdropNode.hasImage && strongSelf.backdropMaskForeground.superlayer == nil {
|
||||
strongSelf.bubbleBackdropNode?.overrideMask = true
|
||||
strongSelf.bubbleBackdropNode?.maskView?.layer.addSublayer(strongSelf.backdropMaskForeground)
|
||||
}
|
||||
|
||||
strongSelf.maskLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 640.0, height: 640.0))
|
||||
strongSelf.backdropMaskLayer.frame = strongSelf.maskLayer.frame
|
||||
|
||||
let radius: CGFloat = displaySize.width / 2.0
|
||||
let maskCornerRadius = isExpanded ? 1.0 : radius
|
||||
let maskFrame = CGRect(origin: CGPoint(x: isExpanded ? 1.0 : (incoming ? 7.0 : 1.0), y: isExpanded ? 0.0 : 1.0), size: isExpanded ? finalSize : CGSize(width: radius * 2.0, height: radius * 2.0))
|
||||
animation.animator.updateCornerRadius(layer: strongSelf.maskForeground, cornerRadius: maskCornerRadius, completion: nil)
|
||||
animation.animator.updateFrame(layer: strongSelf.maskForeground, frame: maskFrame, completion: nil)
|
||||
|
||||
let backdropMaskFrame = CGRect(origin: CGPoint(x: isExpanded ? (incoming ? 8.0 : 2.0) : (incoming ? 8.0 : 2.0), y: isExpanded ? 2.0 : 2.0), size: isExpanded ? CGSize(width: finalSize.width - 11.0, height: finalSize.height - 2.0) : CGSize(width: radius * 2.0, height: radius * 2.0))
|
||||
|
||||
// let auxiliaryRadius = item.presentationData.chatBubbleCorners.auxiliaryRadius
|
||||
let backdropRadius = isExpanded ? item.presentationData.chatBubbleCorners.mainRadius : radius
|
||||
let path = CGPath(roundedRect: backdropMaskFrame, cornerWidth: backdropRadius, cornerHeight: backdropRadius, transform: nil)
|
||||
strongSelf.backdropMaskForeground.frame = strongSelf.maskLayer.frame
|
||||
animation.transition.updatePath(layer: strongSelf.backdropMaskForeground, path: path)
|
||||
|
||||
|
||||
let videoLayoutData: ChatMessageInstantVideoItemLayoutData
|
||||
if incoming {
|
||||
videoLayoutData = .constrained(left: 0.0, right: 0.0) //max(0.0, availableContentWidth - videoFrame.width))
|
||||
} else {
|
||||
videoLayoutData = .constrained(left: 0.0, right: 0.0)
|
||||
}
|
||||
|
||||
var videoAnimation = animation
|
||||
if currentExpanded != isExpanded {
|
||||
videoAnimation = .None
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.interactiveVideoNode.layer, frame: videoFrame, completion: nil)
|
||||
videoApply(videoLayoutData, videoAnimation)
|
||||
|
||||
if let fileSize = finalFileSize {
|
||||
strongSelf.interactiveFileNode.frame = CGRect(origin: CGPoint(x: layoutConstants.file.bubbleInsets.left, y: layoutConstants.file.bubbleInsets.top), size: fileSize)
|
||||
finalFileApply?(synchronousLoads, .None, applyInfo)
|
||||
}
|
||||
|
||||
if currentExpanded != isExpanded {
|
||||
if isExpanded {
|
||||
strongSelf.interactiveVideoNode.animateTo(strongSelf.interactiveFileNode, animator: animation.animator)
|
||||
} else {
|
||||
strongSelf.interactiveVideoNode.animateFrom(strongSelf.interactiveFileNode, animator: animation.animator)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
return nil
|
||||
}
|
||||
|
||||
override func updateHiddenMedia(_ media: [Media]?) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
self.interactiveVideoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.interactiveVideoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.interactiveVideoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func willUpdateIsExtractedToContextPreview(_ value: Bool) {
|
||||
// self.interactiveFileNode.willUpdateIsExtractedToContextPreview(value)
|
||||
}
|
||||
|
||||
override func updateIsExtractedToContextPreview(_ value: Bool) {
|
||||
// self.interactiveFileNode.updateIsExtractedToContextPreview(value)
|
||||
}
|
||||
|
||||
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
if !self.interactiveFileNode.isHidden {
|
||||
if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) {
|
||||
return .ignore
|
||||
}
|
||||
if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) {
|
||||
return .ignore
|
||||
}
|
||||
}
|
||||
if !self.interactiveVideoNode.isHidden {
|
||||
if self.interactiveVideoNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveVideoNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveVideoNode.dateAndStatusNode.view), with: nil) {
|
||||
return .ignore
|
||||
}
|
||||
if let audioTranscriptionButton = self.interactiveVideoNode.audioTranscriptionButton, let _ = audioTranscriptionButton.hitTest(self.view.convert(point, to: audioTranscriptionButton), with: nil) {
|
||||
return .ignore
|
||||
}
|
||||
}
|
||||
return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.isExpanded, let result = self.interactiveFileNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
if !self.isExpanded, let result = self.interactiveVideoNode.hitTest(self.view.convert(point, to: self.interactiveVideoNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
|
||||
if !self.interactiveVideoNode.dateAndStatusNode.isHidden {
|
||||
return self.interactiveVideoNode.dateAndStatusNode.reactionView(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override var disablesClipping: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
@ -32,12 +32,12 @@ private struct FetchControls {
|
||||
let cancel: () -> Void
|
||||
}
|
||||
|
||||
private enum TranscribedText {
|
||||
enum TranscribedText {
|
||||
case success(text: String, isPending: Bool)
|
||||
case error(AudioTranscriptionMessageAttribute.TranscriptionError)
|
||||
}
|
||||
|
||||
private func transcribedText(message: Message) -> TranscribedText? {
|
||||
func transcribedText(message: Message) -> TranscribedText? {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? AudioTranscriptionMessageAttribute {
|
||||
if !attribute.text.isEmpty {
|
||||
@ -127,10 +127,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private let titleNode: TextNode
|
||||
private let descriptionNode: TextNode
|
||||
private let descriptionMeasuringNode: TextNode
|
||||
private let fetchingTextNode: ImmediateTextNode
|
||||
private let fetchingCompactTextNode: ImmediateTextNode
|
||||
let fetchingTextNode: ImmediateTextNode
|
||||
let fetchingCompactTextNode: ImmediateTextNode
|
||||
|
||||
private var waveformView: ComponentHostView<Empty>?
|
||||
var waveformView: ComponentHostView<Empty>?
|
||||
|
||||
/*private let waveformNode: AudioWaveformNode
|
||||
private let waveformForegroundNode: AudioWaveformNode
|
||||
@ -138,9 +138,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private var waveformMaskNode: AudioWaveformNode?
|
||||
private var waveformScrubbingNode: MediaPlayerScrubbingNode?*/
|
||||
|
||||
private var audioTranscriptionButton: ComponentHostView<Empty>?
|
||||
var audioTranscriptionButton: ComponentHostView<Empty>?
|
||||
private var transcriptionPendingIndicator: ComponentHostView<Empty>?
|
||||
private let textNode: TextNode
|
||||
let textNode: TextNode
|
||||
private let textClippingNode: ASDisplayNode
|
||||
private var textSelectionNode: TextSelectionNode?
|
||||
|
||||
@ -151,7 +151,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
private var iconNode: TransformImageNode?
|
||||
let statusContainerNode: ContextExtractedContentContainingNode
|
||||
private var statusNode: SemanticStatusNode?
|
||||
var statusNode: SemanticStatusNode?
|
||||
private var playbackAudioLevelNode: VoiceBlobNode?
|
||||
private var streamingStatusNode: SemanticStatusNode?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
@ -199,6 +199,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
||||
var displayImportedTooltip: (ASDisplayNode) -> Void = { _ in }
|
||||
|
||||
var updateTranscribeExpanded: ((AudioTranscriptionButtonComponent.TranscriptionState) -> Void)?
|
||||
|
||||
private var context: AccountContext?
|
||||
private var message: Message?
|
||||
private var arguments: Arguments?
|
||||
@ -208,7 +210,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private var streamingCacheStatusFrame: CGRect?
|
||||
private var fileIconImage: UIImage?
|
||||
|
||||
private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
|
||||
var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
|
||||
var forcedAudioTranscriptionText: TranscribedText?
|
||||
private var transcribeDisposable: Disposable?
|
||||
var hasExpandedAudioTranscription: Bool {
|
||||
if case .expanded = audioTranscriptionState {
|
||||
@ -444,6 +447,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.audioTranscriptionState = .collapsed
|
||||
self.isWaitingForCollapse = true
|
||||
self.requestUpdateLayout(true)
|
||||
self.updateTranscribeExpanded?(self.audioTranscriptionState)
|
||||
case .collapsed:
|
||||
self.audioTranscriptionState = .inProgress
|
||||
self.requestUpdateLayout(true)
|
||||
@ -464,6 +468,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
let currentMessage = self.message
|
||||
let audioTranscriptionState = self.audioTranscriptionState
|
||||
let forcedAudioTranscriptionText = self.forcedAudioTranscriptionText
|
||||
|
||||
return { arguments in
|
||||
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
|
||||
@ -489,7 +494,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
statusUpdated = true
|
||||
}
|
||||
|
||||
let hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice
|
||||
let hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo
|
||||
|
||||
if mediaUpdated {
|
||||
if largestImageRepresentation(arguments.file.previewRepresentations) != nil || arguments.file.immediateThumbnailData != nil {
|
||||
@ -549,10 +554,20 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
let messageTheme = arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing
|
||||
|
||||
let isInstantVideo = arguments.file.isInstantVideo
|
||||
for attribute in arguments.file.attributes {
|
||||
if case let .Video(videoDuration, _, flags) = attribute, flags.contains(.instantRoundVideo) {
|
||||
isAudio = true
|
||||
isVoice = true
|
||||
|
||||
let durationString = stringForDuration(Int32(videoDuration))
|
||||
candidateDescriptionString = NSAttributedString(string: durationString, font: durationFont, textColor: messageTheme.fileDurationColor)
|
||||
}
|
||||
if case let .Audio(voice, duration, title, performer, waveform) = attribute {
|
||||
isAudio = true
|
||||
|
||||
let voice = voice || isInstantVideo
|
||||
|
||||
if let forcedResourceStatus = arguments.forcedResourceStatus, statusUpdated {
|
||||
updatedStatusSignal = .single((forcedResourceStatus, nil))
|
||||
} else if let currentUpdatedStatusSignal = updatedStatusSignal {
|
||||
@ -591,7 +606,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
candidateDescriptionString = NSAttributedString(string: descriptionText, font: descriptionFont, textColor: messageTheme.fileDescriptionColor)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -639,7 +653,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
var textString: NSAttributedString?
|
||||
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
|
||||
|
||||
let transcribedText = transcribedText(message: arguments.message)
|
||||
let transcribedText = forcedAudioTranscriptionText ?? transcribedText(message: arguments.message)
|
||||
|
||||
switch audioTranscriptionState {
|
||||
case .inProgress:
|
||||
@ -1352,13 +1366,16 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
var isVoice = false
|
||||
var audioDuration: Int32?
|
||||
for attribute in file.attributes {
|
||||
if case let .Audio(voice, duration, _, _, _) = attribute {
|
||||
if case let .Video(duration, _, flags) = attribute, flags.contains(.instantRoundVideo) {
|
||||
isAudio = true
|
||||
isVoice = true
|
||||
audioDuration = Int32(duration)
|
||||
} else if case let .Audio(voice, duration, _, _, _) = attribute {
|
||||
isAudio = true
|
||||
if voice {
|
||||
isVoice = true
|
||||
audioDuration = Int32(duration)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -1432,7 +1449,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
case .Local:
|
||||
if isAudio {
|
||||
if isAudio {
|
||||
state = .play
|
||||
} else if let fileIconImage = self.fileIconImage {
|
||||
state = .customIcon(fileIconImage)
|
||||
@ -1786,6 +1803,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
view.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
func animateTo(_ node: ChatMessageInteractiveInstantVideoNode) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -13,6 +13,8 @@ import PhotoResources
|
||||
import TelegramUniversalVideoContent
|
||||
import FileMediaResourceStatus
|
||||
import HierarchyTrackingLayer
|
||||
import ComponentFlow
|
||||
import AudioTranscriptionButtonComponent
|
||||
|
||||
struct ChatMessageInstantVideoItemLayoutResult {
|
||||
let contentSize: CGSize
|
||||
@ -44,14 +46,27 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
var customIsHidden: Bool = false {
|
||||
didSet {
|
||||
if self.customIsHidden != oldValue {
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.videoNode?.canAttachContent = self.shouldAcquireVideoContext
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var videoNode: UniversalVideoNode?
|
||||
private let secretVideoPlaceholderBackground: ASImageNode
|
||||
private let secretVideoPlaceholder: TransformImageNode
|
||||
|
||||
var audioTranscriptionButton: ComponentHostView<Empty>?
|
||||
|
||||
private var statusNode: RadialStatusNode?
|
||||
private var disappearingStatusNode: RadialStatusNode?
|
||||
private var playbackStatusNode: InstantVideoRadialStatusNode?
|
||||
private(set) var videoFrame: CGRect?
|
||||
private var imageScale: CGFloat = 1.0
|
||||
|
||||
private var item: ChatMessageBubbleContentItem?
|
||||
private var automaticDownload: Bool?
|
||||
@ -80,7 +95,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
private let fetchedThumbnailDisposable = MetaDisposable()
|
||||
|
||||
private var shouldAcquireVideoContext: Bool {
|
||||
if self.visibility && self.trackingIsInHierarchy {
|
||||
if self.visibility && self.trackingIsInHierarchy && !self.customIsHidden {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -97,6 +112,20 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
|
||||
var shouldOpen: () -> Bool = { return true }
|
||||
|
||||
var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
|
||||
var audioTranscriptionText: TranscribedText?
|
||||
private var transcribeDisposable: Disposable?
|
||||
var hasExpandedAudioTranscription: Bool {
|
||||
if case .expanded = audioTranscriptionState {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
private var isWaitingForCollapse: Bool = false
|
||||
|
||||
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
||||
|
||||
override init() {
|
||||
self.secretVideoPlaceholderBackground = ASImageNode()
|
||||
self.secretVideoPlaceholderBackground.isLayerBacked = true
|
||||
@ -138,7 +167,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
recognizer.tapActionAtPoint = { _ in
|
||||
recognizer.tapActionAtPoint = { point in
|
||||
return .waitForSingleTap
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
@ -169,6 +198,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
|
||||
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
|
||||
|
||||
let audioTranscriptionState = self.audioTranscriptionState
|
||||
|
||||
return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusDisplayType, automaticDownload in
|
||||
var secretVideoPlaceholderBackgroundImage: UIImage?
|
||||
var updatedInfoBackgroundImage: UIImage?
|
||||
@ -176,9 +207,10 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
|
||||
var updatedInstantVideoBackgroundImage: UIImage?
|
||||
let instantVideoBackgroundImage: UIImage?
|
||||
|
||||
switch statusDisplayType {
|
||||
case .free:
|
||||
instantVideoBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
||||
instantVideoBackgroundImage = nil
|
||||
case .bubble:
|
||||
instantVideoBackgroundImage = nil
|
||||
}
|
||||
@ -355,12 +387,32 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
|
||||
let result = ChatMessageInstantVideoItemLayoutResult(contentSize: contentSize, overflowLeft: 0.0, overflowRight: dateAndStatusOverflow ? 0.0 : (max(0.0, floorToScreenPixels(videoFrame.midX) + 55.0 + dateAndStatusSize.width - videoFrame.width)))
|
||||
|
||||
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
|
||||
let transcribedText = transcribedText(message: item.message)
|
||||
|
||||
switch audioTranscriptionState {
|
||||
case .inProgress:
|
||||
if transcribedText != nil {
|
||||
updatedAudioTranscriptionState = .expanded
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let effectiveAudioTranscriptionState = updatedAudioTranscriptionState ?? audioTranscriptionState
|
||||
|
||||
return (result, { [weak self] layoutData, animation in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.videoFrame = displayVideoFrame
|
||||
strongSelf.secretProgressIcon = secretProgressIcon
|
||||
strongSelf.automaticDownload = automaticDownload
|
||||
|
||||
if let updatedAudioTranscriptionState = updatedAudioTranscriptionState {
|
||||
strongSelf.audioTranscriptionState = updatedAudioTranscriptionState
|
||||
strongSelf.audioTranscriptionText = transcribedText
|
||||
strongSelf.updateTranscribeExpanded?(updatedAudioTranscriptionState, transcribedText)
|
||||
}
|
||||
|
||||
if let updatedInfoBackgroundImage = updatedInfoBackgroundImage {
|
||||
strongSelf.infoBackgroundNode.image = updatedInfoBackgroundImage
|
||||
@ -383,7 +435,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
if let infoBackgroundImage = strongSelf.infoBackgroundNode.image, let muteImage = strongSelf.muteIconNode.image {
|
||||
let infoWidth = muteImage.size.width
|
||||
let infoBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(displayVideoFrame.minX + (displayVideoFrame.size.width - infoWidth) / 2.0), y: displayVideoFrame.maxY - infoBackgroundImage.size.height - 8.0), size: CGSize(width: infoWidth, height: infoBackgroundImage.size.height))
|
||||
strongSelf.infoBackgroundNode.frame = infoBackgroundFrame
|
||||
animation.animator.updateFrame(layer: strongSelf.infoBackgroundNode.layer, frame: infoBackgroundFrame, completion: nil)
|
||||
|
||||
let muteIconFrame = CGRect(origin: CGPoint(x: infoBackgroundFrame.width - muteImage.size.width, y: 0.0), size: muteImage.size)
|
||||
strongSelf.muteIconNode.frame = muteIconFrame
|
||||
}
|
||||
@ -395,40 +448,26 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
strongSelf.fetchedThumbnailDisposable.set(nil)
|
||||
}
|
||||
}
|
||||
|
||||
dateAndStatusApply(animation)
|
||||
switch layoutData {
|
||||
case let .unconstrained(width):
|
||||
let dateAndStatusOrigin: CGPoint
|
||||
if dateAndStatusOverflow {
|
||||
dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0)
|
||||
} else {
|
||||
dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height)
|
||||
}
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize), completion: nil)
|
||||
case let .constrained(_, right):
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize), completion: nil)
|
||||
|
||||
var durationBlurColor: (UIColor, Bool)?
|
||||
let durationTextColor: UIColor
|
||||
switch statusDisplayType {
|
||||
case .free:
|
||||
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
||||
durationTextColor = serviceColor.primaryText
|
||||
durationBlurColor = (selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper))
|
||||
case .bubble:
|
||||
durationBlurColor = nil
|
||||
if item.message.effectivelyIncoming(item.context.account.peerId) {
|
||||
durationTextColor = theme.theme.chat.message.incoming.secondaryTextColor
|
||||
} else {
|
||||
durationTextColor = theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var updatedPlayerStatusSignal: Signal<MediaPlayerStatus?, NoError>?
|
||||
if let telegramFile = updatedFile {
|
||||
if updatedMedia {
|
||||
let durationTextColor: UIColor
|
||||
let durationBlurColor: (UIColor, Bool)?
|
||||
switch statusDisplayType {
|
||||
case .free:
|
||||
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
||||
durationTextColor = serviceColor.primaryText
|
||||
durationBlurColor = (selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper))
|
||||
case .bubble:
|
||||
durationBlurColor = nil
|
||||
if item.message.effectivelyIncoming(item.context.account.peerId) {
|
||||
durationTextColor = theme.theme.chat.message.incoming.secondaryTextColor
|
||||
} else {
|
||||
durationTextColor = theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
}
|
||||
}
|
||||
|
||||
if let durationBlurColor = durationBlurColor {
|
||||
if let durationBackgroundNode = strongSelf.durationBackgroundNode {
|
||||
durationBackgroundNode.updateColor(color: durationBlurColor.0, enableBlur: durationBlurColor.1, transition: .immediate)
|
||||
@ -525,28 +564,114 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}))
|
||||
}
|
||||
|
||||
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
|
||||
var canTranscribe = statusDisplayType == .free && item.associatedData.isPremium && item.message.id.peerId.namespace != Namespaces.Peer.SecretChat
|
||||
if canTranscribe, let durationBlurColor = durationBlurColor {
|
||||
let audioTranscriptionButton: ComponentHostView<Empty>
|
||||
if let current = strongSelf.audioTranscriptionButton {
|
||||
audioTranscriptionButton = current
|
||||
} else {
|
||||
audioTranscriptionButton = ComponentHostView<Empty>()
|
||||
strongSelf.audioTranscriptionButton = audioTranscriptionButton
|
||||
strongSelf.view.addSubview(audioTranscriptionButton)
|
||||
}
|
||||
let audioTranscriptionButtonSize = audioTranscriptionButton.update(
|
||||
transition: animation.isAnimated ? .easeInOut(duration: 0.3) : .immediate,
|
||||
component: AnyComponent(AudioTranscriptionButtonComponent(
|
||||
theme: .freeform(durationBlurColor),
|
||||
transcriptionState: effectiveAudioTranscriptionState,
|
||||
pressed: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.transcribe()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
|
||||
var audioTranscriptionButtonFrame: CGRect
|
||||
if incoming {
|
||||
audioTranscriptionButtonFrame = CGRect(origin: CGPoint(x: displayVideoFrame.maxX - 30.0, y: displayVideoFrame.maxY - 30.0), size: audioTranscriptionButtonSize)
|
||||
if !scaleProgress.isZero {
|
||||
audioTranscriptionButtonFrame.origin.x = displayVideoFrame.midX + 43.0
|
||||
}
|
||||
} else {
|
||||
audioTranscriptionButtonFrame = CGRect(origin: CGPoint(x: displayVideoFrame.minX, y: displayVideoFrame.maxY - 30.0), size: audioTranscriptionButtonSize)
|
||||
if !scaleProgress.isZero {
|
||||
audioTranscriptionButtonFrame.origin.x = displayVideoFrame.midX - 74.0
|
||||
}
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: audioTranscriptionButton.layer, frame: audioTranscriptionButtonFrame, completion: nil)
|
||||
|
||||
animation.animator.updateAlpha(layer: audioTranscriptionButton.layer, alpha: scaleProgress.isZero ? 1.0 : 0.0, completion: nil)
|
||||
if !scaleProgress.isZero {
|
||||
canTranscribe = false
|
||||
}
|
||||
} else {
|
||||
if let audioTranscriptionButton = strongSelf.audioTranscriptionButton {
|
||||
strongSelf.audioTranscriptionButton = nil
|
||||
audioTranscriptionButton.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if let durationNode = strongSelf.durationNode {
|
||||
durationNode.frame = CGRect(origin: CGPoint(x: displayVideoFrame.midX - 56.0 - 25.0 * scaleProgress, y: displayVideoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
var durationFrame = CGRect(origin: CGPoint(x: displayVideoFrame.midX - 56.0 - 25.0 * scaleProgress, y: displayVideoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
animation.animator.updateFrame(layer: durationNode.layer, frame: durationFrame, completion: nil)
|
||||
|
||||
durationNode.isSeen = !notConsumed
|
||||
let size = durationNode.size
|
||||
if let durationBackgroundNode = strongSelf.durationBackgroundNode, size.width > 1.0 {
|
||||
durationBackgroundNode.frame = CGRect(origin: CGPoint(x: durationNode.frame.maxX - size.width, y: durationNode.frame.minY), size: size)
|
||||
durationBackgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate)
|
||||
|
||||
if !incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, canTranscribe {
|
||||
durationFrame.origin.x = audioTranscriptionButton.frame.minX - 7.0
|
||||
}
|
||||
animation.animator.updateFrame(layer: durationNode.layer, frame: durationFrame, completion: nil)
|
||||
animation.animator.updateFrame(layer: durationBackgroundNode.layer, frame: CGRect(origin: CGPoint(x: durationNode.frame.maxX - size.width, y: durationNode.frame.minY), size: size), completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
dateAndStatusApply(animation)
|
||||
switch layoutData {
|
||||
case let .unconstrained(width):
|
||||
var dateAndStatusOrigin: CGPoint
|
||||
if dateAndStatusOverflow {
|
||||
dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0)
|
||||
} else {
|
||||
dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height)
|
||||
if !incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, canTranscribe {
|
||||
dateAndStatusOrigin.x = audioTranscriptionButton.frame.maxX + 7.0
|
||||
}
|
||||
}
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize), completion: nil)
|
||||
case let .constrained(_, right):
|
||||
var dateAndStatusFrame = CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize)
|
||||
if incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, canTranscribe {
|
||||
dateAndStatusFrame.origin.x = audioTranscriptionButton.frame.maxX + 7.0
|
||||
}
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||
}
|
||||
|
||||
if let videoNode = strongSelf.videoNode {
|
||||
videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
|
||||
videoNode.transform = CATransform3DMakeScale(imageScale, imageScale, 1.0)
|
||||
videoNode.position = displayVideoFrame.center
|
||||
videoNode.updateLayout(size: arguments.boundingSize, transition: .immediate)
|
||||
if imageScale != strongSelf.imageScale {
|
||||
strongSelf.imageScale = imageScale
|
||||
animation.animator.updateScale(layer: videoNode.layer, scale: imageScale, completion: nil)
|
||||
}
|
||||
animation.animator.updatePosition(layer: videoNode.layer, position: displayVideoFrame.center, completion: nil)
|
||||
videoNode.updateLayout(size: arguments.boundingSize, transition: animation.transition)
|
||||
}
|
||||
strongSelf.secretVideoPlaceholderBackground.frame = displayVideoFrame
|
||||
animation.animator.updateFrame(layer: strongSelf.secretVideoPlaceholderBackground.layer, frame: displayVideoFrame, completion: nil)
|
||||
|
||||
let placeholderFrame = videoFrame.insetBy(dx: 2.0, dy: 2.0)
|
||||
strongSelf.secretVideoPlaceholder.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
|
||||
strongSelf.secretVideoPlaceholder.transform = CATransform3DMakeScale(imageScale, imageScale, 1.0)
|
||||
strongSelf.secretVideoPlaceholder.position = displayVideoFrame.center
|
||||
animation.animator.updateScale(layer: strongSelf.secretVideoPlaceholder.layer, scale: imageScale, completion: nil)
|
||||
animation.animator.updatePosition(layer: strongSelf.secretVideoPlaceholder.layer, position: displayVideoFrame.center, completion: nil)
|
||||
|
||||
let makeSecretPlaceholderLayout = strongSelf.secretVideoPlaceholder.asyncLayout()
|
||||
let arguments = TransformImageArguments(corners: ImageCorners(radius: placeholderFrame.size.width / 2.0), imageSize: placeholderFrame.size, boundingSize: placeholderFrame.size, intrinsicInsets: UIEdgeInsets())
|
||||
let applySecretPlaceholder = makeSecretPlaceholderLayout(arguments)
|
||||
@ -748,6 +873,11 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
self.addSubnode(playbackStatusNode)
|
||||
|
||||
if let audioTranscriptionButton = self.audioTranscriptionButton {
|
||||
audioTranscriptionButton.superview?.bringSubviewToFront(audioTranscriptionButton)
|
||||
}
|
||||
|
||||
self.playbackStatusNode = playbackStatusNode
|
||||
}
|
||||
playbackStatusNode.frame = videoFrame.insetBy(dx: 1.5, dy: 1.5)
|
||||
@ -779,6 +909,11 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if let audioTranscriptionButton = self.audioTranscriptionButton, !audioTranscriptionButton.isHidden, audioTranscriptionButton.frame.contains(location) {
|
||||
self.transcribe()
|
||||
return
|
||||
}
|
||||
|
||||
if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(location) {
|
||||
self.progressPressed()
|
||||
return
|
||||
@ -822,6 +957,9 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
if let audioTranscriptionButton = self.audioTranscriptionButton, !audioTranscriptionButton.isHidden, audioTranscriptionButton.frame.contains(point) {
|
||||
return audioTranscriptionButton
|
||||
}
|
||||
if let playbackNode = self.playbackStatusNode, !self.isPlaying, !playbackNode.frame.insetBy(dx: 0.2 * playbackNode.frame.width, dy: 0.2 * playbackNode.frame.height).contains(point) {
|
||||
let distanceFromCenter = point.distanceTo(playbackNode.position)
|
||||
if distanceFromCenter < 0.2 * playbackNode.frame.width {
|
||||
@ -833,6 +971,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(point) {
|
||||
return self.view
|
||||
}
|
||||
|
||||
if let videoNode = self.videoNode, videoNode.frame.contains(point) {
|
||||
return self.view
|
||||
}
|
||||
@ -990,5 +1129,225 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updateTranscribeExpanded: ((AudioTranscriptionButtonComponent.TranscriptionState, TranscribedText?) -> Void)?
|
||||
private func transcribe() {
|
||||
guard let context = self.item?.context, let message = self.item?.message else {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldBeginTranscription = false
|
||||
var shouldExpandNow = false
|
||||
|
||||
if case .expanded = self.audioTranscriptionState {
|
||||
shouldExpandNow = true
|
||||
} else {
|
||||
if let result = transcribedText(message: message) {
|
||||
shouldExpandNow = true
|
||||
|
||||
if case let .success(_, isPending) = result {
|
||||
shouldBeginTranscription = isPending
|
||||
} else {
|
||||
shouldBeginTranscription = true
|
||||
}
|
||||
} else {
|
||||
shouldBeginTranscription = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldBeginTranscription {
|
||||
if self.transcribeDisposable == nil {
|
||||
self.audioTranscriptionState = .inProgress
|
||||
self.requestUpdateLayout(true)
|
||||
|
||||
self.transcribeDisposable = (context.engine.messages.transcribeAudio(messageId: message.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.transcribeDisposable = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if shouldExpandNow {
|
||||
switch self.audioTranscriptionState {
|
||||
case .expanded:
|
||||
self.audioTranscriptionState = .collapsed
|
||||
self.isWaitingForCollapse = true
|
||||
self.requestUpdateLayout(true)
|
||||
case .collapsed:
|
||||
self.audioTranscriptionState = .inProgress
|
||||
self.requestUpdateLayout(true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTranscribeExpanded?(self.audioTranscriptionState, self.audioTranscriptionText)
|
||||
}
|
||||
|
||||
func animateTo(_ node: ChatMessageInteractiveFileNode, animator: ControlledTransitionAnimator) {
|
||||
let duration: Double = 0.2
|
||||
|
||||
node.alpha = 1.0
|
||||
node.isHidden = false
|
||||
|
||||
self.alpha = 0.0
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
|
||||
node.waveformView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
|
||||
|
||||
if let videoNode = self.videoNode, let targetNode = node.statusNode, let videoSnapshotView = videoNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
videoSnapshotView.frame = videoNode.bounds
|
||||
videoNode.view.insertSubview(videoSnapshotView, at: 1)
|
||||
videoSnapshotView.alpha = 0.0
|
||||
videoSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak videoSnapshotView] _ in
|
||||
videoSnapshotView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
let targetFrame = targetNode.view.convert(targetNode.bounds, to: self.view)
|
||||
animator.animatePosition(layer: videoNode.layer, from: videoNode.position, to: targetFrame.center, completion: { _ in
|
||||
self.isHidden = true
|
||||
self.customIsHidden = true
|
||||
})
|
||||
let targetScale = targetNode.frame.width / videoNode.bounds.width
|
||||
animator.animateScale(layer: videoNode.layer, from: self.imageScale, to: targetScale, completion: nil)
|
||||
|
||||
animator.animatePosition(layer: self.infoBackgroundNode.layer, from: self.infoBackgroundNode.position, to: targetFrame.center.offsetBy(dx: 0.0, dy: 19.0), completion: nil)
|
||||
animator.animateScale(layer: self.infoBackgroundNode.layer, from: 1.0, to: targetScale / self.imageScale, completion: nil)
|
||||
self.infoBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
|
||||
if let playbackStatusNode = self.playbackStatusNode {
|
||||
animator.animatePosition(layer: playbackStatusNode.layer, from: playbackStatusNode.position, to: targetFrame.center, completion: nil)
|
||||
animator.animateScale(layer: playbackStatusNode.layer, from: 1.0, to: targetScale / self.imageScale, completion: nil)
|
||||
playbackStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
}
|
||||
|
||||
let sourceFrame = self.view.convert(videoNode.frame, to: node.view)
|
||||
animator.animatePosition(layer: targetNode.layer, from: sourceFrame.center, to: targetNode.position, completion: nil)
|
||||
let sourceScale = (videoNode.bounds.width * self.imageScale) / targetNode.frame.width
|
||||
animator.animateScale(layer: targetNode.layer, from: sourceScale, to: 1.0, completion: nil)
|
||||
targetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
|
||||
let verticalDelta = (videoNode.position.y - targetFrame.center.y) * 2.0
|
||||
animator.animatePosition(layer: node.textNode.layer, from: node.textNode.position.offsetBy(dx: 0.0, dy: verticalDelta), to: node.textNode.position, completion: nil)
|
||||
node.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
}
|
||||
|
||||
if let audioTranscriptionButton = self.audioTranscriptionButton, let targetAudioTranscriptionButton = node.audioTranscriptionButton {
|
||||
let sourceFrame = audioTranscriptionButton.convert(audioTranscriptionButton.bounds, to: node.view)
|
||||
|
||||
animator.animatePosition(layer: targetAudioTranscriptionButton.layer, from: sourceFrame.center, to: targetAudioTranscriptionButton.center, completion: nil)
|
||||
targetAudioTranscriptionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
|
||||
let targetFrame = targetAudioTranscriptionButton.convert(targetAudioTranscriptionButton.bounds, to: self.view)
|
||||
animator.animatePosition(layer: audioTranscriptionButton.layer, from: audioTranscriptionButton.center, to: targetFrame.center, completion: nil)
|
||||
audioTranscriptionButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
}
|
||||
|
||||
let sourceDateFrame = self.dateAndStatusNode.view.convert(self.dateAndStatusNode.view.bounds, to: node.view)
|
||||
let targetDateFrame = node.dateAndStatusNode.view.convert(node.dateAndStatusNode.view.bounds, to: self.view)
|
||||
|
||||
animator.animatePosition(layer: self.dateAndStatusNode.layer, from: self.dateAndStatusNode.position, to: CGPoint(x: targetDateFrame.maxX - self.dateAndStatusNode.frame.width / 2.0 + 2.0, y: targetDateFrame.midY - 7.0), completion: nil)
|
||||
animator.animatePosition(layer: node.dateAndStatusNode.layer, from: CGPoint(x: sourceDateFrame.maxX - node.dateAndStatusNode.frame.width / 2.0, y: sourceDateFrame.midY + 7.0), to: node.dateAndStatusNode.position, completion: nil)
|
||||
|
||||
self.dateAndStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
node.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05)
|
||||
|
||||
if let durationNode = self.durationNode, let durationBackgroundNode = self.durationBackgroundNode {
|
||||
let sourceDurationFrame = durationNode.view.convert(durationNode.view.bounds, to: node.view)
|
||||
let targetDurationFrame = node.fetchingTextNode.view.convert(node.fetchingTextNode.view.bounds, to: self.view)
|
||||
|
||||
let delta = CGPoint(x: targetDurationFrame.center.x - durationNode.position.x, y: targetDurationFrame.center.y - durationNode.position.y)
|
||||
animator.animatePosition(layer: durationNode.layer, from: durationNode.position, to: targetDurationFrame.center, completion: nil)
|
||||
animator.animatePosition(layer: durationBackgroundNode.layer, from: durationBackgroundNode.position, to: durationBackgroundNode.position.offsetBy(dx: delta.x, dy: delta.y), completion: nil)
|
||||
animator.animatePosition(layer: node.fetchingTextNode.layer, from: sourceDurationFrame.center, to: node.fetchingTextNode.position, completion: nil)
|
||||
|
||||
durationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
self.durationBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
|
||||
node.fetchingTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05)
|
||||
}
|
||||
}
|
||||
|
||||
func animateFrom(_ node: ChatMessageInteractiveFileNode, animator: ControlledTransitionAnimator) {
|
||||
let duration: Double = 0.2
|
||||
|
||||
self.alpha = 1.0
|
||||
self.isHidden = false
|
||||
|
||||
node.alpha = 0.0
|
||||
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { _ in
|
||||
node.isHidden = true
|
||||
})
|
||||
node.waveformView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
|
||||
if let videoNode = self.videoNode, let sourceNode = node.statusNode {
|
||||
videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
|
||||
let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
|
||||
animator.animatePosition(layer: videoNode.layer, from: sourceFrame.center, to: videoNode.position, completion: nil)
|
||||
let sourceScale = sourceNode.frame.width / videoNode.bounds.width
|
||||
animator.animateScale(layer: videoNode.layer, from: sourceScale, to: self.imageScale, completion: nil)
|
||||
|
||||
animator.animatePosition(layer: self.infoBackgroundNode.layer, from: sourceFrame.center.offsetBy(dx: 0.0, dy: 19.0), to: self.infoBackgroundNode.position, completion: nil)
|
||||
animator.animateScale(layer: self.infoBackgroundNode.layer, from: sourceScale / self.imageScale, to: 1.0, completion: nil)
|
||||
self.infoBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
|
||||
if let playbackStatusNode = self.playbackStatusNode {
|
||||
animator.animatePosition(layer: playbackStatusNode.layer, from: sourceFrame.center, to: playbackStatusNode.position, completion: nil)
|
||||
animator.animateScale(layer: playbackStatusNode.layer, from: sourceScale / self.imageScale, to: 1.0, completion: nil)
|
||||
playbackStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
}
|
||||
|
||||
let targetFrame = self.view.convert(videoNode.frame, to: node.view)
|
||||
animator.animatePosition(layer: sourceNode.layer, from: sourceNode.position, to: targetFrame.center, completion: nil)
|
||||
let targetScale = (videoNode.bounds.width * self.imageScale) / sourceNode.frame.width
|
||||
animator.animateScale(layer: sourceNode.layer, from: 1.0, to: targetScale, completion: nil)
|
||||
sourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
|
||||
let verticalDelta = (videoNode.position.y - sourceFrame.center.y) * 2.0
|
||||
animator.animatePosition(layer: node.textNode.layer, from: node.textNode.position, to: node.textNode.position.offsetBy(dx: 0.0, dy: verticalDelta), completion: nil)
|
||||
node.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
}
|
||||
|
||||
if let audioTranscriptionButton = self.audioTranscriptionButton, let sourceAudioTranscriptionButton = node.audioTranscriptionButton {
|
||||
audioTranscriptionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
|
||||
let targetFrame = audioTranscriptionButton.convert(audioTranscriptionButton.bounds, to: node.view)
|
||||
animator.animatePosition(layer: sourceAudioTranscriptionButton.layer, from: sourceAudioTranscriptionButton.center, to: targetFrame.center, completion: nil)
|
||||
sourceAudioTranscriptionButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
|
||||
let sourceFrame = sourceAudioTranscriptionButton.convert(sourceAudioTranscriptionButton.bounds, to: self.view)
|
||||
animator.animatePosition(layer: audioTranscriptionButton.layer, from: sourceFrame.center, to: audioTranscriptionButton.center, completion: nil)
|
||||
}
|
||||
|
||||
let sourceDateFrame = node.dateAndStatusNode.view.convert(node.dateAndStatusNode.view.bounds, to: self.view)
|
||||
let targetDateFrame = self.dateAndStatusNode.view.convert(self.dateAndStatusNode.view.bounds, to: node.view)
|
||||
|
||||
animator.animatePosition(layer: self.dateAndStatusNode.layer, from: CGPoint(x: sourceDateFrame.maxX - self.dateAndStatusNode.frame.width / 2.0 + 2.0, y: sourceDateFrame.midY - 7.0), to: self.dateAndStatusNode.position, completion: nil)
|
||||
animator.animatePosition(layer: node.dateAndStatusNode.layer, from: node.dateAndStatusNode.position, to: CGPoint(x: targetDateFrame.maxX - node.dateAndStatusNode.frame.width / 2.0, y: targetDateFrame.midY + 7.0), completion: nil)
|
||||
|
||||
self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
node.dateAndStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
|
||||
if let durationNode = self.durationNode, let durationBackgroundNode = self.durationBackgroundNode {
|
||||
let sourceDurationFrame = node.fetchingTextNode.view.convert(node.fetchingTextNode.view.bounds, to: self.view)
|
||||
let targetDurationFrame = durationNode.view.convert(durationNode.view.bounds, to: node.view)
|
||||
|
||||
let delta = CGPoint(x: sourceDurationFrame.center.x - durationNode.position.x, y: sourceDurationFrame.center.y - durationNode.position.y)
|
||||
animator.animatePosition(layer: durationNode.layer, from: sourceDurationFrame.center, to: durationNode.position, completion: nil)
|
||||
animator.animatePosition(layer: durationBackgroundNode.layer, from: durationBackgroundNode.position.offsetBy(dx: delta.x, dy: delta.y), to: durationBackgroundNode.position, completion: nil)
|
||||
animator.animatePosition(layer: node.fetchingTextNode.layer, from: node.fetchingTextNode.position, to: targetDurationFrame.center, completion: nil)
|
||||
|
||||
durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
self.durationBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05)
|
||||
|
||||
node.fetchingTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
}
|
||||
|
||||
self.customIsHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,53 @@ struct ChatMessageDateAndStatus {
|
||||
var dateText: String
|
||||
}
|
||||
|
||||
public func roundedRectCgPath(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) -> CGPath {
|
||||
let path = CGMutablePath()
|
||||
|
||||
let topLeft = rect.origin
|
||||
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
|
||||
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
|
||||
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
|
||||
|
||||
if topLeftRadius != .zero {
|
||||
path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
|
||||
} else {
|
||||
path.move(to: CGPoint(x: topLeft.x, y: topLeft.y))
|
||||
}
|
||||
|
||||
if topRightRadius != .zero {
|
||||
path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y))
|
||||
path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: topRight.x, y: topRight.y))
|
||||
}
|
||||
|
||||
if bottomRightRadius != .zero {
|
||||
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius))
|
||||
path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y))
|
||||
}
|
||||
|
||||
if bottomLeftRadius != .zero {
|
||||
path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y))
|
||||
path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y))
|
||||
}
|
||||
|
||||
if topLeftRadius != .zero {
|
||||
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius))
|
||||
path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y))
|
||||
}
|
||||
|
||||
path.closeSubpath()
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
extension UIBezierPath {
|
||||
convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) {
|
||||
self.init()
|
||||
|
@ -431,7 +431,8 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
|
||||
break loop
|
||||
case let .Video(_, _, flags):
|
||||
if flags.contains(.instantRoundVideo) {
|
||||
viewClassName = ChatMessageInstantVideoItemNode.self
|
||||
// viewClassName = ChatMessageInstantVideoItemNode.self
|
||||
viewClassName = ChatMessageBubbleItemNode.self
|
||||
break loop
|
||||
}
|
||||
default:
|
||||
|
@ -543,6 +543,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode))
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
@ -237,6 +237,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
|
||||
}, updatePeerGrouping: { _, _ in
|
||||
}, togglePeerMarkedUnread: { _, _ in
|
||||
}, toggleArchivedFolderHiddenByDefault: {
|
||||
}, toggleThreadsSelection: { _, _ in
|
||||
}, hidePsa: { _ in
|
||||
}, activateChatPreview: { [weak self] item, node, gesture, _ in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -164,6 +164,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: true), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
|
@ -235,21 +235,22 @@ public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePee
|
||||
return
|
||||
}
|
||||
|
||||
let chatLocation: ChatLocation = .replyThread(message: result.message)
|
||||
|
||||
let subject: ChatControllerSubject?
|
||||
if let messageId = messageId {
|
||||
subject = .message(id: .id(messageId), highlight: true, timecode: nil)
|
||||
} else {
|
||||
subject = nil
|
||||
}
|
||||
|
||||
var actualActivateInput: ChatControllerActivateInput? = result.isEmpty ? .text : nil
|
||||
if let activateInput = activateInput {
|
||||
actualActivateInput = activateInput
|
||||
}
|
||||
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, subject: subject, activateInput: actualActivateInput, keepStack: .never))
|
||||
context.sharedContext.navigateToChatController(
|
||||
NavigateToChatControllerParams(
|
||||
navigationController: navigationController,
|
||||
context: context,
|
||||
chatLocation: .replyThread(message: result.message),
|
||||
chatLocationContextHolder: result.contextHolder,
|
||||
subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) },
|
||||
activateInput: actualActivateInput,
|
||||
keepStack: .never
|
||||
)
|
||||
)
|
||||
}
|
||||
|> ignoreValues
|
||||
|> `catch` { _ -> Signal<Never, NoError> in
|
||||
@ -257,6 +258,21 @@ public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePee
|
||||
}
|
||||
}
|
||||
|
||||
public func chatControllerForForumThreadImpl(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64) -> Signal<ChatController, NoError> {
|
||||
return fetchAndPreloadReplyThreadInfo(context: context, subject: .groupMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))), atMessageId: nil, preload: false)
|
||||
|> deliverOnMainQueue
|
||||
|> `catch` { _ -> Signal<ReplyThreadInfo, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|> map { result in
|
||||
return ChatControllerImpl(
|
||||
context: context,
|
||||
chatLocation: .replyThread(message: result.message),
|
||||
chatLocationContextHolder: result.contextHolder
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func navigateToForumChannelImpl(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) {
|
||||
let controller = ChatListControllerImpl(context: context, location: .forum(peerId: peerId), controlsHistoryPreload: false, enableDebugActions: false)
|
||||
controller.navigationPresentation = .master
|
||||
|
@ -157,6 +157,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
|
@ -648,6 +648,13 @@ final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode {
|
||||
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
|
||||
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
|
||||
clipStyle = .roundedRect
|
||||
} else {
|
||||
clipStyle = .round
|
||||
}
|
||||
|
||||
if canEditPeerInfo(context: self.context, peer: peer, threadData: threadData) {
|
||||
var overlayHidden = true
|
||||
if let updatingAvatar = updatingAvatar {
|
||||
@ -658,7 +665,8 @@ final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode {
|
||||
if case let .image(representation) = updatingAvatar {
|
||||
if representation != self.currentRepresentation {
|
||||
self.currentRepresentation = representation
|
||||
if let signal = peerAvatarImage(account: context.account, peerReference: nil, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: avatarSize, height: avatarSize), emptyColor: nil, synchronousLoad: false, provideUnrounded: false) {
|
||||
|
||||
if let signal = peerAvatarImage(account: context.account, peerReference: nil, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: avatarSize, height: avatarSize), clipStyle: clipStyle, emptyColor: nil, synchronousLoad: false, provideUnrounded: false) {
|
||||
self.imageNode.setSignal(signal |> map { $0?.0 })
|
||||
}
|
||||
}
|
||||
@ -680,7 +688,14 @@ final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
if !overlayHidden && self.updatingAvatarOverlay.image == nil {
|
||||
self.updatingAvatarOverlay.image = generateFilledCircleImage(diameter: avatarSize, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil)
|
||||
switch clipStyle {
|
||||
case .round:
|
||||
self.updatingAvatarOverlay.image = generateFilledCircleImage(diameter: avatarSize, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil)
|
||||
case .roundedRect:
|
||||
self.updatingAvatarOverlay.image = generateFilledRoundedRectImage(size: CGSize(width: avatarSize, height: avatarSize), cornerRadius: avatarSize * 0.25, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.statusNode.transitionToState(.none)
|
||||
|
@ -2541,6 +2541,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
|
||||
@ -7297,7 +7298,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
}
|
||||
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer, _ in
|
||||
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer, threadId in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongSelf = self, let _ = peerSelectionController {
|
||||
@ -7334,27 +7335,40 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
peerSelectionController.dismiss()
|
||||
}
|
||||
} else {
|
||||
let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: nil, { currentState in
|
||||
let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in
|
||||
return currentState.withUpdatedForwardMessageIds(Array(messageIds))
|
||||
})
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil)
|
||||
|
||||
if let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||
let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId))
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers.insert(chatController, at: viewControllers.count - 1)
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
let proceed: (ChatController) -> Void = { chatController in
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil)
|
||||
|
||||
strongSelf.activeActionDisposable.set((chatController.ready.get()
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
if let peerSelectionController = peerSelectionController {
|
||||
peerSelectionController.dismiss()
|
||||
if let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
if threadId != nil {
|
||||
viewControllers.insert(chatController, at: viewControllers.count - 2)
|
||||
} else {
|
||||
viewControllers.insert(chatController, at: viewControllers.count - 1)
|
||||
}
|
||||
}))
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
|
||||
strongSelf.activeActionDisposable.set((chatController.ready.get()
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak navigationController] _ in
|
||||
viewControllers.removeAll(where: { $0 is PeerSelectionController })
|
||||
navigationController?.setViewControllers(viewControllers, animated: true)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if let threadId = threadId {
|
||||
let _ = (strongSelf.context.sharedContext.chatControllerForForumThread(context: strongSelf.context, peerId: peerId, threadId: threadId)
|
||||
|> deliverOnMainQueue).start(next: { chatController in
|
||||
proceed(chatController)
|
||||
})
|
||||
} else {
|
||||
proceed(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -94,14 +94,16 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
self.navigationPresentation = .modal
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
self.customTitle = params.title
|
||||
self.title = self.customTitle ?? self.presentationData.strings.Conversation_ForwardTitle
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
if params.forumPeerId == nil {
|
||||
self.navigationPresentation = .modal
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
}
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
@ -179,7 +181,29 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
|
||||
self.peerSelectionNode.requestOpenPeer = { [weak self] peer, threadId in
|
||||
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
|
||||
peerSelected(peer, threadId)
|
||||
if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum), threadId == nil {
|
||||
let controller = PeerSelectionControllerImpl(
|
||||
PeerSelectionControllerParams(
|
||||
context: strongSelf.context,
|
||||
updatedPresentationData: nil,
|
||||
filter: strongSelf.filter,
|
||||
forumPeerId: peer.id,
|
||||
hasChatListSelector: false,
|
||||
hasContactSelector: false,
|
||||
hasGlobalSearch: false,
|
||||
title: EnginePeer(peer).compactDisplayTitle,
|
||||
attemptSelection: strongSelf.attemptSelection,
|
||||
createNewGroup: nil,
|
||||
pretendPresentedInModal: false,
|
||||
multipleSelection: false,
|
||||
forwardedMessageIds: [],
|
||||
hasTypeHeaders: false)
|
||||
)
|
||||
controller.peerSelected = strongSelf.peerSelected
|
||||
strongSelf.push(controller)
|
||||
} else {
|
||||
peerSelected(peer, threadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1163,6 +1163,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return navigateToForumThreadImpl(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: activateInput)
|
||||
}
|
||||
|
||||
public func chatControllerForForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64) -> Signal<ChatController, NoError> {
|
||||
return chatControllerForForumThreadImpl(context: context, peerId: peerId, threadId: threadId)
|
||||
}
|
||||
|
||||
public func openStorageUsage(context: AccountContext) {
|
||||
guard let navigationController = self.mainWindow?.viewController as? NavigationController else {
|
||||
return
|
||||
@ -1347,6 +1351,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))
|
||||
|
||||
|
@ -1115,7 +1115,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
var insets = layout.insets(options: [.input])
|
||||
switch self.placementPosition {
|
||||
case .top:
|
||||
insets.top = layout.statusBarHeight ?? 0.0
|
||||
break
|
||||
case .bottom:
|
||||
if self.elevatedLayout {
|
||||
insets.bottom += 49.0
|
||||
@ -1126,8 +1126,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
var panelWrapperFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight))
|
||||
|
||||
if case .top = self.placementPosition {
|
||||
panelFrame.origin.y = insets.top
|
||||
panelWrapperFrame.origin.y = insets.top
|
||||
panelFrame.origin.y = insets.top + margin
|
||||
panelWrapperFrame.origin.y = insets.top + margin
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.panelNode, frame: panelFrame)
|
||||
@ -1232,7 +1232,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
|
||||
func animateIn(asReplacement: Bool) {
|
||||
if asReplacement {
|
||||
let offset = self.bounds.height - self.panelWrapperNode.frame.minY
|
||||
let offset: CGFloat
|
||||
switch self.placementPosition {
|
||||
case .top:
|
||||
offset = -self.panelWrapperNode.frame.maxY
|
||||
case.bottom:
|
||||
offset = self.bounds.height - self.panelWrapperNode.frame.minY
|
||||
}
|
||||
|
||||
self.panelWrapperNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: nil)
|
||||
self.panelNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: nil)
|
||||
} else {
|
||||
|
@ -1115,7 +1115,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
self.gradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.isAnimating = false
|
||||
if strongSelf.isLooping {
|
||||
if strongSelf.isLooping && strongSelf.validLayout != nil {
|
||||
strongSelf.animateEvent(transition: transition, extendAnimation: extendAnimation)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user