mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
no message
This commit is contained in:
@@ -25,8 +25,17 @@
|
||||
D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */; };
|
||||
D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */; };
|
||||
D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */; };
|
||||
D073CE631DCBBE5D007511FD /* MessageSent.caf in Resources */ = {isa = PBXBuildFile; fileRef = D073CE621DCBBE5D007511FD /* MessageSent.caf */; };
|
||||
D073CE651DCBC26B007511FD /* ServiceSoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */; };
|
||||
D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */; };
|
||||
D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */; };
|
||||
D07CFF741DCA207200761F81 /* PeerSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF731DCA207200761F81 /* PeerSelectionController.swift */; };
|
||||
D07CFF761DCA224100761F81 /* PeerSelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF751DCA224100761F81 /* PeerSelectionControllerNode.swift */; };
|
||||
D07CFF791DCA226F00761F81 /* ChatListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF781DCA226F00761F81 /* ChatListNode.swift */; };
|
||||
D07CFF7B1DCA24BF00761F81 /* ChatListNodeEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF7A1DCA24BF00761F81 /* ChatListNodeEntries.swift */; };
|
||||
D07CFF7D1DCA273400761F81 /* ChatListViewTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */; };
|
||||
D07CFF7F1DCA308500761F81 /* ChatListNodeLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */; };
|
||||
D07CFF871DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */; };
|
||||
D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */; };
|
||||
D08C36811DB66AAC0064C744 /* ChatMediaInputGridEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */; };
|
||||
D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */; };
|
||||
@@ -245,8 +254,17 @@
|
||||
D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = "<group>"; };
|
||||
D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = "<group>"; };
|
||||
D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = "<group>"; };
|
||||
D073CE621DCBBE5D007511FD /* MessageSent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MessageSent.caf; path = TelegramUI/Sounds/MessageSent.caf; sourceTree = "<group>"; };
|
||||
D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = "<group>"; };
|
||||
D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageSnippetItemNode.swift; sourceTree = "<group>"; };
|
||||
D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageNode.swift; sourceTree = "<group>"; };
|
||||
D07CFF731DCA207200761F81 /* PeerSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSelectionController.swift; sourceTree = "<group>"; };
|
||||
D07CFF751DCA224100761F81 /* PeerSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSelectionControllerNode.swift; sourceTree = "<group>"; };
|
||||
D07CFF781DCA226F00761F81 /* ChatListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNode.swift; sourceTree = "<group>"; };
|
||||
D07CFF7A1DCA24BF00761F81 /* ChatListNodeEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNodeEntries.swift; sourceTree = "<group>"; };
|
||||
D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListViewTransition.swift; sourceTree = "<group>"; };
|
||||
D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNodeLocation.swift; sourceTree = "<group>"; };
|
||||
D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardAccessoryPanelNode.swift; sourceTree = "<group>"; };
|
||||
D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPanelEntries.swift; sourceTree = "<group>"; };
|
||||
D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputGridEntries.swift; sourceTree = "<group>"; };
|
||||
D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerGridItem.swift; sourceTree = "<group>"; };
|
||||
@@ -541,10 +559,34 @@
|
||||
children = (
|
||||
D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */,
|
||||
D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */,
|
||||
D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */,
|
||||
);
|
||||
name = "Accessory Panels";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D073CE611DCBBE09007511FD /* Sounds */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D073CE621DCBBE5D007511FD /* MessageSent.caf */,
|
||||
);
|
||||
name = Sounds;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D07CFF771DCA226200761F81 /* Chat List Node */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D07CFF781DCA226F00761F81 /* ChatListNode.swift */,
|
||||
D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */,
|
||||
D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */,
|
||||
D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */,
|
||||
D0F69DFD1D6B8A880046BCD6 /* ChatListSearchItem.swift */,
|
||||
D07CFF7A1DCA24BF00761F81 /* ChatListNodeEntries.swift */,
|
||||
D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */,
|
||||
D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */,
|
||||
);
|
||||
name = "Chat List Node";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D08D45281D5E340200A7428A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -615,12 +657,14 @@
|
||||
name = "Navigation Accessory Panels";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0D2689B1D79D31500C422DA /* Share Recipients */ = {
|
||||
D0D2689B1D79D31500C422DA /* Peer Selection */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D07CFF731DCA207200761F81 /* PeerSelectionController.swift */,
|
||||
D07CFF751DCA224100761F81 /* PeerSelectionControllerNode.swift */,
|
||||
D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */,
|
||||
);
|
||||
name = "Share Recipients";
|
||||
name = "Peer Selection";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0DE772C1D934DCB002B8809 /* List Items */ = {
|
||||
@@ -761,6 +805,7 @@
|
||||
D0F69DBE1D6B89880046BCD6 /* Gestures */,
|
||||
D0F69DBF1D6B89AE0046BCD6 /* Nodes */,
|
||||
D0F69DD31D6B8A160046BCD6 /* Controllers */,
|
||||
D07CFF771DCA226200761F81 /* Chat List Node */,
|
||||
D0E7A1BB1D8C17EB00C37A6F /* Chat History Node */,
|
||||
);
|
||||
name = Components;
|
||||
@@ -840,7 +885,7 @@
|
||||
D0F69E4E1D6B8BB90046BCD6 /* Media */,
|
||||
D0F69E6C1D6B8C220046BCD6 /* Contacts */,
|
||||
D0EE97131D88BB1A006C18E1 /* Peer Info */,
|
||||
D0D2689B1D79D31500C422DA /* Share Recipients */,
|
||||
D0D2689B1D79D31500C422DA /* Peer Selection */,
|
||||
D0F69E791D6B8C3B0046BCD6 /* Settings */,
|
||||
);
|
||||
name = Controllers;
|
||||
@@ -865,10 +910,6 @@
|
||||
children = (
|
||||
D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */,
|
||||
D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */,
|
||||
D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */,
|
||||
D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */,
|
||||
D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */,
|
||||
D0F69DFD1D6B8A880046BCD6 /* ChatListSearchItem.swift */,
|
||||
D0F69E051D6B8A8B0046BCD6 /* Search */,
|
||||
);
|
||||
name = "Chat List";
|
||||
@@ -1064,6 +1105,7 @@
|
||||
D0F69E941D6B8C9B0046BCD6 /* WebP.swift */,
|
||||
D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */,
|
||||
D0ED5D4A1DC806D7007CBB15 /* ApplicationSpecificData.swift */,
|
||||
D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */,
|
||||
);
|
||||
name = Utils;
|
||||
sourceTree = "<group>";
|
||||
@@ -1085,6 +1127,7 @@
|
||||
children = (
|
||||
D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */,
|
||||
D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */,
|
||||
D073CE611DCBBE09007511FD /* Sounds */,
|
||||
D0FC40811D5B8E7400261D9D /* TelegramUI */,
|
||||
D0FC408C1D5B8E7500261D9D /* TelegramUITests */,
|
||||
D0FC40801D5B8E7400261D9D /* Products */,
|
||||
@@ -1227,6 +1270,7 @@
|
||||
files = (
|
||||
D0AB0BBB1D6719B5002C78E7 /* Images.xcassets in Resources */,
|
||||
D0F69DBA1D6B88190046BCD6 /* TelegramUI.xcconfig in Resources */,
|
||||
D073CE631DCBBE5D007511FD /* MessageSent.caf in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1253,6 +1297,7 @@
|
||||
D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */,
|
||||
D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */,
|
||||
D021E0E51DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift in Sources */,
|
||||
D07CFF7F1DCA308500761F81 /* ChatListNodeLocation.swift in Sources */,
|
||||
D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */,
|
||||
D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */,
|
||||
D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */,
|
||||
@@ -1269,6 +1314,7 @@
|
||||
D0B843DB1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift in Sources */,
|
||||
D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */,
|
||||
D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */,
|
||||
D073CE651DCBC26B007511FD /* ServiceSoundManager.swift in Sources */,
|
||||
D0F69D521D6B87D30046BCD6 /* MediaPlayer.swift in Sources */,
|
||||
D0F69E031D6B8A880046BCD6 /* ChatListItem.swift in Sources */,
|
||||
D0F69E081D6B8A9C0046BCD6 /* ChatListSearchContainerNode.swift in Sources */,
|
||||
@@ -1292,6 +1338,7 @@
|
||||
D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */,
|
||||
D0F69E421D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift in Sources */,
|
||||
D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */,
|
||||
D07CFF871DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift in Sources */,
|
||||
D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */,
|
||||
D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */,
|
||||
D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */,
|
||||
@@ -1303,6 +1350,7 @@
|
||||
D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */,
|
||||
D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */,
|
||||
D0B843921DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift in Sources */,
|
||||
D07CFF791DCA226F00761F81 /* ChatListNode.swift in Sources */,
|
||||
D0B7F8E81D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift in Sources */,
|
||||
D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */,
|
||||
D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */,
|
||||
@@ -1323,6 +1371,7 @@
|
||||
D0DE77301D934DEF002B8809 /* ListMessageItem.swift in Sources */,
|
||||
D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */,
|
||||
D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */,
|
||||
D07CFF7B1DCA24BF00761F81 /* ChatListNodeEntries.swift in Sources */,
|
||||
D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */,
|
||||
D021E0D01DB413BC00C6B04F /* ChatInputNode.swift in Sources */,
|
||||
D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */,
|
||||
@@ -1373,6 +1422,7 @@
|
||||
D0B843D91DAAAA0C005F29E1 /* PeerInfoPeerItem.swift in Sources */,
|
||||
D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */,
|
||||
D0DE77271D932627002B8809 /* ChatHistoryNode.swift in Sources */,
|
||||
D07CFF7D1DCA273400761F81 /* ChatListViewTransition.swift in Sources */,
|
||||
D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */,
|
||||
D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */,
|
||||
D0DE772B1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift in Sources */,
|
||||
@@ -1384,6 +1434,7 @@
|
||||
D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */,
|
||||
D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */,
|
||||
D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */,
|
||||
D07CFF761DCA224100761F81 /* PeerSelectionControllerNode.swift in Sources */,
|
||||
D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */,
|
||||
D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */,
|
||||
D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */,
|
||||
@@ -1418,6 +1469,7 @@
|
||||
D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */,
|
||||
D0F69DDF1D6B8A420046BCD6 /* ListController.swift in Sources */,
|
||||
D0F69E3A1D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift in Sources */,
|
||||
D07CFF741DCA207200761F81 /* PeerSelectionController.swift in Sources */,
|
||||
D0EE971A1D88BCA0006C18E1 /* ChatInfo.swift in Sources */,
|
||||
D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */,
|
||||
D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */,
|
||||
|
||||
@@ -35,6 +35,9 @@ public class ChatController: ViewController {
|
||||
private var controllerInteraction: ChatControllerInteraction?
|
||||
private var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||
|
||||
private let controllerNavigationDisposable = MetaDisposable()
|
||||
private let sentMessageEventsDisposable = MetaDisposable()
|
||||
|
||||
public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) {
|
||||
self.account = account
|
||||
self.peerId = peerId
|
||||
@@ -164,7 +167,7 @@ public class ChatController: ViewController {
|
||||
}, sendSticker: { [weak self] file in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({})
|
||||
enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: "", replyMessageId: nil, media: file).start()
|
||||
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", media: file, replyToMessageId: nil)]).start()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -207,6 +210,8 @@ public class ChatController: ViewController {
|
||||
self.navigationActionDisposable.dispose()
|
||||
self.galleryHiddenMesageAndMediaDisposable.dispose()
|
||||
self.peerDisposable.dispose()
|
||||
self.controllerNavigationDisposable.dispose()
|
||||
self.sentMessageEventsDisposable.dispose()
|
||||
}
|
||||
|
||||
var chatDisplayNode: ChatControllerNode {
|
||||
@@ -218,7 +223,19 @@ public class ChatController: ViewController {
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!)
|
||||
|
||||
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyReady.get(), self._peerReady.get()) |> map { $0 && $1 })
|
||||
let initialData = self.chatDisplayNode.historyNode.initialData
|
||||
|> take(1)
|
||||
|> beforeNext { [weak self] initialData in
|
||||
if let strongSelf = self, let initialData = initialData {
|
||||
if let interfaceState = initialData.chatInterfaceState as? ChatInterfaceState {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState({ _ in return interfaceState }) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyReady.get(), self._peerReady.get(), initialData) |> map { historyReady, peerReady, _ in
|
||||
return historyReady && peerReady
|
||||
})
|
||||
|
||||
self.chatDisplayNode.historyNode.visibleContentOffsetChanged = { [weak self] offset in
|
||||
if let strongSelf = self {
|
||||
@@ -282,7 +299,7 @@ public class ChatController: ViewController {
|
||||
stationaryItemRange = (maxInsertedItem + 1, Int.max)
|
||||
}
|
||||
|
||||
mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange), updateSizeAndInsets)
|
||||
mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData), updateSizeAndInsets)
|
||||
})
|
||||
|
||||
if let mappedTransition = mappedTransition {
|
||||
@@ -309,7 +326,7 @@ public class ChatController: ViewController {
|
||||
let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier)
|
||||
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)])
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({})
|
||||
enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: "", replyMessageId: nil, media: media).start()
|
||||
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", media: media, replyToMessageId: nil)]).start()
|
||||
}
|
||||
}
|
||||
controller.location = { [weak strongSelf] in
|
||||
@@ -356,9 +373,49 @@ public class ChatController: ViewController {
|
||||
}
|
||||
}, forwardSelectedMessages: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let controller = ShareRecipientsActionSheetController()
|
||||
//let controller = ShareRecipientsActionSheetController()
|
||||
//strongSelf.present(controller, in: .window)
|
||||
|
||||
if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds {
|
||||
let forwardMessageIds = Array(forwardMessageIdsSet).sorted()
|
||||
|
||||
let controller = PeerSelectionController(account: strongSelf.account)
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
if let strongSelf = self, let strongController = controller {
|
||||
if peerId == strongSelf.peerId {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds).withoutSelectionState() }) })
|
||||
strongController.dismiss()
|
||||
} else {
|
||||
(strongSelf.account.postbox.modify({ modifier -> Void in
|
||||
modifier.updatePeerChatInterfaceState(peerId, update: { currentState in
|
||||
if let currentState = currentState as? ChatInterfaceState {
|
||||
return currentState.withUpdatedForwardMessageIds(forwardMessageIds)
|
||||
} else {
|
||||
return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds)
|
||||
}
|
||||
return currentState
|
||||
})
|
||||
}) |> deliverOnMainQueue).start(completed: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||||
|
||||
let ready = ValuePromise<Bool>()
|
||||
|
||||
strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
}
|
||||
}))
|
||||
|
||||
(strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, peerId: peerId), animated: false, ready: ready)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
strongSelf.present(controller, in: .window)
|
||||
}
|
||||
}
|
||||
}, updateTextInputState: { [weak self] textInputState in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedInputState(textInputState) } })
|
||||
@@ -373,6 +430,10 @@ public class ChatController: ViewController {
|
||||
self.chatDisplayNode.interfaceInteraction = interfaceInteraction
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.sentMessageEventsDisposable.set(self.account.pendingMessageManager.deliveredMessageEvents(peerId: self.peerId).start(next: { _ in
|
||||
serviceSoundManager.playMessageDeliveredSound()
|
||||
}))
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
@@ -388,6 +449,18 @@ public class ChatController: ViewController {
|
||||
self.chatDisplayNode.loadInputPanels()
|
||||
}
|
||||
|
||||
override public func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
let peerId = self.peerId
|
||||
let interfaceState = self.presentationInterfaceState.interfaceState
|
||||
self.account.postbox.modify({ modifier -> Void in
|
||||
modifier.updatePeerChatInterfaceState(peerId, update: { _ in
|
||||
return interfaceState
|
||||
})
|
||||
}).start()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
||||
@@ -104,16 +104,28 @@ class ChatControllerNode: ASDisplayNode {
|
||||
}
|
||||
let text = textInputPanelNode.text
|
||||
|
||||
if !text.isEmpty || strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {
|
||||
strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in
|
||||
if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode {
|
||||
strongSelf.ignoreUpdateHeight = true
|
||||
textInputPanelNode.text = ""
|
||||
strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil) })
|
||||
strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil) })
|
||||
strongSelf.ignoreUpdateHeight = false
|
||||
}
|
||||
})
|
||||
|
||||
let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text, replyMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId).start()
|
||||
var messages: [EnqueueMessage] = []
|
||||
if !text.isEmpty {
|
||||
messages.append(.message(text: text, media: nil, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId))
|
||||
}
|
||||
if let forwardMessageIds = strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
|
||||
for id in forwardMessageIds {
|
||||
messages.append(.forward(source: id))
|
||||
}
|
||||
}
|
||||
|
||||
let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +252,11 @@ class ChatControllerNode: ASDisplayNode {
|
||||
|
||||
accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in
|
||||
if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode {
|
||||
if let _ = accessoryPanelNode as? ReplyAccessoryPanelNode {
|
||||
strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedReplyMessageId(nil) })
|
||||
} else if let _ = accessoryPanelNode as? ForwardAccessoryPanelNode {
|
||||
strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedForwardMessageIds(nil) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,8 +455,11 @@ class ChatControllerNode: ASDisplayNode {
|
||||
var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.inputState != chatPresentationInterfaceState.interfaceState.inputState
|
||||
self.chatPresentationInterfaceState = chatPresentationInterfaceState
|
||||
|
||||
let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil
|
||||
if let textInputPanelNode = self.textInputPanelNode, updateInputTextState {
|
||||
textInputPanelNode.inputTextState = chatPresentationInterfaceState.interfaceState.inputState
|
||||
textInputPanelNode.updateInputTextState(chatPresentationInterfaceState.interfaceState.inputState, keepSendButtonEnabled: keepSendButtonEnabled, animated: animated)
|
||||
} else {
|
||||
textInputPanelNode?.updateKeepSendButtonEnabled(keepSendButtonEnabled: keepSendButtonEnabled, animated: animated)
|
||||
}
|
||||
|
||||
let layoutTransition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
||||
|
||||
@@ -149,7 +149,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
|
||||
}
|
||||
}
|
||||
return .complete()
|
||||
case let .HistoryView(view, type, scrollPosition):
|
||||
case let .HistoryView(view, type, scrollPosition, _):
|
||||
let reason: ChatHistoryViewTransitionReason
|
||||
var prepareOnMainQueue = false
|
||||
switch type {
|
||||
@@ -172,7 +172,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
|
||||
let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false))
|
||||
let previous = previousView.swap(processedView)
|
||||
|
||||
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|
||||
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ enum ChatHistoryViewUpdateType {
|
||||
}
|
||||
|
||||
enum ChatHistoryViewUpdate {
|
||||
case Loading
|
||||
case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?)
|
||||
case Loading(initialData: InitialMessageHistoryData?)
|
||||
case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?)
|
||||
}
|
||||
|
||||
struct ChatHistoryView {
|
||||
@@ -59,6 +59,7 @@ struct ChatHistoryViewTransition {
|
||||
let options: ListViewDeleteAndInsertOptions
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let stationaryItemRange: (Int, Int)?
|
||||
let initialData: InitialMessageHistoryData?
|
||||
}
|
||||
|
||||
struct ChatHistoryListViewTransition {
|
||||
@@ -69,6 +70,7 @@ struct ChatHistoryListViewTransition {
|
||||
let options: ListViewDeleteAndInsertOptions
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let stationaryItemRange: (Int, Int)?
|
||||
let initialData: InitialMessageHistoryData?
|
||||
}
|
||||
|
||||
private func maxIncomingMessageIdForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageId? {
|
||||
@@ -121,7 +123,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt
|
||||
}
|
||||
|
||||
private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition {
|
||||
return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange)
|
||||
return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData)
|
||||
}
|
||||
|
||||
private final class ChatHistoryTransactionOpaqueState {
|
||||
@@ -153,6 +155,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
public let historyReady = Promise<Bool>()
|
||||
private var didSetHistoryReady = false
|
||||
|
||||
private let _initialData = Promise<InitialMessageHistoryData?>()
|
||||
private var didSetInitialData = false
|
||||
public var initialData: Signal<InitialMessageHistoryData?, NoError> {
|
||||
return self._initialData.get()
|
||||
}
|
||||
|
||||
private let maxVisibleIncomingMessageId = ValuePromise<MessageId>()
|
||||
let canReadHistory = ValuePromise<Bool>()
|
||||
|
||||
@@ -191,7 +199,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|> mapToSignal { location in
|
||||
return chatHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: fixedCombinedReadState.with { $0 }, tagMask: tagMask) |> beforeNext { viewUpdate in
|
||||
switch viewUpdate {
|
||||
case let .HistoryView(view, _, _):
|
||||
case let .HistoryView(view, _, _, _):
|
||||
let _ = fixedCombinedReadState.swap(view.combinedReadState)
|
||||
default:
|
||||
break
|
||||
@@ -202,10 +210,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
let previousView = Atomic<ChatHistoryView?>(value: nil)
|
||||
|
||||
let historyViewTransition = historyViewUpdate |> mapToQueue { [weak self] update -> Signal<ChatHistoryListViewTransition, NoError> in
|
||||
let initialData: InitialMessageHistoryData?
|
||||
switch update {
|
||||
case .Loading:
|
||||
case let .Loading(data):
|
||||
initialData = data
|
||||
Queue.mainQueue().async { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if !strongSelf.didSetInitialData {
|
||||
strongSelf.didSetInitialData = true
|
||||
strongSelf._initialData.set(.single(data))
|
||||
}
|
||||
if !strongSelf.didSetHistoryReady {
|
||||
strongSelf.didSetHistoryReady = true
|
||||
strongSelf.historyReady.set(.single(true))
|
||||
@@ -213,7 +227,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
}
|
||||
return .complete()
|
||||
case let .HistoryView(view, type, scrollPosition):
|
||||
case let .HistoryView(view, type, scrollPosition, data):
|
||||
initialData = data
|
||||
let reason: ChatHistoryViewTransitionReason
|
||||
var prepareOnMainQueue = false
|
||||
switch type {
|
||||
@@ -236,7 +251,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles))
|
||||
let previous = previousView.swap(processedView)
|
||||
|
||||
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|
||||
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +359,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.dequeueHistoryViewTransition()
|
||||
} else {
|
||||
if !strongSelf.didSetInitialData {
|
||||
strongSelf.didSetInitialData = true
|
||||
strongSelf._initialData.set(.single(transition.initialData))
|
||||
}
|
||||
if !strongSelf.didSetHistoryReady {
|
||||
strongSelf.didSetHistoryReady = true
|
||||
strongSelf.historyReady.set(.single(true))
|
||||
@@ -374,7 +393,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !strongSelf.didSetInitialData {
|
||||
strongSelf.didSetInitialData = true
|
||||
strongSelf._initialData.set(.single(transition.initialData))
|
||||
}
|
||||
if !strongSelf.didSetHistoryReady {
|
||||
strongSelf.didSetHistoryReady = true
|
||||
strongSelf.historyReady.set(.single(true))
|
||||
|
||||
@@ -9,15 +9,15 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
case let .Initial(count):
|
||||
var preloaded = false
|
||||
var fadeIn = false
|
||||
let signal: Signal<(MessageHistoryView, ViewUpdateType), NoError>
|
||||
let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>
|
||||
if let tagMask = tagMask {
|
||||
signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: count, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask)
|
||||
} else {
|
||||
signal = account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask)
|
||||
}
|
||||
return signal |> map { view, updateType -> ChatHistoryViewUpdate in
|
||||
return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
|
||||
if preloaded {
|
||||
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil)
|
||||
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: initialData)
|
||||
} else {
|
||||
if let maxReadIndex = view.maxReadIndex {
|
||||
var targetIndex = 0
|
||||
@@ -33,25 +33,25 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
for i in targetIndex ..< maxIndex {
|
||||
if case .HoleEntry = view.entries[i] {
|
||||
fadeIn = true
|
||||
return .Loading
|
||||
return .Loading(initialData: initialData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preloaded = true
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Unread(index: maxReadIndex))
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Unread(index: maxReadIndex), initialData: initialData)
|
||||
} else {
|
||||
preloaded = true
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: nil)
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: nil, initialData: initialData)
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .InitialSearch(messageId, count):
|
||||
var preloaded = false
|
||||
var fadeIn = false
|
||||
return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
|
||||
return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
|
||||
if preloaded {
|
||||
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil)
|
||||
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: initialData)
|
||||
} else {
|
||||
let anchorIndex = view.anchorIndex
|
||||
|
||||
@@ -68,19 +68,19 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
for i in targetIndex ..< maxIndex {
|
||||
if case .HoleEntry = view.entries[i] {
|
||||
fadeIn = true
|
||||
return .Loading
|
||||
return .Loading(initialData: initialData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preloaded = true
|
||||
//case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false))
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: initialData)
|
||||
}
|
||||
}
|
||||
case let .Navigation(index, anchorIndex):
|
||||
var first = true
|
||||
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
|
||||
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
|
||||
let genericType: ViewUpdateType
|
||||
if first {
|
||||
first = false
|
||||
@@ -88,13 +88,13 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
} else {
|
||||
genericType = updateType
|
||||
}
|
||||
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil)
|
||||
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: initialData)
|
||||
}
|
||||
case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated):
|
||||
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
|
||||
let chatScrollPosition = ChatHistoryViewScrollPosition.Index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
|
||||
var first = true
|
||||
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
|
||||
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
|
||||
let genericType: ViewUpdateType
|
||||
let scrollPosition: ChatHistoryViewScrollPosition? = first ? chatScrollPosition : nil
|
||||
if first {
|
||||
@@ -103,7 +103,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
} else {
|
||||
genericType = updateType
|
||||
}
|
||||
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition)
|
||||
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: initialData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
|
||||
struct ChatInterfaceSelectionState: Equatable {
|
||||
struct ChatInterfaceSelectionState: Coding, Equatable {
|
||||
let selectedIds: Set<MessageId>
|
||||
|
||||
static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool {
|
||||
return lhs.selectedIds == rhs.selectedIds
|
||||
}
|
||||
|
||||
init(selectedIds: Set<MessageId>) {
|
||||
self.selectedIds = selectedIds
|
||||
}
|
||||
|
||||
struct ChatTextInputState: Equatable {
|
||||
init(decoder: Decoder) {
|
||||
if let data = decoder.decodeBytesForKeyNoCopy("i") {
|
||||
self.selectedIds = Set(MessageId.decodeArrayFromBuffer(data))
|
||||
} else {
|
||||
self.selectedIds = Set()
|
||||
}
|
||||
}
|
||||
|
||||
func encode(_ encoder: Encoder) {
|
||||
let buffer = WriteBuffer()
|
||||
MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer)
|
||||
encoder.encodeBytes(buffer, forKey: "i")
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatTextInputState: Coding, Equatable {
|
||||
let inputText: String
|
||||
let selectionRange: Range<Int>
|
||||
|
||||
@@ -26,35 +44,123 @@ struct ChatTextInputState: Equatable {
|
||||
self.inputText = inputText
|
||||
self.selectionRange = selectionRange
|
||||
}
|
||||
|
||||
init(decoder: Decoder) {
|
||||
self.inputText = decoder.decodeStringForKey("t")
|
||||
self.selectionRange = Int(decoder.decodeInt32ForKey("s0")) ..< Int(decoder.decodeInt32ForKey("s1"))
|
||||
}
|
||||
|
||||
final class ChatInterfaceState: Equatable {
|
||||
func encode(_ encoder: Encoder) {
|
||||
encoder.encodeString(self.inputText, forKey: "t")
|
||||
encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "s0")
|
||||
encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "s1")
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatInterfaceState: PeerChatInterfaceState, Equatable {
|
||||
let inputState: ChatTextInputState
|
||||
let replyMessageId: MessageId?
|
||||
let forwardMessageIds: [MessageId]?
|
||||
let selectionState: ChatInterfaceSelectionState?
|
||||
|
||||
var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? {
|
||||
return nil
|
||||
}
|
||||
|
||||
init() {
|
||||
self.inputState = ChatTextInputState()
|
||||
self.replyMessageId = nil
|
||||
self.forwardMessageIds = nil
|
||||
self.selectionState = nil
|
||||
}
|
||||
|
||||
init(inputState: ChatTextInputState, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) {
|
||||
init(inputState: ChatTextInputState, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, selectionState: ChatInterfaceSelectionState?) {
|
||||
self.inputState = inputState
|
||||
self.replyMessageId = replyMessageId
|
||||
self.forwardMessageIds = forwardMessageIds
|
||||
self.selectionState = selectionState
|
||||
}
|
||||
|
||||
init(decoder: Decoder) {
|
||||
if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState {
|
||||
self.inputState = inputState
|
||||
} else {
|
||||
self.inputState = ChatTextInputState()
|
||||
}
|
||||
let replyMessageIdPeerId: Int64? = decoder.decodeInt64ForKey("r.p")
|
||||
let replyMessageIdNamespace: Int32? = decoder.decodeInt32ForKey("r.n")
|
||||
let replyMessageIdId: Int32? = decoder.decodeInt32ForKey("r.i")
|
||||
if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId {
|
||||
self.replyMessageId = MessageId(peerId: PeerId(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId)
|
||||
} else {
|
||||
self.replyMessageId = nil
|
||||
}
|
||||
if let forwardMessageIdsData = decoder.decodeBytesForKeyNoCopy("fm") {
|
||||
self.forwardMessageIds = MessageId.decodeArrayFromBuffer(forwardMessageIdsData)
|
||||
} else {
|
||||
self.forwardMessageIds = nil
|
||||
}
|
||||
if let selectionState = decoder.decodeObjectForKey("ss", decoder: { return ChatInterfaceSelectionState(decoder: $0) }) as? ChatInterfaceSelectionState {
|
||||
self.selectionState = selectionState
|
||||
} else {
|
||||
self.selectionState = nil
|
||||
}
|
||||
}
|
||||
|
||||
func encode(_ encoder: Encoder) {
|
||||
encoder.encodeObject(self.inputState, forKey: "is")
|
||||
if let replyMessageId = self.replyMessageId {
|
||||
encoder.encodeInt64(replyMessageId.peerId.toInt64(), forKey: "r.p")
|
||||
encoder.encodeInt32(replyMessageId.namespace, forKey: "r.n")
|
||||
encoder.encodeInt32(replyMessageId.id, forKey: "r.i")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "r.p")
|
||||
encoder.encodeNil(forKey: "r.n")
|
||||
encoder.encodeNil(forKey: "r.i")
|
||||
}
|
||||
if let forwardMessageIds = self.forwardMessageIds {
|
||||
let buffer = WriteBuffer()
|
||||
MessageId.encodeArrayToBuffer(forwardMessageIds, buffer: buffer)
|
||||
encoder.encodeBytes(buffer, forKey: "fm")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "fm")
|
||||
}
|
||||
if let selectionState = self.selectionState {
|
||||
encoder.encodeObject(selectionState, forKey: "ss")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "ss")
|
||||
}
|
||||
}
|
||||
|
||||
func isEqual(to: PeerChatInterfaceState) -> Bool {
|
||||
if let to = to as? ChatInterfaceState, self == to {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool {
|
||||
if let lhsForwardMessageIds = lhs.forwardMessageIds, let rhsForwardMessageIds = rhs.forwardMessageIds {
|
||||
if lhsForwardMessageIds != rhsForwardMessageIds {
|
||||
return false
|
||||
}
|
||||
} else if (lhs.forwardMessageIds != nil) != (rhs.forwardMessageIds != nil) {
|
||||
return false
|
||||
}
|
||||
return lhs.inputState == rhs.inputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState
|
||||
}
|
||||
|
||||
func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(inputState: inputState, replyMessageId: self.replyMessageId, selectionState: self.selectionState)
|
||||
return ChatInterfaceState(inputState: inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: self.selectionState)
|
||||
}
|
||||
|
||||
func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: replyMessageId, selectionState: self.selectionState)
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: self.selectionState)
|
||||
}
|
||||
|
||||
func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, selectionState: self.selectionState)
|
||||
}
|
||||
|
||||
func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState {
|
||||
@@ -63,7 +169,7 @@ final class ChatInterfaceState: Equatable {
|
||||
selectedIds.formUnion(selectionState.selectedIds)
|
||||
}
|
||||
selectedIds.insert(messageId)
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds))
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds))
|
||||
}
|
||||
|
||||
func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState {
|
||||
@@ -76,10 +182,10 @@ final class ChatInterfaceState: Equatable {
|
||||
} else {
|
||||
selectedIds.insert(messageId)
|
||||
}
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds))
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds))
|
||||
}
|
||||
|
||||
func withoutSelectionState() -> ChatInterfaceState {
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: nil)
|
||||
return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,16 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
|
||||
return nil
|
||||
}
|
||||
|
||||
if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId {
|
||||
if let forwardMessageIds = chatPresentationInterfaceState.interfaceState.forwardMessageIds {
|
||||
if let forwardPanelNode = currentPanel as? ForwardAccessoryPanelNode, forwardPanelNode.messageIds == forwardMessageIds {
|
||||
forwardPanelNode.interfaceInteraction = interfaceInteraction
|
||||
return forwardPanelNode
|
||||
} else {
|
||||
let panelNode = ForwardAccessoryPanelNode(account: account, messageIds: forwardMessageIds)
|
||||
panelNode.interfaceInteraction = interfaceInteraction
|
||||
return panelNode
|
||||
}
|
||||
} else if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId {
|
||||
if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId {
|
||||
replyPanelNode.interfaceInteraction = interfaceInteraction
|
||||
return replyPanelNode
|
||||
|
||||
@@ -4,202 +4,14 @@ import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
|
||||
enum ChatListMessageViewPosition: Equatable {
|
||||
case Tail(count: Int)
|
||||
case Around(index: MessageIndex, anchorIndex: MessageIndex, scrollPosition: ListViewScrollPosition?)
|
||||
}
|
||||
|
||||
func ==(lhs: ChatListMessageViewPosition, rhs: ChatListMessageViewPosition) -> Bool {
|
||||
switch lhs {
|
||||
case let .Tail(lhsCount):
|
||||
switch rhs {
|
||||
case let .Tail(rhsCount) where lhsCount == rhsCount:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .Around(lhsId, lhsAnchorIndex, lhsScrollPosition):
|
||||
switch rhs {
|
||||
case let .Around(rhsId, rhsAnchorIndex, rhsScrollPosition) where lhsId == rhsId && lhsAnchorIndex == rhsAnchorIndex && lhsScrollPosition == rhsScrollPosition:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChatListControllerEntryId: Hashable, CustomStringConvertible {
|
||||
case Search
|
||||
case Hole(Int64)
|
||||
case PeerId(Int64)
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case .Search:
|
||||
return 0
|
||||
case let .Hole(peerId):
|
||||
return peerId.hashValue
|
||||
case let .PeerId(peerId):
|
||||
return peerId.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .Search:
|
||||
return "search"
|
||||
case let .Hole(value):
|
||||
return "hole(\(value))"
|
||||
case let .PeerId(value):
|
||||
return "peerId(\(value))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func <(lhs: ChatListControllerEntryId, rhs: ChatListControllerEntryId) -> Bool {
|
||||
return lhs.hashValue < rhs.hashValue
|
||||
}
|
||||
|
||||
private func ==(lhs: ChatListControllerEntryId, rhs: ChatListControllerEntryId) -> Bool {
|
||||
switch lhs {
|
||||
case .Search:
|
||||
switch rhs {
|
||||
case .Search:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .Hole(lhsId):
|
||||
switch rhs {
|
||||
case .Hole(lhsId):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .PeerId(lhsId):
|
||||
switch rhs {
|
||||
case let .PeerId(rhsId):
|
||||
return lhsId == rhsId
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChatListControllerEntry: Comparable, Identifiable {
|
||||
case SearchEntry
|
||||
case MessageEntry(Message, CombinedPeerReadState?, PeerNotificationSettings?)
|
||||
case HoleEntry(ChatListHole)
|
||||
case Nothing(MessageIndex)
|
||||
|
||||
var index: MessageIndex {
|
||||
switch self {
|
||||
case .SearchEntry:
|
||||
return MessageIndex.absoluteUpperBound()
|
||||
case let .MessageEntry(message, _, _):
|
||||
return MessageIndex(message)
|
||||
case let .HoleEntry(hole):
|
||||
return hole.index
|
||||
case let .Nothing(index):
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: ChatListControllerEntryId {
|
||||
switch self {
|
||||
case .SearchEntry:
|
||||
return .Search
|
||||
case let .MessageEntry(message, _, _):
|
||||
return .PeerId(message.id.peerId.toInt64())
|
||||
case let .HoleEntry(hole):
|
||||
return .Hole(Int64(hole.index.id.id))
|
||||
case let .Nothing(index):
|
||||
return .PeerId(index.id.peerId.toInt64())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func <(lhs: ChatListControllerEntry, rhs: ChatListControllerEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
private func ==(lhs: ChatListControllerEntry, rhs: ChatListControllerEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .SearchEntry:
|
||||
switch rhs {
|
||||
case .SearchEntry:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .MessageEntry(lhsMessage, lhsUnreadCount, lhsNotificationSettings):
|
||||
switch rhs {
|
||||
case let .MessageEntry(rhsMessage, rhsUnreadCount, rhsNotificationSettings):
|
||||
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags || lhsUnreadCount != rhsUnreadCount {
|
||||
return false
|
||||
}
|
||||
if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings {
|
||||
if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsNotificationSettings != nil) != (rhsNotificationSettings != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
break
|
||||
}
|
||||
case let .HoleEntry(lhsHole):
|
||||
switch rhs {
|
||||
case let .HoleEntry(rhsHole):
|
||||
return lhsHole == rhsHole
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .Nothing(lhsIndex):
|
||||
switch rhs {
|
||||
case let .Nothing(rhsIndex):
|
||||
return lhsIndex == rhsIndex
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
extension ChatListEntry: Identifiable {
|
||||
public var stableId: Int64 {
|
||||
return self.index.id.peerId.toInt64()
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatListOpaqueTransactionState {
|
||||
let chatListViewAndEntries: (ChatListView, [ChatListControllerEntry])
|
||||
|
||||
init(chatListViewAndEntries: (ChatListView, [ChatListControllerEntry])) {
|
||||
self.chatListViewAndEntries = chatListViewAndEntries
|
||||
}
|
||||
}
|
||||
|
||||
public class ChatListController: ViewController {
|
||||
let account: Account
|
||||
|
||||
private var chatListViewAndEntries: (ChatListView, [ChatListControllerEntry])?
|
||||
|
||||
var chatListPosition: ChatListMessageViewPosition?
|
||||
let chatListDisposable: MetaDisposable = MetaDisposable()
|
||||
|
||||
let messageViewQueue = Queue()
|
||||
let messageViewTransactionQueue = ListViewTransactionQueue()
|
||||
var settingView = false
|
||||
private let account: Account
|
||||
|
||||
let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable()
|
||||
|
||||
var chatListDisplayNode: ChatListControllerNode {
|
||||
get {
|
||||
private var chatListDisplayNode: ChatListControllerNode {
|
||||
return super.displayNode as! ChatListControllerNode
|
||||
}
|
||||
}
|
||||
|
||||
public init(account: Account) {
|
||||
self.account = account
|
||||
@@ -216,47 +28,39 @@ public class ChatListController: ViewController {
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let (view, _) = strongSelf.chatListViewAndEntries, view.laterIndex == nil {
|
||||
strongSelf.chatListDisplayNode.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
} else {
|
||||
strongSelf.setMessageViewPosition(.Around(index: MessageIndex.absoluteUpperBound(), anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: .Top), hint: "later", force: true)
|
||||
strongSelf.chatListDisplayNode.chatListNode.scrollToLatest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.setMessageViewPosition(.Tail(count: 50), hint: "initial", force: false)
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.chatListDisposable.dispose()
|
||||
self.openMessageFromSearchDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ChatListControllerNode(account: self.account)
|
||||
|
||||
self.chatListDisplayNode.listView.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in
|
||||
if let strongSelf = self, !strongSelf.settingView {
|
||||
if let range = range.loadedRange, let (view, _) = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListViewAndEntries {
|
||||
if range.firstIndex < 5 && view.laterIndex != nil {
|
||||
strongSelf.setMessageViewPosition(.Around(index: view.entries[view.entries.count - 1].index, anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: nil), hint: "later", force: false)
|
||||
} else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlierIndex != nil {
|
||||
strongSelf.setMessageViewPosition(.Around(index: view.entries[0].index, anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: nil), hint: "earlier", force: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.navigationBar = self.navigationBar
|
||||
|
||||
self.chatListDisplayNode.requestDeactivateSearch = { [weak self] in
|
||||
self?.deactivateSearch()
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.chatListNode.activateSearch = { [weak self] in
|
||||
self?.activateSearch()
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.chatListNode.peerSelected = { [weak self] peerId in
|
||||
if let strongSelf = self {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))
|
||||
strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in
|
||||
if let strongSelf = self {
|
||||
let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in
|
||||
@@ -283,56 +87,6 @@ public class ChatListController: ViewController {
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
private func setMessageViewPosition(_ position: ChatListMessageViewPosition, hint: String, force: Bool) {
|
||||
if self.chatListPosition == nil || self.chatListPosition! != position || force {
|
||||
let signal: Signal<(ChatListView, ViewUpdateType), NoError>
|
||||
self.chatListPosition = position
|
||||
var scrollPosition: (MessageIndex, ListViewScrollPosition, ListViewScrollToItemDirectionHint)?
|
||||
switch position {
|
||||
case let .Tail(count):
|
||||
signal = self.account.postbox.tailChatListView(count)
|
||||
case let .Around(index, _, position):
|
||||
trace("request around \(index.id.id) \(hint)")
|
||||
signal = self.account.postbox.aroundChatListView(index, count: 80)
|
||||
if let position = position {
|
||||
var directionHint: ListViewScrollToItemDirectionHint = .Up
|
||||
if let visibleItemRange = self.chatListDisplayNode.listView.displayedItemRange.loadedRange, let (_, entries) = self.chatListViewAndEntries {
|
||||
if visibleItemRange.firstIndex >= 0 && visibleItemRange.firstIndex < entries.count {
|
||||
if entries[visibleItemRange.firstIndex].index < index {
|
||||
directionHint = .Up
|
||||
} else {
|
||||
directionHint = .Down
|
||||
}
|
||||
}
|
||||
}
|
||||
scrollPosition = (index, position, directionHint)
|
||||
}
|
||||
}
|
||||
|
||||
var firstTime = true
|
||||
chatListDisposable.set((
|
||||
signal |> deliverOnMainQueue
|
||||
).start(next: {[weak self] (view, updateType) in
|
||||
if let strongSelf = self {
|
||||
let animated: Bool
|
||||
switch updateType {
|
||||
case .Generic:
|
||||
animated = !firstTime
|
||||
case .FillHole:
|
||||
animated = false
|
||||
case .InitialUnread:
|
||||
animated = false
|
||||
case .UpdateVisible:
|
||||
animated = false
|
||||
}
|
||||
|
||||
strongSelf.setPeerView(view, firstTime: strongSelf.chatListViewAndEntries == nil, scrollPosition: firstTime ? scrollPosition : nil, animated: animated)
|
||||
firstTime = false
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
}
|
||||
@@ -341,179 +95,6 @@ public class ChatListController: ViewController {
|
||||
super.viewDidDisappear(animated)
|
||||
}
|
||||
|
||||
private func chatListControllerEntries(_ view: ChatListView) -> [ChatListControllerEntry] {
|
||||
var result: [ChatListControllerEntry] = []
|
||||
for entry in view.entries {
|
||||
switch entry {
|
||||
case let .MessageEntry(message, combinedReadState, notificationSettings):
|
||||
result.append(.MessageEntry(message, combinedReadState, notificationSettings))
|
||||
case let .HoleEntry(hole):
|
||||
result.append(.HoleEntry(hole))
|
||||
case let .Nothing(index):
|
||||
result.append(.Nothing(index))
|
||||
}
|
||||
}
|
||||
if view.laterIndex == nil {
|
||||
result.append(.SearchEntry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func setPeerView(_ view: ChatListView, firstTime: Bool, scrollPosition: (MessageIndex, ListViewScrollPosition, ListViewScrollToItemDirectionHint)?, animated: Bool) {
|
||||
self.messageViewTransactionQueue.addTransaction { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
strongSelf.settingView = true
|
||||
let currentEntries = strongSelf.chatListViewAndEntries?.1 ?? []
|
||||
let viewEntries = strongSelf.chatListControllerEntries(view)
|
||||
|
||||
strongSelf.messageViewQueue.async {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: currentEntries, rightList: viewEntries)
|
||||
//let (deleteIndices, indicesAndItems) = mergeListsStable(leftList: currentEntries, rightList: viewEntries)
|
||||
//let updateIndices: [(Int, ChatListControllerEntry)] = []
|
||||
|
||||
Queue.mainQueue().async {
|
||||
var adjustedDeleteIndices: [ListViewDeleteItem] = []
|
||||
let previousCount = currentEntries.count
|
||||
if deleteIndices.count != 0 {
|
||||
for index in deleteIndices {
|
||||
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
|
||||
}
|
||||
}
|
||||
|
||||
let updatedCount = viewEntries.count
|
||||
|
||||
var maxAnimatedInsertionIndex = -1
|
||||
if animated {
|
||||
for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) {
|
||||
let adjustedIndex = updatedCount - 1 - index
|
||||
if adjustedIndex == maxAnimatedInsertionIndex + 1 {
|
||||
maxAnimatedInsertionIndex += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var adjustedIndicesAndItems: [ListViewInsertItem] = []
|
||||
for (index, entry, previousIndex) in indicesAndItems {
|
||||
let adjustedIndex = updatedCount - 1 - index
|
||||
|
||||
var adjustedPreviousIndex: Int?
|
||||
if let previousIndex = previousIndex {
|
||||
adjustedPreviousIndex = previousCount - 1 - previousIndex
|
||||
}
|
||||
|
||||
var directionHint: ListViewItemOperationDirectionHint?
|
||||
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
|
||||
directionHint = .Down
|
||||
}
|
||||
|
||||
switch entry {
|
||||
case .SearchEntry:
|
||||
adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { [weak self] in
|
||||
self?.activateSearch()
|
||||
}), directionHint: directionHint))
|
||||
case let .MessageEntry(message, combinedReadState, notificationSettings):
|
||||
adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListItem(account: strongSelf.account, message: message, combinedReadState: combinedReadState, notificationSettings: notificationSettings, action: { [weak self] message in
|
||||
if let strongSelf = self {
|
||||
strongSelf.entrySelected(entry)
|
||||
strongSelf.chatListDisplayNode.listView.clearHighlightAnimated(true)
|
||||
}
|
||||
}), directionHint: directionHint))
|
||||
case .HoleEntry:
|
||||
adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListHoleItem(), directionHint: directionHint))
|
||||
case .Nothing:
|
||||
adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListEmptyItem(), directionHint: directionHint))
|
||||
}
|
||||
}
|
||||
|
||||
var adjustedUpdateItems: [ListViewUpdateItem] = []
|
||||
for (index, entry, previousIndex) in updateIndices {
|
||||
let adjustedIndex = updatedCount - 1 - index
|
||||
let adjustedPreviousIndex = previousCount - 1 - previousIndex
|
||||
|
||||
let directionHint: ListViewItemOperationDirectionHint? = nil
|
||||
|
||||
switch entry {
|
||||
case .SearchEntry:
|
||||
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { [weak self] in
|
||||
self?.activateSearch()
|
||||
}), directionHint: directionHint))
|
||||
case let .MessageEntry(message, combinedReadState, notificationSettings):
|
||||
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListItem(account: strongSelf.account, message: message, combinedReadState: combinedReadState, notificationSettings: notificationSettings, action: { [weak self] message in
|
||||
if let strongSelf = self {
|
||||
strongSelf.entrySelected(entry)
|
||||
strongSelf.chatListDisplayNode.listView.clearHighlightAnimated(true)
|
||||
}
|
||||
}), directionHint: directionHint))
|
||||
case .HoleEntry:
|
||||
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListHoleItem(), directionHint: directionHint))
|
||||
case .Nothing:
|
||||
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListEmptyItem(), directionHint: directionHint))
|
||||
}
|
||||
}
|
||||
|
||||
if !adjustedDeleteIndices.isEmpty || !adjustedIndicesAndItems.isEmpty || !adjustedUpdateItems.isEmpty || scrollPosition != nil {
|
||||
var options: ListViewDeleteAndInsertOptions = []
|
||||
if firstTime {
|
||||
} else {
|
||||
let _ = options.insert(.AnimateAlpha)
|
||||
|
||||
if animated {
|
||||
let _ = options.insert(.AnimateInsertion)
|
||||
}
|
||||
}
|
||||
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
if let (itemIndex, itemPosition, directionHint) = scrollPosition {
|
||||
var index = viewEntries.count - 1
|
||||
for entry in viewEntries {
|
||||
if entry.index >= itemIndex {
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: itemPosition, animated: true, curve: .Default, directionHint: directionHint)
|
||||
break
|
||||
}
|
||||
index -= 1
|
||||
}
|
||||
|
||||
if scrollToItem == nil {
|
||||
var index = 0
|
||||
for entry in viewEntries.reversed() {
|
||||
if entry.index < itemIndex {
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: itemPosition, animated: true, curve: .Default, directionHint: directionHint)
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.chatListDisplayNode.listView.transaction(deleteIndices: adjustedDeleteIndices, insertIndicesAndItems: adjustedIndicesAndItems, updateIndicesAndItems: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, updateOpaqueState: ChatListOpaqueTransactionState(chatListViewAndEntries: (view, viewEntries)), completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.ready.set(single(true, NoError.self))
|
||||
strongSelf.settingView = false
|
||||
completed()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
strongSelf.ready.set(single(true, NoError.self))
|
||||
strongSelf.settingView = false
|
||||
completed()
|
||||
}
|
||||
|
||||
strongSelf.chatListViewAndEntries = (view, viewEntries)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func entrySelected(_ entry: ChatListControllerEntry) {
|
||||
if case let .MessageEntry(message, _, _) = entry {
|
||||
//(self.navigationController as? NavigationController)?.pushViewController(PeerMediaCollectionController(account: self.account, peerId: message.id.peerId))
|
||||
(self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: message.id.peerId))
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import TelegramCore
|
||||
class ChatListControllerNode: ASDisplayNode {
|
||||
private let account: Account
|
||||
|
||||
let listView: ListView
|
||||
let chatListNode: ChatListNode
|
||||
var navigationBar: NavigationBar?
|
||||
|
||||
private var searchDisplayController: SearchDisplayController?
|
||||
@@ -20,13 +20,13 @@ class ChatListControllerNode: ASDisplayNode {
|
||||
|
||||
init(account: Account) {
|
||||
self.account = account
|
||||
self.listView = ListView()
|
||||
self.chatListNode = ChatListNode(account: account, mode: .chatList)
|
||||
|
||||
super.init(viewBlock: {
|
||||
return UITracingLayerView()
|
||||
}, didLoad: nil)
|
||||
|
||||
self.addSubnode(self.listView)
|
||||
self.addSubnode(self.chatListNode)
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
@@ -35,8 +35,8 @@ class ChatListControllerNode: ASDisplayNode {
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
|
||||
|
||||
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
||||
self.listView.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
||||
self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
||||
self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
||||
|
||||
var duration: Double = 0.0
|
||||
var curve: UInt = 0
|
||||
@@ -63,7 +63,7 @@ class ChatListControllerNode: ASDisplayNode {
|
||||
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve)
|
||||
|
||||
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
|
||||
|
||||
if let searchDisplayController = self.searchDisplayController {
|
||||
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
@@ -76,7 +76,7 @@ class ChatListControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
var maybePlaceholderNode: SearchBarPlaceholderNode?
|
||||
self.listView.forEachItemNode { node in
|
||||
self.chatListNode.forEachItemNode { node in
|
||||
if let node = node as? ChatListSearchItemNode {
|
||||
maybePlaceholderNode = node.searchBarNode
|
||||
}
|
||||
@@ -111,7 +111,7 @@ class ChatListControllerNode: ASDisplayNode {
|
||||
func deactivateSearch() {
|
||||
if let searchDisplayController = self.searchDisplayController {
|
||||
var maybePlaceholderNode: SearchBarPlaceholderNode?
|
||||
self.listView.forEachItemNode { node in
|
||||
self.chatListNode.forEachItemNode { node in
|
||||
if let node = node as? ChatListSearchItemNode {
|
||||
maybePlaceholderNode = node.searchBarNode
|
||||
}
|
||||
|
||||
@@ -476,7 +476,7 @@ class ChatListItemNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
|
||||
}
|
||||
|
||||
|
||||
292
TelegramUI/ChatListNode.swift
Normal file
292
TelegramUI/ChatListNode.swift
Normal file
@@ -0,0 +1,292 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
|
||||
enum ChatListNodeMode {
|
||||
case chatList
|
||||
case peers
|
||||
}
|
||||
|
||||
struct ChatListNodeListViewTransition {
|
||||
let chatListView: ChatListNodeView
|
||||
let deleteItems: [ListViewDeleteItem]
|
||||
let insertItems: [ListViewInsertItem]
|
||||
let updateItems: [ListViewUpdateItem]
|
||||
let options: ListViewDeleteAndInsertOptions
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let stationaryItemRange: (Int, Int)?
|
||||
}
|
||||
|
||||
final class ChatListNodeInteraction {
|
||||
let activateSearch: () -> Void
|
||||
let peerSelected: (PeerId) -> Void
|
||||
|
||||
init(activateSearch: @escaping () -> Void, peerSelected: @escaping (PeerId) -> Void) {
|
||||
self.activateSearch = activateSearch
|
||||
self.peerSelected = peerSelected
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
|
||||
return entries.map { entry -> ListViewInsertItem in
|
||||
switch entry.entry {
|
||||
case .SearchEntry:
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: {
|
||||
nodeInteraction.activateSearch()
|
||||
}), directionHint: entry.directionHint)
|
||||
case let .MessageEntry(message, combinedReadState, notificationSettings):
|
||||
switch mode {
|
||||
case .chatList:
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(account: account, message: message, combinedReadState: combinedReadState, notificationSettings: notificationSettings, action: { _ in
|
||||
nodeInteraction.peerSelected(message.id.peerId)
|
||||
}), directionHint: entry.directionHint)
|
||||
case .peers:
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(account: account, peer: message.peers[message.id.peerId], presence: nil, index: nil, action: { _ in
|
||||
nodeInteraction.peerSelected(message.id.peerId)
|
||||
}), directionHint: entry.directionHint)
|
||||
}
|
||||
case .HoleEntry:
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint)
|
||||
case .Nothing:
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyItem(), directionHint: entry.directionHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
|
||||
return entries.map { entry -> ListViewUpdateItem in
|
||||
switch entry.entry {
|
||||
case .SearchEntry:
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: {
|
||||
nodeInteraction.activateSearch()
|
||||
}), directionHint: entry.directionHint)
|
||||
case let .MessageEntry(message, combinedReadState, notificationSettings):
|
||||
switch mode {
|
||||
case .chatList:
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(account: account, message: message, combinedReadState: combinedReadState, notificationSettings: notificationSettings, action: { _ in
|
||||
nodeInteraction.peerSelected(message.id.peerId)
|
||||
}), directionHint: entry.directionHint)
|
||||
case .peers:
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(account: account, peer: message.peers[message.id.peerId], presence: nil, index: nil, action: { _ in
|
||||
nodeInteraction.peerSelected(message.id.peerId)
|
||||
}), directionHint: entry.directionHint)
|
||||
}
|
||||
case .HoleEntry:
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint)
|
||||
case .Nothing:
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyItem(), directionHint: entry.directionHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedChatListNodeViewListTransition(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition {
|
||||
return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, nodeInteraction: nodeInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, nodeInteraction: nodeInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange)
|
||||
}
|
||||
|
||||
private final class ChatListOpaqueTransactionState {
|
||||
let chatListView: ChatListNodeView
|
||||
|
||||
init(chatListView: ChatListNodeView) {
|
||||
self.chatListView = chatListView
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatListNode: ListView {
|
||||
private let _ready = ValuePromise<Bool>()
|
||||
private var didSetReady = false
|
||||
var ready: Signal<Bool, NoError> {
|
||||
return _ready.get()
|
||||
}
|
||||
|
||||
var peerSelected: ((PeerId) -> Void)?
|
||||
var activateSearch: (() -> Void)?
|
||||
|
||||
private let viewProcessingQueue = Queue()
|
||||
private var chatListView: ChatListNodeView?
|
||||
|
||||
private var dequeuedInitialTransitionOnLayout = false
|
||||
private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)?
|
||||
|
||||
private var currentLocation: ChatListNodeLocation?
|
||||
private let chatListLocation = ValuePromise<ChatListNodeLocation>()
|
||||
private let chatListDisposable = MetaDisposable()
|
||||
|
||||
init(account: Account, mode: ChatListNodeMode) {
|
||||
super.init()
|
||||
|
||||
let nodeInteraction = ChatListNodeInteraction(activateSearch: { [weak self] in
|
||||
if let strongSelf = self, let activateSearch = strongSelf.activateSearch {
|
||||
activateSearch()
|
||||
}
|
||||
}, peerSelected: { [weak self] peerId in
|
||||
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
|
||||
peerSelected(peerId)
|
||||
}
|
||||
})
|
||||
|
||||
let viewProcessingQueue = self.viewProcessingQueue
|
||||
|
||||
let chastListViewUpdate = self.chatListLocation.get()
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { location in
|
||||
return chatListViewForLocation(location, account: account)
|
||||
}
|
||||
|
||||
let previousView = Atomic<ChatListNodeView?>(value: nil)
|
||||
|
||||
let chatListNodeViewTransition = chastListViewUpdate |> mapToQueue { [weak self] update -> Signal<ChatListNodeListViewTransition, NoError> in
|
||||
let processedView = ChatListNodeView(originalView: update.view, filteredEntries: chatListNodeEntriesForView(update.view))
|
||||
let previous = previousView.swap(processedView)
|
||||
|
||||
let reason: ChatListNodeViewTransitionReason
|
||||
var prepareOnMainQueue = false
|
||||
|
||||
var previousWasEmptyOrSingleHole = false
|
||||
if let previous = previous {
|
||||
if previous.filteredEntries.count == 1 {
|
||||
if case .HoleEntry = previous.filteredEntries[0] {
|
||||
previousWasEmptyOrSingleHole = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
previousWasEmptyOrSingleHole = true
|
||||
}
|
||||
|
||||
if previousWasEmptyOrSingleHole {
|
||||
reason = .initial
|
||||
if previous == nil {
|
||||
prepareOnMainQueue = true
|
||||
}
|
||||
} else {
|
||||
switch update.type {
|
||||
case .InitialUnread:
|
||||
reason = .initial
|
||||
prepareOnMainQueue = true
|
||||
case .Generic:
|
||||
reason = .interactiveChanges
|
||||
case .UpdateVisible:
|
||||
reason = .reload
|
||||
case .FillHole:
|
||||
reason = .reload
|
||||
}
|
||||
}
|
||||
|
||||
return preparedChatListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: update.scrollPosition)
|
||||
|> map({ mappedChatListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, mode: mode, transition: $0) })
|
||||
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
|
||||
}
|
||||
|
||||
let appliedTransition = chatListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal<Void, NoError> in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
||||
self.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in
|
||||
if let strongSelf = self, let range = range.loadedRange, let view = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.originalView {
|
||||
var location: ChatListNodeLocation?
|
||||
if range.firstIndex < 5 && view.laterIndex != nil {
|
||||
location = .navigation(index: view.entries[view.entries.count - 1].index)
|
||||
} else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlierIndex != nil {
|
||||
location = .navigation(index: view.entries[0].index)
|
||||
}
|
||||
|
||||
if let location = location, location != strongSelf.currentLocation {
|
||||
strongSelf.currentLocation = location
|
||||
strongSelf.chatListLocation.set(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.chatListDisposable.set(appliedTransition.start())
|
||||
|
||||
let initialLocation: ChatListNodeLocation = .initial(count: 50)
|
||||
self.currentLocation = initialLocation
|
||||
self.chatListLocation.set(initialLocation)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.chatListDisposable.dispose()
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: ChatListNodeListViewTransition) -> Signal<Void, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.enqueuedTransition {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
strongSelf.enqueuedTransition = (transition, {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.dequeueTransition()
|
||||
} else {
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
} |> runOn(Queue.mainQueue())
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let (transition, completion) = self.enqueuedTransition {
|
||||
self.enqueuedTransition = nil
|
||||
|
||||
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatListView = transition.chatListView
|
||||
|
||||
/*if let range = visibleRange.loadedRange {
|
||||
strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index)
|
||||
|
||||
if let visible = visibleRange.visibleRange {
|
||||
if let messageId = maxIncomingMessageIdForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visible.firstIndex)) {
|
||||
strongSelf.updateMaxVisibleReadIncomingMessageId(messageId)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(true)
|
||||
}
|
||||
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatListOpaqueTransactionState(chatListView: transition.chatListView), completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) {
|
||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
if !self.dequeuedInitialTransitionOnLayout {
|
||||
self.dequeuedInitialTransitionOnLayout = true
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToLatest() {
|
||||
if let view = self.chatListView?.originalView, view.laterIndex == nil {
|
||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
} else {
|
||||
let location: ChatListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .Top, animated: true)
|
||||
self.currentLocation = location
|
||||
self.chatListLocation.set(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
160
TelegramUI/ChatListNodeEntries.swift
Normal file
160
TelegramUI/ChatListNodeEntries.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
enum ChatListNodeEntryId: Hashable, CustomStringConvertible {
|
||||
case Search
|
||||
case Hole(Int64)
|
||||
case PeerId(Int64)
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case .Search:
|
||||
return 0
|
||||
case let .Hole(peerId):
|
||||
return peerId.hashValue
|
||||
case let .PeerId(peerId):
|
||||
return peerId.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .Search:
|
||||
return "search"
|
||||
case let .Hole(value):
|
||||
return "hole(\(value))"
|
||||
case let .PeerId(value):
|
||||
return "peerId(\(value))"
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ChatListNodeEntryId, rhs: ChatListNodeEntryId) -> Bool {
|
||||
return lhs.hashValue < rhs.hashValue
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatListNodeEntryId, rhs: ChatListNodeEntryId) -> Bool {
|
||||
switch lhs {
|
||||
case .Search:
|
||||
switch rhs {
|
||||
case .Search:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .Hole(lhsId):
|
||||
switch rhs {
|
||||
case .Hole(lhsId):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .PeerId(lhsId):
|
||||
switch rhs {
|
||||
case let .PeerId(rhsId):
|
||||
return lhsId == rhsId
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
case SearchEntry
|
||||
case MessageEntry(Message, CombinedPeerReadState?, PeerNotificationSettings?)
|
||||
case HoleEntry(ChatListHole)
|
||||
case Nothing(MessageIndex)
|
||||
|
||||
var index: MessageIndex {
|
||||
switch self {
|
||||
case .SearchEntry:
|
||||
return MessageIndex.absoluteUpperBound()
|
||||
case let .MessageEntry(message, _, _):
|
||||
return MessageIndex(message)
|
||||
case let .HoleEntry(hole):
|
||||
return hole.index
|
||||
case let .Nothing(index):
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: ChatListNodeEntryId {
|
||||
switch self {
|
||||
case .SearchEntry:
|
||||
return .Search
|
||||
case let .MessageEntry(message, _, _):
|
||||
return .PeerId(message.id.peerId.toInt64())
|
||||
case let .HoleEntry(hole):
|
||||
return .Hole(Int64(hole.index.id.id))
|
||||
case let .Nothing(index):
|
||||
return .PeerId(index.id.peerId.toInt64())
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .SearchEntry:
|
||||
switch rhs {
|
||||
case .SearchEntry:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .MessageEntry(lhsMessage, lhsUnreadCount, lhsNotificationSettings):
|
||||
switch rhs {
|
||||
case let .MessageEntry(rhsMessage, rhsUnreadCount, rhsNotificationSettings):
|
||||
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags || lhsUnreadCount != rhsUnreadCount {
|
||||
return false
|
||||
}
|
||||
if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings {
|
||||
if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsNotificationSettings != nil) != (rhsNotificationSettings != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
break
|
||||
}
|
||||
case let .HoleEntry(lhsHole):
|
||||
switch rhs {
|
||||
case let .HoleEntry(rhsHole):
|
||||
return lhsHole == rhsHole
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .Nothing(lhsIndex):
|
||||
switch rhs {
|
||||
case let .Nothing(rhsIndex):
|
||||
return lhsIndex == rhsIndex
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func chatListNodeEntriesForView(_ view: ChatListView) -> [ChatListNodeEntry] {
|
||||
var result: [ChatListNodeEntry] = []
|
||||
for entry in view.entries {
|
||||
switch entry {
|
||||
case let .MessageEntry(message, combinedReadState, notificationSettings):
|
||||
result.append(.MessageEntry(message, combinedReadState, notificationSettings))
|
||||
case let .HoleEntry(hole):
|
||||
result.append(.HoleEntry(hole))
|
||||
case let .Nothing(index):
|
||||
result.append(.Nothing(index))
|
||||
}
|
||||
}
|
||||
if view.laterIndex == nil {
|
||||
result.append(.SearchEntry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
69
TelegramUI/ChatListNodeLocation.swift
Normal file
69
TelegramUI/ChatListNodeLocation.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
enum ChatListNodeLocation: Equatable {
|
||||
case initial(count: Int)
|
||||
case navigation(index: MessageIndex)
|
||||
case scroll(index: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition, animated: Bool)
|
||||
|
||||
static func ==(lhs: ChatListNodeLocation, rhs: ChatListNodeLocation) -> Bool {
|
||||
switch lhs {
|
||||
case let .navigation(index):
|
||||
switch rhs {
|
||||
case .navigation(index):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatListNodeViewUpdate {
|
||||
let view: ChatListView
|
||||
let type: ViewUpdateType
|
||||
let scrollPosition: ChatListNodeViewScrollPosition?
|
||||
}
|
||||
|
||||
func chatListViewForLocation(_ location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
|
||||
switch location {
|
||||
case let .initial(count):
|
||||
let signal: Signal<(ChatListView, ViewUpdateType), NoError>
|
||||
signal = account.postbox.tailChatListView(count)
|
||||
return signal |> map { view, updateType -> ChatListNodeViewUpdate in
|
||||
return ChatListNodeViewUpdate(view: view, type: updateType, scrollPosition: nil)
|
||||
}
|
||||
case let .navigation(index):
|
||||
var first = true
|
||||
return account.postbox.aroundChatListView(index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in
|
||||
let genericType: ViewUpdateType
|
||||
if first {
|
||||
first = false
|
||||
genericType = ViewUpdateType.UpdateVisible
|
||||
} else {
|
||||
genericType = updateType
|
||||
}
|
||||
return ChatListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil)
|
||||
}
|
||||
case let .scroll(index, sourceIndex, scrollPosition, animated):
|
||||
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
|
||||
let chatScrollPosition: ChatListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
|
||||
var first = true
|
||||
return account.postbox.aroundChatListView(index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in
|
||||
let genericType: ViewUpdateType
|
||||
let scrollPosition: ChatListNodeViewScrollPosition? = first ? chatScrollPosition : nil
|
||||
if first {
|
||||
first = false
|
||||
genericType = ViewUpdateType.UpdateVisible
|
||||
} else {
|
||||
genericType = updateType
|
||||
}
|
||||
return ChatListNodeViewUpdate(view: view, type: genericType, scrollPosition: scrollPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
170
TelegramUI/ChatListViewTransition.swift
Normal file
170
TelegramUI/ChatListViewTransition.swift
Normal file
@@ -0,0 +1,170 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
struct ChatListNodeView {
|
||||
let originalView: ChatListView
|
||||
let filteredEntries: [ChatListNodeEntry]
|
||||
}
|
||||
|
||||
enum ChatListNodeViewTransitionReason {
|
||||
case initial
|
||||
case interactiveChanges
|
||||
case holeChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection])
|
||||
case reload
|
||||
}
|
||||
|
||||
struct ChatListNodeViewTransitionInsertEntry {
|
||||
let index: Int
|
||||
let previousIndex: Int?
|
||||
let entry: ChatListNodeEntry
|
||||
let directionHint: ListViewItemOperationDirectionHint?
|
||||
}
|
||||
|
||||
struct ChatListNodeViewTransitionUpdateEntry {
|
||||
let index: Int
|
||||
let previousIndex: Int
|
||||
let entry: ChatListNodeEntry
|
||||
let directionHint: ListViewItemOperationDirectionHint?
|
||||
}
|
||||
|
||||
struct ChatListNodeViewTransition {
|
||||
let chatListView: ChatListNodeView
|
||||
let deleteItems: [ListViewDeleteItem]
|
||||
let insertEntries: [ChatListNodeViewTransitionInsertEntry]
|
||||
let updateEntries: [ChatListNodeViewTransitionUpdateEntry]
|
||||
let options: ListViewDeleteAndInsertOptions
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let stationaryItemRange: (Int, Int)?
|
||||
}
|
||||
|
||||
enum ChatListNodeViewScrollPosition {
|
||||
case index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
|
||||
}
|
||||
|
||||
func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toView: ChatListNodeView, reason: ChatListNodeViewTransitionReason, account: Account, scrollPosition: ChatListNodeViewScrollPosition?) -> Signal<ChatListNodeViewTransition, NoError> {
|
||||
return Signal { subscriber in
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries)
|
||||
|
||||
var adjustedDeleteIndices: [ListViewDeleteItem] = []
|
||||
let previousCount: Int
|
||||
if let fromView = fromView {
|
||||
previousCount = fromView.filteredEntries.count
|
||||
} else {
|
||||
previousCount = 0;
|
||||
}
|
||||
for index in deleteIndices {
|
||||
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
|
||||
}
|
||||
var adjustedIndicesAndItems: [ChatListNodeViewTransitionInsertEntry] = []
|
||||
var adjustedUpdateItems: [ChatListNodeViewTransitionUpdateEntry] = []
|
||||
let updatedCount = toView.filteredEntries.count
|
||||
|
||||
var options: ListViewDeleteAndInsertOptions = []
|
||||
var maxAnimatedInsertionIndex = -1
|
||||
var stationaryItemRange: (Int, Int)?
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
|
||||
switch reason {
|
||||
case .initial:
|
||||
let _ = options.insert(.LowLatency)
|
||||
let _ = options.insert(.Synchronous)
|
||||
case .interactiveChanges:
|
||||
let _ = options.insert(.AnimateAlpha)
|
||||
let _ = options.insert(.AnimateInsertion)
|
||||
|
||||
for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) {
|
||||
let adjustedIndex = updatedCount - 1 - index
|
||||
if adjustedIndex == maxAnimatedInsertionIndex + 1 {
|
||||
maxAnimatedInsertionIndex += 1
|
||||
}
|
||||
}
|
||||
case .reload:
|
||||
break
|
||||
case let .holeChanges(filledHoleDirections, removeHoleDirections):
|
||||
if let (_, removeDirection) = removeHoleDirections.first {
|
||||
switch removeDirection {
|
||||
case .LowerToUpper:
|
||||
var holeIndex: MessageIndex?
|
||||
for (index, _) in filledHoleDirections {
|
||||
if holeIndex == nil || index < holeIndex! {
|
||||
holeIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
if let holeIndex = holeIndex {
|
||||
for i in 0 ..< toView.filteredEntries.count {
|
||||
if toView.filteredEntries[i].index >= holeIndex {
|
||||
let index = toView.filteredEntries.count - 1 - (i - 1)
|
||||
stationaryItemRange = (index, Int.max)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case .UpperToLower:
|
||||
break
|
||||
case .AroundIndex:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (index, entry, previousIndex) in indicesAndItems {
|
||||
let adjustedIndex = updatedCount - 1 - index
|
||||
|
||||
let adjustedPrevousIndex: Int?
|
||||
if let previousIndex = previousIndex {
|
||||
adjustedPrevousIndex = previousCount - 1 - previousIndex
|
||||
} else {
|
||||
adjustedPrevousIndex = nil
|
||||
}
|
||||
|
||||
var directionHint: ListViewItemOperationDirectionHint?
|
||||
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
|
||||
directionHint = .Down
|
||||
}
|
||||
|
||||
adjustedIndicesAndItems.append(ChatListNodeViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint))
|
||||
}
|
||||
|
||||
for (index, entry, previousIndex) in updateIndices {
|
||||
let adjustedIndex = updatedCount - 1 - index
|
||||
let adjustedPreviousIndex = previousCount - 1 - previousIndex
|
||||
|
||||
let directionHint: ListViewItemOperationDirectionHint? = nil
|
||||
adjustedUpdateItems.append(ChatListNodeViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint))
|
||||
}
|
||||
|
||||
if let scrollPosition = scrollPosition {
|
||||
switch scrollPosition {
|
||||
case let .index(scrollIndex, position, directionHint, animated):
|
||||
var index = toView.filteredEntries.count - 1
|
||||
for entry in toView.filteredEntries {
|
||||
if entry.index >= scrollIndex {
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
|
||||
break
|
||||
}
|
||||
index -= 1
|
||||
}
|
||||
|
||||
if scrollToItem == nil {
|
||||
var index = 0
|
||||
for entry in toView.filteredEntries.reversed() {
|
||||
if entry.index < scrollIndex {
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.putNext(ChatListNodeViewTransition(chatListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange))
|
||||
subscriber.putCompletion()
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
@@ -135,12 +135,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let boundingSize = self.bounds.insetBy(dx: 6.0, dy: 6.0).size
|
||||
let bounds = self.bounds
|
||||
let boundingSize = bounds.insetBy(dx: 6.0, dy: 6.0).size
|
||||
|
||||
if let (_, _, mediaDimensions) = self.currentState {
|
||||
let imageSize = mediaDimensions.aspectFitted(boundingSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: imageSize)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,8 +117,8 @@ class ChatMessageActionItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration)
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
||||
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
@@ -177,8 +177,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration)
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
||||
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
|
||||
@@ -95,11 +95,13 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration)
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
if short {
|
||||
self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
} else {
|
||||
self.transitionOffset = -self.bounds.size.height * 1.6
|
||||
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
|
||||
}
|
||||
//self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration)
|
||||
}
|
||||
|
||||
|
||||
@@ -84,8 +84,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration)
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
||||
|
||||
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
@@ -102,8 +102,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
private var presentationInterfaceState = ChatPresentationInterfaceState()
|
||||
|
||||
private var keepSendButtonEnabled = false
|
||||
|
||||
var inputTextState: ChatTextInputState {
|
||||
get {
|
||||
if let textInputNode = self.textInputNode {
|
||||
let text = textInputNode.attributedText?.string ?? ""
|
||||
let selectionRange: Range<Int> = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length)
|
||||
@@ -111,14 +112,28 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
} else {
|
||||
return ChatTextInputState()
|
||||
}
|
||||
} set(value) {
|
||||
}
|
||||
|
||||
func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, animated: Bool) {
|
||||
if !state.inputText.isEmpty && self.textInputNode == nil {
|
||||
self.loadTextInputNode()
|
||||
}
|
||||
|
||||
if let textInputNode = self.textInputNode {
|
||||
self.updatingInputState = true
|
||||
textInputNode.attributedText = NSAttributedString(string: value.inputText, font: Font.regular(17.0), textColor: UIColor.black)
|
||||
textInputNode.selectedRange = NSMakeRange(value.selectionRange.lowerBound, value.selectionRange.count)
|
||||
textInputNode.attributedText = NSAttributedString(string: state.inputText, font: Font.regular(17.0), textColor: UIColor.black)
|
||||
textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count)
|
||||
self.updatingInputState = false
|
||||
self.keepSendButtonEnabled = keepSendButtonEnabled
|
||||
self.updateTextNodeText(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func updateKeepSendButtonEnabled(keepSendButtonEnabled: Bool, animated: Bool) {
|
||||
if keepSendButtonEnabled != self.keepSendButtonEnabled {
|
||||
self.keepSendButtonEnabled = keepSendButtonEnabled
|
||||
self.updateTextNodeText(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
var text: String {
|
||||
@@ -360,9 +375,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let textInputNode = self.textInputNode {
|
||||
self.textPlaceholderNode.isHidden = editableTextNode.attributedText?.length ?? 0 != 0
|
||||
self.interfaceInteraction?.updateTextInputState(self.inputTextState)
|
||||
self.updateTextNodeText(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
if let text = self.textInputNode?.attributedText, text.length != 0 {
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
var hasText = false
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
||||
hasText = true
|
||||
}
|
||||
self.textPlaceholderNode.isHidden = hasText
|
||||
|
||||
if hasText || self.keepSendButtonEnabled {
|
||||
if self.sendButton.alpha.isZero {
|
||||
self.sendButton.alpha = 1.0
|
||||
self.micButton.alpha = 0.0
|
||||
@@ -380,15 +405,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
self.interfaceInteraction?.updateTextInputState(self.inputTextState)
|
||||
|
||||
let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight)
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) {
|
||||
if !dueToEditing && !updatingInputState {
|
||||
@@ -412,10 +434,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
@objc func sendButtonPressed() {
|
||||
let text = self.textInputNode?.attributedText?.string ?? ""
|
||||
if !text.isEmpty {
|
||||
self.sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func attachmentButtonPressed() {
|
||||
self.displayAttachmentMenu()
|
||||
|
||||
@@ -52,8 +52,8 @@ class ChatUnreadItemNode: ListViewItemNode {
|
||||
self.scrollPositioningInsets = UIEdgeInsets(top: 5.0, left: 0.0, bottom: 5.0, right: 0.0)
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration)
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
||||
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
|
||||
@@ -12,14 +12,14 @@ private let statusFont = Font.regular(13.0)
|
||||
|
||||
class ContactsPeerItem: ListViewItem {
|
||||
let account: Account
|
||||
let peer: Peer
|
||||
let peer: Peer?
|
||||
let presence: PeerPresence?
|
||||
let action: (Peer) -> Void
|
||||
let selectable: Bool = true
|
||||
|
||||
let headerAccessoryItem: ListViewAccessoryItem?
|
||||
|
||||
init(account: Account, peer: Peer, presence: PeerPresence?, index: PeerNameIndex?, action: @escaping (Peer) -> Void) {
|
||||
init(account: Account, peer: Peer?, presence: PeerPresence?, index: PeerNameIndex?, action: @escaping (Peer) -> Void) {
|
||||
self.account = account
|
||||
self.peer = peer
|
||||
self.presence = presence
|
||||
@@ -115,7 +115,9 @@ class ContactsPeerItem: ListViewItem {
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.action(self.peer)
|
||||
if let peer = self.peer {
|
||||
self.action(peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +132,7 @@ class ContactsPeerItemNode: ListViewItemNode {
|
||||
private let titleNode: TextNode
|
||||
private let statusNode: TextNode
|
||||
|
||||
private var avatarState: (Account, Peer)?
|
||||
private var avatarState: (Account, Peer?)?
|
||||
|
||||
private var peerPresenceManager: PeerPresenceStatusManager?
|
||||
private var layoutParams: (ContactsPeerItem, CGFloat, Bool, Bool)?
|
||||
@@ -228,8 +230,7 @@ class ContactsPeerItemNode: ListViewItemNode {
|
||||
var titleAttributedString: NSAttributedString?
|
||||
var statusAttributedString: NSAttributedString?
|
||||
|
||||
let peer = item.peer
|
||||
|
||||
if let peer = item.peer {
|
||||
if let user = peer as? TelegramUser {
|
||||
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
|
||||
let string = NSMutableAttributedString()
|
||||
@@ -249,14 +250,13 @@ class ContactsPeerItemNode: ListViewItemNode {
|
||||
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp))
|
||||
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? UIColor(0x007ee5) : UIColor(0xa6a6a6))
|
||||
} else {
|
||||
statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6))
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black)
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black)
|
||||
}
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), nil)
|
||||
|
||||
@@ -264,18 +264,24 @@ class ContactsPeerItemNode: ListViewItemNode {
|
||||
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 48.0), insets: UIEdgeInsets(top: first ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
||||
|
||||
let titleFrame: CGRect
|
||||
if statusAttributedString != nil {
|
||||
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 4.0), size: titleLayout.size)
|
||||
} else {
|
||||
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 13.0), size: titleLayout.size)
|
||||
}
|
||||
|
||||
return (nodeLayout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.layoutParams = (item, width, first, last)
|
||||
|
||||
if strongSelf.avatarState == nil || strongSelf.avatarState!.0 !== item.account || !strongSelf.avatarState!.1.isEqual(peer) {
|
||||
strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer)
|
||||
if let peer = item.peer {
|
||||
strongSelf.avatarNode.setPeer(account: item.account, peer: peer)
|
||||
}
|
||||
|
||||
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0))
|
||||
|
||||
let _ = titleApply()
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 4.0), size: titleLayout.size)
|
||||
strongSelf.titleNode.frame = titleFrame
|
||||
|
||||
let _ = statusApply()
|
||||
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 25.0), size: statusLayout.size)
|
||||
@@ -299,7 +305,7 @@ class ContactsPeerItemNode: ListViewItemNode {
|
||||
accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0))
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
|
||||
}
|
||||
|
||||
|
||||
106
TelegramUI/ForwardAccessoryPanelNode.swift
Normal file
106
TelegramUI/ForwardAccessoryPanelNode.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
final class ForwardAccessoryPanelNode: AccessoryPanelNode {
|
||||
private let messageDisposable = MetaDisposable()
|
||||
let messageIds: [MessageId]
|
||||
|
||||
let closeButton: ASButtonNode
|
||||
let lineNode: ASDisplayNode
|
||||
let titleNode: ASTextNode
|
||||
let textNode: ASTextNode
|
||||
|
||||
init(account: Account, messageIds: [MessageId]) {
|
||||
self.messageIds = messageIds
|
||||
|
||||
self.closeButton = ASButtonNode()
|
||||
self.closeButton.setImage(UIImage(bundleImageName: "Chat/Input/Acessory Panels/CloseButton")?.precomposed(), for: [])
|
||||
self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0)
|
||||
self.closeButton.displaysAsynchronously = false
|
||||
|
||||
self.lineNode = ASDisplayNode()
|
||||
self.lineNode.backgroundColor = UIColor(0x007ee5)
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.truncationMode = .byTruncatingTail
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.truncationMode = .byTruncatingTail
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
|
||||
self.addSubnode(self.closeButton)
|
||||
|
||||
self.addSubnode(self.lineNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.messageDisposable.set((account.postbox.messagesAtIds(messageIds)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
||||
if let strongSelf = self {
|
||||
var authors = ""
|
||||
var uniquePeerIds = Set<PeerId>()
|
||||
var text = ""
|
||||
for message in messages {
|
||||
if let author = message.author, !uniquePeerIds.contains(author.id) {
|
||||
uniquePeerIds.insert(author.id)
|
||||
if !authors.isEmpty {
|
||||
authors.append(", ")
|
||||
}
|
||||
authors.append(author.compactDisplayTitle)
|
||||
}
|
||||
}
|
||||
if messages.count == 1 {
|
||||
text = messages[0].text
|
||||
} else {
|
||||
text = "\(messages.count) messages"
|
||||
}
|
||||
|
||||
strongSelf.titleNode.attributedText = NSAttributedString(string: authors, font: Font.regular(14.5), textColor: UIColor(0x007ee5))
|
||||
strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.5), textColor: UIColor.black)
|
||||
|
||||
strongSelf.setNeedsLayout()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.messageDisposable.dispose()
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: constrainedSize.width, height: 40.0)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let bounds = self.bounds
|
||||
|
||||
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
|
||||
self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - self.insets.right - closeButtonSize.width, y: 12.0), size: closeButtonSize)
|
||||
|
||||
self.lineNode.frame = CGRect(origin: CGPoint(x: self.insets.left, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 5.0))
|
||||
|
||||
let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 11.0 - insets.left - insets.right - 14.0, height: bounds.size.height))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: self.insets.left + 11.0, y: 7.0), size: titleSize)
|
||||
|
||||
let textSize = self.textNode.measure(CGSize(width: bounds.size.width - 11.0 - insets.left - insets.right - 14.0, height: bounds.size.height))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: self.insets.left + 11.0, y: 25.0), size: textSize)
|
||||
}
|
||||
|
||||
@objc func closePressed() {
|
||||
if let dismiss = self.dismiss {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import Foundation
|
||||
private class FrameworkBundleClass: NSObject {
|
||||
}
|
||||
|
||||
private let frameworkBundle: Bundle = Bundle(for: FrameworkBundleClass.self)
|
||||
let frameworkBundle: Bundle = Bundle(for: FrameworkBundleClass.self)
|
||||
private let screenScaleFactor = Int(UIScreen.main.scale)
|
||||
|
||||
extension UIImage {
|
||||
|
||||
@@ -156,7 +156,7 @@ class GalleryController: ViewController {
|
||||
let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, tagMask: tags)
|
||||
|
||||
return view
|
||||
|> mapToSignal { (view, _) -> Signal<GalleryMessageHistoryView?, Void> in
|
||||
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, Void> in
|
||||
let mapped = GalleryMessageHistoryView.view(view)
|
||||
return .single(mapped)
|
||||
}
|
||||
|
||||
@@ -213,8 +213,8 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration)
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
||||
|
||||
self.transitionOffset = self.bounds.size.height * 1.6
|
||||
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
|
||||
|
||||
@@ -79,8 +79,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration)
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
||||
|
||||
self.transitionOffset = self.bounds.size.height * 1.6
|
||||
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
|
||||
|
||||
@@ -246,7 +246,7 @@ class PeerInfoActionItemNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ class PeerInfoDisclosureItemNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ class PeerInfoPeerActionItemNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ class PeerInfoPeerItemNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class PeerInfoTextWithLabelItemNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
|
||||
134
TelegramUI/PeerSelectionController.swift
Normal file
134
TelegramUI/PeerSelectionController.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
|
||||
public final class PeerSelectionController: ViewController {
|
||||
private let account: Account
|
||||
|
||||
var peerSelected: ((PeerId) -> Void)?
|
||||
|
||||
private var peerSelectionNode: PeerSelectionControllerNode {
|
||||
return super.displayNode as! PeerSelectionControllerNode
|
||||
}
|
||||
|
||||
let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable()
|
||||
|
||||
public init(account: Account) {
|
||||
self.account = account
|
||||
|
||||
super.init()
|
||||
|
||||
self.title = "Forward"
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.peerSelectionNode.chatListNode.scrollToLatest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.openMessageFromSearchDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PeerSelectionControllerNode(account: self.account, dismiss: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
})
|
||||
self.displayNode.backgroundColor = .white
|
||||
|
||||
self.peerSelectionNode.navigationBar = self.navigationBar
|
||||
|
||||
self.peerSelectionNode.requestDeactivateSearch = { [weak self] in
|
||||
self?.deactivateSearch()
|
||||
}
|
||||
|
||||
self.peerSelectionNode.chatListNode.activateSearch = { [weak self] in
|
||||
self?.activateSearch()
|
||||
}
|
||||
|
||||
self.peerSelectionNode.chatListNode.peerSelected = { [weak self] peerId in
|
||||
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
|
||||
peerSelected(peerId)
|
||||
}
|
||||
}
|
||||
|
||||
self.peerSelectionNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in
|
||||
if let strongSelf = self {
|
||||
let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in
|
||||
if modifier.getPeer(peer.id) == nil {
|
||||
modifier.updatePeers([peer], update: { previousPeer, updatedPeer in
|
||||
return updatedPeer
|
||||
})
|
||||
}
|
||||
}
|
||||
strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in
|
||||
if let strongSelf = strongSelf {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: messageId.peerId, messageId: messageId))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
self.peerSelectionNode.requestOpenPeerFromSearch = { [weak self] peerId in
|
||||
if let strongSelf = self {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))
|
||||
}
|
||||
}
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.peerSelectionNode.animateIn()
|
||||
}
|
||||
|
||||
override public func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.peerSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition)
|
||||
}
|
||||
|
||||
@objc func cancelPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
private func activateSearch() {
|
||||
if self.displayNavigationBar {
|
||||
if let scrollToTop = self.scrollToTop {
|
||||
scrollToTop()
|
||||
}
|
||||
self.peerSelectionNode.activateSearch()
|
||||
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
private func deactivateSearch() {
|
||||
if !self.displayNavigationBar {
|
||||
self.peerSelectionNode.deactivateSearch()
|
||||
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
public func dismiss() {
|
||||
self.peerSelectionNode.animateOut()
|
||||
}
|
||||
}
|
||||
139
TelegramUI/PeerSelectionControllerNode.swift
Normal file
139
TelegramUI/PeerSelectionControllerNode.swift
Normal file
@@ -0,0 +1,139 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
private let account: Account
|
||||
private let dismiss: () -> Void
|
||||
|
||||
let chatListNode: ChatListNode
|
||||
var navigationBar: NavigationBar?
|
||||
|
||||
private var searchDisplayController: SearchDisplayController?
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
var requestDeactivateSearch: (() -> Void)?
|
||||
var requestOpenPeerFromSearch: ((PeerId) -> Void)?
|
||||
var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)?
|
||||
|
||||
init(account: Account, dismiss: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.dismiss = dismiss
|
||||
self.chatListNode = ChatListNode(account: account, mode: .peers)
|
||||
|
||||
super.init(viewBlock: {
|
||||
return UITracingLayerView()
|
||||
}, didLoad: nil)
|
||||
|
||||
self.addSubnode(self.chatListNode)
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
|
||||
|
||||
self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
||||
self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
||||
|
||||
var duration: Double = 0.0
|
||||
var curve: UInt = 0
|
||||
switch transition {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(animationDuration, animationCurve):
|
||||
duration = animationDuration
|
||||
switch animationCurve {
|
||||
case .easeInOut:
|
||||
break
|
||||
case .spring:
|
||||
curve = 7
|
||||
}
|
||||
}
|
||||
|
||||
let listViewCurve: ListViewAnimationCurve
|
||||
var speedFactor: CGFloat = 1.0
|
||||
if curve == 7 {
|
||||
listViewCurve = .Spring(duration: duration)
|
||||
} else {
|
||||
listViewCurve = .Default
|
||||
}
|
||||
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve)
|
||||
|
||||
self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
|
||||
|
||||
if let searchDisplayController = self.searchDisplayController {
|
||||
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func activateSearch() {
|
||||
guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else {
|
||||
return
|
||||
}
|
||||
|
||||
var maybePlaceholderNode: SearchBarPlaceholderNode?
|
||||
self.chatListNode.forEachItemNode { node in
|
||||
if let node = node as? ChatListSearchItemNode {
|
||||
maybePlaceholderNode = node.searchBarNode
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = self.searchDisplayController {
|
||||
return
|
||||
}
|
||||
|
||||
if let placeholderNode = maybePlaceholderNode {
|
||||
self.searchDisplayController = SearchDisplayController(contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in
|
||||
if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch {
|
||||
requestOpenPeerFromSearch(peerId)
|
||||
}
|
||||
}, openMessage: { [weak self] peer, messageId in
|
||||
if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch {
|
||||
requestOpenMessageFromSearch(peer, messageId)
|
||||
}
|
||||
}), cancel: { [weak self] in
|
||||
if let requestDeactivateSearch = self?.requestDeactivateSearch {
|
||||
requestDeactivateSearch()
|
||||
}
|
||||
})
|
||||
|
||||
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
self.searchDisplayController?.activate(insertSubnode: { subnode in
|
||||
self.insertSubnode(subnode, belowSubnode: navigationBar)
|
||||
}, placeholder: placeholderNode)
|
||||
}
|
||||
}
|
||||
|
||||
func deactivateSearch() {
|
||||
if let searchDisplayController = self.searchDisplayController {
|
||||
var maybePlaceholderNode: SearchBarPlaceholderNode?
|
||||
self.chatListNode.forEachItemNode { node in
|
||||
if let node = node as? ChatListSearchItemNode {
|
||||
maybePlaceholderNode = node.searchBarNode
|
||||
}
|
||||
}
|
||||
|
||||
searchDisplayController.deactivate(placeholder: maybePlaceholderNode)
|
||||
self.searchDisplayController = nil
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import Postbox
|
||||
import TelegramCore
|
||||
import Display
|
||||
|
||||
func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?) -> Signal<ChatHistoryViewTransition, NoError> {
|
||||
func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?) -> Signal<ChatHistoryViewTransition, NoError> {
|
||||
return Signal { subscriber in
|
||||
//let updateIndices: [(Int, ChatHistoryEntry, Int)] = []
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries)
|
||||
@@ -169,7 +169,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange))
|
||||
subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData))
|
||||
subscriber.putCompletion()
|
||||
|
||||
return EmptyDisposable
|
||||
|
||||
34
TelegramUI/ServiceSoundManager.swift
Normal file
34
TelegramUI/ServiceSoundManager.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import AudioToolbox
|
||||
|
||||
private func loadSystemSoundFromBundle(name: String) -> SystemSoundID? {
|
||||
let path = "\(frameworkBundle.resourcePath!)/\(name)"
|
||||
let url = URL(fileURLWithPath: path)
|
||||
var sound: SystemSoundID = 0
|
||||
if AudioServicesCreateSystemSoundID(url as CFURL, &sound) == noErr {
|
||||
return sound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
final class ServiceSoundManager {
|
||||
private let queue = Queue()
|
||||
private var messageDeliverySound: SystemSoundID?
|
||||
|
||||
init() {
|
||||
self.queue.async {
|
||||
self.messageDeliverySound = loadSystemSoundFromBundle(name: "MessageSent.caf")
|
||||
}
|
||||
}
|
||||
|
||||
func playMessageDeliveredSound() {
|
||||
self.queue.async {
|
||||
if let messageDeliverySound = self.messageDeliverySound {
|
||||
AudioServicesPlaySystemSound(messageDeliverySound)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let serviceSoundManager = ServiceSoundManager()
|
||||
BIN
TelegramUI/Sounds/MessageSent.caf
Normal file
BIN
TelegramUI/Sounds/MessageSent.caf
Normal file
Binary file not shown.
Reference in New Issue
Block a user