Various improvements

This commit is contained in:
Ilya Laktyushin 2022-10-22 00:26:00 +03:00
parent 3d709ba568
commit 3532108c30
49 changed files with 1881 additions and 342 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -84,6 +84,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, updatePeerGrouping: { _, _ in
}, togglePeerMarkedUnread: { _, _ in
}, toggleArchivedFolderHiddenByDefault: {
}, toggleThreadsSelection: { _, _ in
}, hidePsa: { _ in
}, activateChatPreview: { _, _, gesture, _ in
gesture?.cancel()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ public enum ChatHistoryNodeLoadState: Equatable {
case topic
}
case loading
case loading(Bool)
case empty(EmptyType)
case messages
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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