mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-17 03:40:18 +00:00
Merge branch 'master' of github.com:peter-iakovlev/TelegramUI
# Conflicts: # TelegramUI/ChatTitleView.swift # TelegramUI/OpenUrl.swift
This commit is contained in:
commit
454146cb80
12
Images.xcassets/Chat/Message/FileCloudFetch.imageset/Contents.json
vendored
Normal file
12
Images.xcassets/Chat/Message/FileCloudFetch.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "cloud.pdf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Images.xcassets/Chat/Message/FileCloudFetch.imageset/cloud.pdf
vendored
Normal file
BIN
Images.xcassets/Chat/Message/FileCloudFetch.imageset/cloud.pdf
vendored
Normal file
Binary file not shown.
@ -128,6 +128,8 @@
|
|||||||
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383EDB207D1A1600C45548 /* emoji_suggestions.h */; };
|
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383EDB207D1A1600C45548 /* emoji_suggestions.h */; };
|
||||||
D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */; };
|
D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */; };
|
||||||
D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */; };
|
D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */; };
|
||||||
|
D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */; };
|
||||||
|
D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */; };
|
||||||
D03AA4DF202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */; };
|
D03AA4DF202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */; };
|
||||||
D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */; };
|
D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */; };
|
||||||
D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */; };
|
D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */; };
|
||||||
@ -1257,6 +1259,8 @@
|
|||||||
D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = "<group>"; };
|
D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = "<group>"; };
|
||||||
D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = "<group>"; };
|
D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = "<group>"; };
|
||||||
D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingCancelIndicator.swift; sourceTree = "<group>"; };
|
D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingCancelIndicator.swift; sourceTree = "<group>"; };
|
||||||
|
D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialCloudProgressContentNode.swift; sourceTree = "<group>"; };
|
||||||
|
D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformVideoContent.swift; sourceTree = "<group>"; };
|
||||||
D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatContextResultPeekContentNode.swift; sourceTree = "<group>"; };
|
D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatContextResultPeekContentNode.swift; sourceTree = "<group>"; };
|
||||||
D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewPeekContent.swift; sourceTree = "<group>"; };
|
D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewPeekContent.swift; sourceTree = "<group>"; };
|
||||||
D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListEditableReorderControlNode.swift; sourceTree = "<group>"; };
|
D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListEditableReorderControlNode.swift; sourceTree = "<group>"; };
|
||||||
@ -2291,6 +2295,7 @@
|
|||||||
D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */,
|
D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */,
|
||||||
D01776B41F1D6CCC0044446D /* RadialStatusContentNode.swift */,
|
D01776B41F1D6CCC0044446D /* RadialStatusContentNode.swift */,
|
||||||
D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */,
|
D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */,
|
||||||
|
D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */,
|
||||||
D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */,
|
D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */,
|
||||||
D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */,
|
D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */,
|
||||||
D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */,
|
D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */,
|
||||||
@ -2555,6 +2560,7 @@
|
|||||||
D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */,
|
D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */,
|
||||||
D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */,
|
D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */,
|
||||||
D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */,
|
D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */,
|
||||||
|
D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */,
|
||||||
D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */,
|
D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */,
|
||||||
D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */,
|
D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */,
|
||||||
D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */,
|
D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */,
|
||||||
@ -5122,6 +5128,7 @@
|
|||||||
D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */,
|
D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */,
|
||||||
D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */,
|
D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */,
|
||||||
D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */,
|
D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */,
|
||||||
|
D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */,
|
||||||
D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */,
|
D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */,
|
||||||
D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */,
|
D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */,
|
||||||
D0430B001FF4570500A35ADD /* WebController.swift in Sources */,
|
D0430B001FF4570500A35ADD /* WebController.swift in Sources */,
|
||||||
@ -5330,6 +5337,7 @@
|
|||||||
D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */,
|
D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */,
|
||||||
D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */,
|
D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */,
|
||||||
D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */,
|
D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */,
|
||||||
|
D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */,
|
||||||
D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */,
|
D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */,
|
||||||
D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */,
|
D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */,
|
||||||
D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */,
|
D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */,
|
||||||
|
|||||||
@ -696,6 +696,26 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
|
|||||||
case let .webToken(token):
|
case let .webToken(token):
|
||||||
credentials = .generic(data: token.data, saveOnServer: token.saveOnServer)
|
credentials = .generic(data: token.data, saveOnServer: token.saveOnServer)
|
||||||
case .applePayStripe:
|
case .applePayStripe:
|
||||||
|
guard let paymentForm = self.paymentFormValue, let nativeProvider = paymentForm.nativeProvider else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[strongSelf->_paymentForm.nativeParams dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
|
||||||
|
guard let nativeParamsData = nativeProvider.params.data(using: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let nativeParams = (try? JSONSerialization.jsonObject(with: nativeParamsData, options: [])) as? [String: Any] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let merchantId: String
|
||||||
|
if nativeProvider.name == "stripe" {
|
||||||
|
merchantId = "merchant.ph.telegra.Telegraph"
|
||||||
|
} else if let paramsId = nativeParams["apple_pay_merchant_id"] as? String {
|
||||||
|
merchantId = paramsId
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let botPeerId = self.messageId.peerId
|
let botPeerId = self.messageId.peerId
|
||||||
let _ = (self.account.postbox.transaction({ transaction -> Peer? in
|
let _ = (self.account.postbox.transaction({ transaction -> Peer? in
|
||||||
return transaction.getPeer(botPeerId)
|
return transaction.getPeer(botPeerId)
|
||||||
@ -703,7 +723,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
|
|||||||
if let strongSelf = self, let botPeer = botPeer {
|
if let strongSelf = self, let botPeer = botPeer {
|
||||||
let request = PKPaymentRequest()
|
let request = PKPaymentRequest()
|
||||||
|
|
||||||
request.merchantIdentifier = "merchant.ph.telegra.Telegraph"
|
request.merchantIdentifier = merchantId
|
||||||
request.supportedNetworks = [.visa, .amex, .masterCard]
|
request.supportedNetworks = [.visa, .amex, .masterCard]
|
||||||
request.merchantCapabilities = [.capability3DS]
|
request.merchantCapabilities = [.capability3DS]
|
||||||
request.countryCode = "US"
|
request.countryCode = "US"
|
||||||
@ -898,41 +918,50 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
|
|||||||
guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else {
|
guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let publishableKey = nativeParams["publishable_key"] as? String else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let signal: Signal<STPToken, Error> = Signal { subscriber in
|
if nativeProvider.name == "stripe" {
|
||||||
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
|
guard let publishableKey = nativeParams["publishable_key"] as? String else {
|
||||||
configuration.smsAutofillDisabled = true
|
return
|
||||||
configuration.publishableKey = publishableKey
|
}
|
||||||
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
|
|
||||||
|
|
||||||
let apiClient = STPAPIClient(configuration: configuration)
|
let signal: Signal<STPToken, Error> = Signal { subscriber in
|
||||||
|
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
|
||||||
apiClient.createToken(with: payment, completion: { token, error in
|
configuration.smsAutofillDisabled = true
|
||||||
if let token = token {
|
configuration.publishableKey = publishableKey
|
||||||
subscriber.putNext(token)
|
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
|
||||||
subscriber.putCompletion()
|
|
||||||
} else if let error = error {
|
let apiClient = STPAPIClient(configuration: configuration)
|
||||||
subscriber.putError(error)
|
|
||||||
|
apiClient.createToken(with: payment, completion: { token, error in
|
||||||
|
if let token = token {
|
||||||
|
subscriber.putNext(token)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
} else if let error = error {
|
||||||
|
subscriber.putError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return ActionDisposable {
|
self.paymentAuthDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] token in
|
||||||
}
|
if let strongSelf = self {
|
||||||
}
|
strongSelf.applePayAuthrorizationCompletion = completion
|
||||||
|
strongSelf.pay(liabilityNoticeAccepted: true, receivedCredentials: .generic(data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: false))
|
||||||
self.paymentAuthDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] token in
|
} else {
|
||||||
if let strongSelf = self {
|
completion(.failure)
|
||||||
strongSelf.applePayAuthrorizationCompletion = completion
|
}
|
||||||
strongSelf.pay(liabilityNoticeAccepted: true, receivedCredentials: .generic(data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: false))
|
}, error: { _ in
|
||||||
} else {
|
|
||||||
completion(.failure)
|
completion(.failure)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
self.applePayAuthrorizationCompletion = completion
|
||||||
|
guard let paymentString = String(data: payment.token.paymentData, encoding: .utf8) else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}, error: { _ in
|
self.pay(liabilityNoticeAccepted: true, receivedCredentials: .applePay(data: paymentString))
|
||||||
completion(.failure)
|
}
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
|
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
|
||||||
|
|||||||
@ -214,7 +214,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
|
|||||||
foundMembers = .single([])
|
foundMembers = .single([])
|
||||||
}
|
}
|
||||||
|
|
||||||
let foundContacts: Signal<[Peer], NoError>
|
let foundContacts: Signal<([Peer], [PeerId: PeerPresence]), NoError>
|
||||||
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError>
|
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError>
|
||||||
switch mode {
|
switch mode {
|
||||||
case .inviteActions, .banAndPromoteActions:
|
case .inviteActions, .banAndPromoteActions:
|
||||||
@ -222,7 +222,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
|
|||||||
foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query)
|
foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query)
|
||||||
|> delay(0.2, queue: Queue.concurrentDefaultQueue()))
|
|> delay(0.2, queue: Queue.concurrentDefaultQueue()))
|
||||||
case .searchMembers, .searchBanned, .searchAdmins:
|
case .searchMembers, .searchBanned, .searchAdmins:
|
||||||
foundContacts = .single([])
|
foundContacts = .single(([], [:]))
|
||||||
foundRemotePeers = .single(([], []))
|
foundRemotePeers = .single(([], []))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,7 +322,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for peer in foundContacts {
|
for peer in foundContacts.0 {
|
||||||
if !existingPeerIds.contains(peer.id) {
|
if !existingPeerIds.contains(peer.id) {
|
||||||
existingPeerIds.insert(peer.id)
|
existingPeerIds.insert(peer.id)
|
||||||
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts))
|
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts))
|
||||||
|
|||||||
@ -629,7 +629,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
|
|||||||
if strongSelf.resolvePeerByNameDisposable == nil {
|
if strongSelf.resolvePeerByNameDisposable == nil {
|
||||||
strongSelf.resolvePeerByNameDisposable = MetaDisposable()
|
strongSelf.resolvePeerByNameDisposable = MetaDisposable()
|
||||||
}
|
}
|
||||||
let resolveSignal: Signal<Peer?, NoError>
|
var resolveSignal: Signal<Peer?, NoError>
|
||||||
if let peerName = peerName {
|
if let peerName = peerName {
|
||||||
resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName)
|
resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName)
|
||||||
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
||||||
@ -646,6 +646,32 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
|
|||||||
} else {
|
} else {
|
||||||
resolveSignal = .single(nil)
|
resolveSignal = .single(nil)
|
||||||
}
|
}
|
||||||
|
var cancelImpl: (() -> Void)?
|
||||||
|
let presentationData = strongSelf.presentationData
|
||||||
|
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||||
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||||
|
cancelImpl?()
|
||||||
|
}))
|
||||||
|
self?.present(controller, in: .window(.root))
|
||||||
|
return ActionDisposable { [weak controller] in
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|> delay(0.15, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = progressSignal.start()
|
||||||
|
|
||||||
|
resolveSignal = resolveSignal
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelImpl = {
|
||||||
|
self?.resolvePeerByNameDisposable?.set(nil)
|
||||||
|
}
|
||||||
strongSelf.resolvePeerByNameDisposable?.set((resolveSignal
|
strongSelf.resolvePeerByNameDisposable?.set((resolveSignal
|
||||||
|> deliverOnMainQueue).start(next: { peer in
|
|> deliverOnMainQueue).start(next: { peer in
|
||||||
if let strongSelf = self, !hashtag.isEmpty {
|
if let strongSelf = self, !hashtag.isEmpty {
|
||||||
@ -994,77 +1020,85 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
|
|||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
if case let .peer(peerView) = self.chatLocationInfoData {
|
if case let .peer(peerView) = self.chatLocationInfoData {
|
||||||
peerView.set(account.viewTracker.peerView(peerId))
|
peerView.set(account.viewTracker.peerView(peerId))
|
||||||
self.peerDisposable.set((peerView.get()
|
var onlineMemberCount: Signal<Int32?, NoError> = .single(nil)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] peerView in
|
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||||
if let strongSelf = self {
|
onlineMemberCount = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recentOnline(postbox: account.postbox, network: account.network, peerId: peerId)
|
||||||
if let peer = peerViewMainPeer(peerView) {
|
|> map(Optional.init)
|
||||||
strongSelf.chatTitleView?.titleContent = .peer(peerView)
|
}
|
||||||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
|
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount)
|
||||||
}
|
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount in
|
||||||
var wasGroupChannel: Bool?
|
if let strongSelf = self {
|
||||||
if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info {
|
if let peer = peerViewMainPeer(peerView) {
|
||||||
if case .group = info {
|
strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount)
|
||||||
wasGroupChannel = true
|
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
|
||||||
} else {
|
}
|
||||||
wasGroupChannel = false
|
if strongSelf.peerView === peerView {
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
var isGroupChannel: Bool?
|
var wasGroupChannel: Bool?
|
||||||
if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info {
|
if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info {
|
||||||
if case .group = info {
|
if case .group = info {
|
||||||
isGroupChannel = true
|
wasGroupChannel = true
|
||||||
} else {
|
} else {
|
||||||
isGroupChannel = false
|
wasGroupChannel = false
|
||||||
}
|
|
||||||
}
|
|
||||||
strongSelf.peerView = peerView
|
|
||||||
if wasGroupChannel != isGroupChannel {
|
|
||||||
if let isGroupChannel = isGroupChannel, isGroupChannel {
|
|
||||||
let (recentDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in })
|
|
||||||
let (adminsDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in })
|
|
||||||
let disposable = DisposableSet()
|
|
||||||
disposable.add(recentDisposable)
|
|
||||||
disposable.add(adminsDisposable)
|
|
||||||
strongSelf.chatAdditionalDataDisposable.set(disposable)
|
|
||||||
} else {
|
|
||||||
strongSelf.chatAdditionalDataDisposable.set(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strongSelf.isNodeLoaded {
|
|
||||||
strongSelf.chatDisplayNode.peerView = peerView
|
|
||||||
}
|
|
||||||
var peerIsMuted = false
|
|
||||||
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
|
|
||||||
if case .muted = notificationSettings.muteState {
|
|
||||||
peerIsMuted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var renderedPeer: RenderedPeer?
|
|
||||||
var isContact: Bool = false
|
|
||||||
if let peer = peerView.peers[peerView.peerId] {
|
|
||||||
isContact = peerView.peerIsContact
|
|
||||||
var peers = SimpleDictionary<PeerId, Peer>()
|
|
||||||
peers[peer.id] = peer
|
|
||||||
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
|
|
||||||
peers[associatedPeer.id] = associatedPeer
|
|
||||||
}
|
|
||||||
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers)
|
|
||||||
}
|
|
||||||
|
|
||||||
var animated = false
|
|
||||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState {
|
|
||||||
animated = true
|
|
||||||
}
|
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
|
|
||||||
return $0.updatedPeer { _ in return renderedPeer
|
|
||||||
}.updatedIsContact(isContact).updatedPeerIsMuted(peerIsMuted)
|
|
||||||
})
|
|
||||||
if !strongSelf.didSetChatLocationInfoReady {
|
|
||||||
strongSelf.didSetChatLocationInfoReady = true
|
|
||||||
strongSelf._chatLocationInfoReady.set(.single(true))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
var isGroupChannel: Bool?
|
||||||
|
if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info {
|
||||||
|
if case .group = info {
|
||||||
|
isGroupChannel = true
|
||||||
|
} else {
|
||||||
|
isGroupChannel = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strongSelf.peerView = peerView
|
||||||
|
if wasGroupChannel != isGroupChannel {
|
||||||
|
if let isGroupChannel = isGroupChannel, isGroupChannel {
|
||||||
|
let (recentDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in })
|
||||||
|
let (adminsDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in })
|
||||||
|
let disposable = DisposableSet()
|
||||||
|
disposable.add(recentDisposable)
|
||||||
|
disposable.add(adminsDisposable)
|
||||||
|
strongSelf.chatAdditionalDataDisposable.set(disposable)
|
||||||
|
} else {
|
||||||
|
strongSelf.chatAdditionalDataDisposable.set(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strongSelf.isNodeLoaded {
|
||||||
|
strongSelf.chatDisplayNode.peerView = peerView
|
||||||
|
}
|
||||||
|
var peerIsMuted = false
|
||||||
|
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
|
||||||
|
if case .muted = notificationSettings.muteState {
|
||||||
|
peerIsMuted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var renderedPeer: RenderedPeer?
|
||||||
|
var isContact: Bool = false
|
||||||
|
if let peer = peerView.peers[peerView.peerId] {
|
||||||
|
isContact = peerView.peerIsContact
|
||||||
|
var peers = SimpleDictionary<PeerId, Peer>()
|
||||||
|
peers[peer.id] = peer
|
||||||
|
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
|
||||||
|
peers[associatedPeer.id] = associatedPeer
|
||||||
|
}
|
||||||
|
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers)
|
||||||
|
}
|
||||||
|
|
||||||
|
var animated = false
|
||||||
|
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState {
|
||||||
|
animated = true
|
||||||
|
}
|
||||||
|
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
|
||||||
|
return $0.updatedPeer { _ in return renderedPeer
|
||||||
|
}.updatedIsContact(isContact).updatedPeerIsMuted(peerIsMuted)
|
||||||
|
})
|
||||||
|
if !strongSelf.didSetChatLocationInfoReady {
|
||||||
|
strongSelf.didSetChatLocationInfoReady = true
|
||||||
|
strongSelf._chatLocationInfoReady.set(.single(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
case let .group(groupId):
|
case let .group(groupId):
|
||||||
if case let .group(topPeersView) = self.chatLocationInfoData {
|
if case let .group(topPeersView) = self.chatLocationInfoData {
|
||||||
@ -4305,7 +4339,35 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
|
|||||||
disposable = MetaDisposable()
|
disposable = MetaDisposable()
|
||||||
self.resolvePeerByNameDisposable = disposable
|
self.resolvePeerByNameDisposable = disposable
|
||||||
}
|
}
|
||||||
disposable.set((resolvePeerByName(account: self.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in
|
var resolveSignal = resolvePeerByName(account: self.account, name: name, ageLimit: 10)
|
||||||
|
|
||||||
|
var cancelImpl: (() -> Void)?
|
||||||
|
let presentationData = self.presentationData
|
||||||
|
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||||
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||||
|
cancelImpl?()
|
||||||
|
}))
|
||||||
|
self?.present(controller, in: .window(.root))
|
||||||
|
return ActionDisposable { [weak controller] in
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|> delay(0.15, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = progressSignal.start()
|
||||||
|
|
||||||
|
resolveSignal = resolveSignal
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelImpl = { [weak self] in
|
||||||
|
self?.resolvePeerByNameDisposable?.set(nil)
|
||||||
|
}
|
||||||
|
disposable.set((resolveSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.openResolved(.peer(peerId, .default))
|
strongSelf.openResolved(.peer(peerId, .default))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -348,6 +348,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
self.panRecognizer = recognizer
|
self.panRecognizer = recognizer
|
||||||
self.view.addGestureRecognizer(recognizer)
|
self.view.addGestureRecognizer(recognizer)
|
||||||
|
|
||||||
|
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if let _ = strongSelf.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateIsEmpty(_ isEmpty: Bool, animated: Bool) {
|
private func updateIsEmpty(_ isEmpty: Bool, animated: Bool) {
|
||||||
@ -1377,6 +1387,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func dismissInput() {
|
func dismissInput() {
|
||||||
|
if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch self.chatPresentationInterfaceState.inputMode {
|
switch self.chatPresentationInterfaceState.inputMode {
|
||||||
case .none:
|
case .none:
|
||||||
break
|
break
|
||||||
|
|||||||
@ -383,8 +383,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
|
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false)
|
||||||
surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false)
|
surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.025, removeOnCompletion: false)
|
||||||
|
|
||||||
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||||
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
|
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
|
||||||
@ -404,7 +404,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
intermediateCompletion()
|
intermediateCompletion()
|
||||||
})
|
})
|
||||||
|
|
||||||
self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false)
|
||||||
|
|
||||||
transformedFrame.origin = CGPoint()
|
transformedFrame.origin = CGPoint()
|
||||||
self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||||
|
|||||||
@ -372,8 +372,9 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
|
|||||||
}
|
}
|
||||||
})*/
|
})*/
|
||||||
|
|
||||||
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated)
|
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated, completion: { [weak self] in
|
||||||
strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
|
self?.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,14 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if let item = strongSelf.item {
|
||||||
|
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
private var iconNode: TransformImageNode?
|
private var iconNode: TransformImageNode?
|
||||||
private var statusNode: RadialStatusNode?
|
private var statusNode: RadialStatusNode?
|
||||||
|
private var streamingStatusNode: RadialStatusNode?
|
||||||
private var tapRecognizer: UITapGestureRecognizer?
|
private var tapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
private let statusDisposable = MetaDisposable()
|
private let statusDisposable = MetaDisposable()
|
||||||
@ -35,11 +36,16 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
private let fetchDisposable = MetaDisposable()
|
private let fetchDisposable = MetaDisposable()
|
||||||
|
|
||||||
var activateLocalContent: () -> Void = { }
|
var activateLocalContent: () -> Void = { }
|
||||||
|
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
private var account: Account?
|
private var account: Account?
|
||||||
private var message: Message?
|
private var message: Message?
|
||||||
private var themeAndStrings: (ChatPresentationThemeData, PresentationStrings)?
|
private var themeAndStrings: (ChatPresentationThemeData, PresentationStrings)?
|
||||||
private var file: TelegramMediaFile?
|
private var file: TelegramMediaFile?
|
||||||
|
private var progressFrame: CGRect?
|
||||||
|
private var streamingCacheStatusFrame: CGRect?
|
||||||
|
private var fileIconImage: UIImage?
|
||||||
|
private var cloudFetchIconImage: UIImage?
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.titleNode = TextNode()
|
self.titleNode = TextNode()
|
||||||
@ -77,9 +83,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
self.tapRecognizer = tapRecognizer
|
self.tapRecognizer = tapRecognizer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func cacheProgressPressed() {
|
||||||
|
guard let resourceStatus = self.resourceStatus else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch resourceStatus.fetchStatus {
|
||||||
|
case .Fetching:
|
||||||
|
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
case .Remote:
|
||||||
|
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
|
||||||
|
fetch()
|
||||||
|
}
|
||||||
|
case .Local:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func progressPressed() {
|
@objc func progressPressed() {
|
||||||
if let resourceStatus = self.resourceStatus {
|
if let resourceStatus = self.resourceStatus {
|
||||||
switch resourceStatus {
|
switch resourceStatus.mediaStatus {
|
||||||
case let .fetchStatus(fetchStatus):
|
case let .fetchStatus(fetchStatus):
|
||||||
if let account = self.account, let message = self.message, message.flags.isSending {
|
if let account = self.account, let message = self.message, message.flags.isSending {
|
||||||
let _ = account.postbox.transaction({ transaction -> Void in
|
let _ = account.postbox.transaction({ transaction -> Void in
|
||||||
@ -109,7 +133,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
@objc func fileTap(_ recognizer: UITapGestureRecognizer) {
|
@objc func fileTap(_ recognizer: UITapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state {
|
if case .ended = recognizer.state {
|
||||||
self.progressPressed()
|
if let streamingCacheStatusFrame = self.streamingCacheStatusFrame, streamingCacheStatusFrame.contains(recognizer.location(in: self.view)) {
|
||||||
|
self.cacheProgressPressed()
|
||||||
|
} else {
|
||||||
|
self.progressPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +150,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
let currentMessage = self.message
|
let currentMessage = self.message
|
||||||
let currentTheme = self.themeAndStrings?.0
|
let currentTheme = self.themeAndStrings?.0
|
||||||
|
let currentResourceStatus = self.resourceStatus
|
||||||
|
|
||||||
return { account, presentationData, message, file, automaticDownload, incoming, isRecentActions, dateAndStatusType, constrainedSize in
|
return { account, presentationData, message, file, automaticDownload, incoming, isRecentActions, dateAndStatusType, constrainedSize in
|
||||||
var updatedTheme: ChatPresentationThemeData?
|
var updatedTheme: ChatPresentationThemeData?
|
||||||
@ -224,13 +253,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
if case let .Audio(voice, duration, title, performer, waveform) = attribute {
|
if case let .Audio(voice, duration, title, performer, waveform) = attribute {
|
||||||
isAudio = true
|
isAudio = true
|
||||||
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
||||||
updatedStatusSignal = currentUpdatedStatusSignal |> map { status in
|
updatedStatusSignal = currentUpdatedStatusSignal
|
||||||
switch status {
|
|> map { status in
|
||||||
|
switch status.mediaStatus {
|
||||||
case let .fetchStatus(fetchStatus):
|
case let .fetchStatus(fetchStatus):
|
||||||
if !voice {
|
if !voice {
|
||||||
return .fetchStatus(.Local)
|
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
|
||||||
} else {
|
} else {
|
||||||
return .fetchStatus(fetchStatus)
|
return FileMediaResourceStatus(mediaStatus: .fetchStatus(fetchStatus), fetchStatus: status.fetchStatus)
|
||||||
}
|
}
|
||||||
case .playbackStatus:
|
case .playbackStatus:
|
||||||
return status
|
return status
|
||||||
@ -289,6 +319,23 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
textConstrainedSize.width -= 80.0
|
textConstrainedSize.width -= 80.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let streamingProgressDiameter: CGFloat = 28.0
|
||||||
|
var hasStreamingProgress = false
|
||||||
|
if isAudio && !isVoice {
|
||||||
|
if let resourceStatus = currentResourceStatus {
|
||||||
|
switch resourceStatus.fetchStatus {
|
||||||
|
case .Fetching, .Remote:
|
||||||
|
hasStreamingProgress = true
|
||||||
|
case .Local:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasStreamingProgress {
|
||||||
|
textConstrainedSize.width -= streamingProgressDiameter + 4.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
@ -311,6 +358,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
minLayoutWidth = max(minLayoutWidth, statusSize.width)
|
minLayoutWidth = max(minLayoutWidth, statusSize.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cloudFetchIconImage: UIImage?
|
||||||
|
if hasStreamingProgress {
|
||||||
|
minLayoutWidth += streamingProgressDiameter + 4.0
|
||||||
|
cloudFetchIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchOutgoingIcon(presentationData.theme.theme)
|
||||||
|
}
|
||||||
|
|
||||||
let fileIconImage: UIImage?
|
let fileIconImage: UIImage?
|
||||||
if hasThumbnail {
|
if hasThumbnail {
|
||||||
fileIconImage = nil
|
fileIconImage = nil
|
||||||
@ -325,6 +378,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
var iconFrame: CGRect?
|
var iconFrame: CGRect?
|
||||||
let progressFrame: CGRect
|
let progressFrame: CGRect
|
||||||
|
let streamingCacheStatusFrame: CGRect
|
||||||
let controlAreaWidth: CGFloat
|
let controlAreaWidth: CGFloat
|
||||||
|
|
||||||
if hasThumbnail {
|
if hasThumbnail {
|
||||||
@ -362,7 +416,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0)
|
fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0)
|
||||||
} else {
|
} else {
|
||||||
let unionSize = titleFrame.union(descriptionFrame).union(progressFrame).size
|
let unionSize = titleFrame.union(descriptionFrame).union(progressFrame).size
|
||||||
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 4.0)
|
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 6.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusFrame: CGRect?
|
var statusFrame: CGRect?
|
||||||
@ -376,6 +430,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0)
|
statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isAudio && !isVoice {
|
||||||
|
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: fittedLayoutSize.width + 6.0, y: 4.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
|
||||||
|
if hasStreamingProgress {
|
||||||
|
fittedLayoutSize.width += streamingProgressDiameter + 6.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
streamingCacheStatusFrame = CGRect()
|
||||||
|
}
|
||||||
|
|
||||||
return (fittedLayoutSize, { [weak self] in
|
return (fittedLayoutSize, { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.account = account
|
strongSelf.account = account
|
||||||
@ -468,78 +531,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
|
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
|
||||||
displayLinkDispatcher.dispatch {
|
displayLinkDispatcher.dispatch {
|
||||||
if let strongSelf = strongSelf {
|
if let strongSelf = strongSelf {
|
||||||
|
var previousHadCacheStatus = false
|
||||||
|
if let resourceStatus = strongSelf.resourceStatus {
|
||||||
|
switch resourceStatus.fetchStatus {
|
||||||
|
case .Fetching, .Remote:
|
||||||
|
previousHadCacheStatus = true
|
||||||
|
case .Local:
|
||||||
|
previousHadCacheStatus = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var hasCacheStatus = false
|
||||||
|
switch status.fetchStatus {
|
||||||
|
case .Fetching, .Remote:
|
||||||
|
hasCacheStatus = true
|
||||||
|
case .Local:
|
||||||
|
hasCacheStatus = false
|
||||||
|
}
|
||||||
strongSelf.resourceStatus = status
|
strongSelf.resourceStatus = status
|
||||||
|
if isAudio && !isVoice && previousHadCacheStatus != hasCacheStatus {
|
||||||
if strongSelf.statusNode == nil {
|
strongSelf.requestUpdateLayout(false)
|
||||||
let backgroundNodeColor: UIColor
|
|
||||||
if strongSelf.iconNode != nil {
|
|
||||||
backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor
|
|
||||||
} else if incoming {
|
|
||||||
backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor
|
|
||||||
} else {
|
|
||||||
backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor
|
|
||||||
}
|
|
||||||
let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor)
|
|
||||||
strongSelf.statusNode = statusNode
|
|
||||||
statusNode.frame = progressFrame
|
|
||||||
strongSelf.addSubnode(statusNode)
|
|
||||||
} else if let _ = updatedTheme {
|
|
||||||
//strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage))
|
|
||||||
}
|
|
||||||
|
|
||||||
let state: RadialStatusNodeState
|
|
||||||
let statusForegroundColor: UIColor
|
|
||||||
if strongSelf.iconNode != nil {
|
|
||||||
statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor
|
|
||||||
} else if incoming {
|
|
||||||
statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.incoming.withoutWallpaper.fill : bubbleTheme.incoming.withWallpaper.fill
|
|
||||||
} else {
|
} else {
|
||||||
statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill
|
strongSelf.updateStatus()
|
||||||
}
|
|
||||||
switch status {
|
|
||||||
case let .fetchStatus(fetchStatus):
|
|
||||||
strongSelf.waveformScrubbingNode?.enableScrubbing = false
|
|
||||||
switch fetchStatus {
|
|
||||||
case let .Fetching(isActive, progress):
|
|
||||||
var adjustedProgress = progress
|
|
||||||
if isActive {
|
|
||||||
adjustedProgress = max(adjustedProgress, 0.027)
|
|
||||||
}
|
|
||||||
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
|
|
||||||
case .Local:
|
|
||||||
if isAudio {
|
|
||||||
state = .play(statusForegroundColor)
|
|
||||||
} else if let fileIconImage = fileIconImage {
|
|
||||||
state = .customIcon(fileIconImage)
|
|
||||||
} else {
|
|
||||||
state = .none
|
|
||||||
}
|
|
||||||
case .Remote:
|
|
||||||
if isAudio && !isVoice {
|
|
||||||
state = .play(statusForegroundColor)
|
|
||||||
} else {
|
|
||||||
state = .download(statusForegroundColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case let .playbackStatus(playbackStatus):
|
|
||||||
strongSelf.waveformScrubbingNode?.enableScrubbing = true
|
|
||||||
switch playbackStatus {
|
|
||||||
case .playing:
|
|
||||||
state = .pause(statusForegroundColor)
|
|
||||||
case .paused:
|
|
||||||
state = .play(statusForegroundColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let statusNode = strongSelf.statusNode {
|
|
||||||
if state == .none {
|
|
||||||
strongSelf.statusNode = nil
|
|
||||||
}
|
|
||||||
statusNode.transitionToState(state, completion: { [weak statusNode] in
|
|
||||||
if state == .none {
|
|
||||||
statusNode?.removeFromSupernode()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -551,6 +563,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.statusNode?.frame = progressFrame
|
strongSelf.statusNode?.frame = progressFrame
|
||||||
|
strongSelf.progressFrame = progressFrame
|
||||||
|
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
||||||
|
strongSelf.fileIconImage = fileIconImage
|
||||||
|
strongSelf.cloudFetchIconImage = cloudFetchIconImage
|
||||||
|
|
||||||
if let updatedFetchControls = updatedFetchControls {
|
if let updatedFetchControls = updatedFetchControls {
|
||||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||||
@ -558,6 +574,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
updatedFetchControls.fetch()
|
updatedFetchControls.fetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.updateStatus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -565,6 +583,158 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateStatus() {
|
||||||
|
guard let resourceStatus = self.resourceStatus else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let message = self.message else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let account = self.account else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let presentationData = self.themeAndStrings?.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let progressFrame = self.progressFrame, let streamingCacheStatusFrame = self.streamingCacheStatusFrame else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let file = self.file else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let incoming = message.effectivelyIncoming(account.peerId)
|
||||||
|
let bubbleTheme = presentationData.theme.chat.bubble
|
||||||
|
|
||||||
|
var isAudio = false
|
||||||
|
var isVoice = false
|
||||||
|
for attribute in file.attributes {
|
||||||
|
if case let .Audio(voice, _, _, _, _) = attribute {
|
||||||
|
isAudio = true
|
||||||
|
|
||||||
|
if voice {
|
||||||
|
isVoice = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state: RadialStatusNodeState
|
||||||
|
var streamingState: RadialStatusNodeState = .none
|
||||||
|
|
||||||
|
if isAudio && !isVoice {
|
||||||
|
let streamingStatusForegroundColor: UIColor = incoming ? bubbleTheme.incomingAccentControlColor : bubbleTheme.outgoingAccentControlColor
|
||||||
|
let streamingStatusBackgroundColor: UIColor = incoming ? bubbleTheme.incomingMediaInactiveControlColor : bubbleTheme.outgoingMediaInactiveControlColor
|
||||||
|
switch resourceStatus.fetchStatus {
|
||||||
|
case let .Fetching(isActive, progress):
|
||||||
|
var adjustedProgress = progress
|
||||||
|
if isActive {
|
||||||
|
adjustedProgress = max(adjustedProgress, 0.027)
|
||||||
|
}
|
||||||
|
streamingState = .cloudProgress(color: streamingStatusForegroundColor, strokeBackgroundColor: streamingStatusBackgroundColor, lineWidth: 2.0, value: CGFloat(adjustedProgress))
|
||||||
|
case .Local:
|
||||||
|
streamingState = .none
|
||||||
|
case .Remote:
|
||||||
|
if let cloudFetchIconImage = self.cloudFetchIconImage {
|
||||||
|
streamingState = .customIcon(cloudFetchIconImage)
|
||||||
|
} else {
|
||||||
|
streamingState = .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
streamingState = .none
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusForegroundColor: UIColor
|
||||||
|
if self.iconNode != nil {
|
||||||
|
statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor
|
||||||
|
} else if incoming {
|
||||||
|
statusForegroundColor = presentationData.wallpaper.isEmpty ? bubbleTheme.incoming.withoutWallpaper.fill : bubbleTheme.incoming.withWallpaper.fill
|
||||||
|
} else {
|
||||||
|
statusForegroundColor = presentationData.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill
|
||||||
|
}
|
||||||
|
switch resourceStatus.mediaStatus {
|
||||||
|
case let .fetchStatus(fetchStatus):
|
||||||
|
self.waveformScrubbingNode?.enableScrubbing = false
|
||||||
|
switch fetchStatus {
|
||||||
|
case let .Fetching(isActive, progress):
|
||||||
|
var adjustedProgress = progress
|
||||||
|
if isActive {
|
||||||
|
adjustedProgress = max(adjustedProgress, 0.027)
|
||||||
|
}
|
||||||
|
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
|
||||||
|
case .Local:
|
||||||
|
if isAudio {
|
||||||
|
state = .play(statusForegroundColor)
|
||||||
|
} else if let fileIconImage = self.fileIconImage {
|
||||||
|
state = .customIcon(fileIconImage)
|
||||||
|
} else {
|
||||||
|
state = .none
|
||||||
|
}
|
||||||
|
case .Remote:
|
||||||
|
if isAudio && !isVoice {
|
||||||
|
state = .play(statusForegroundColor)
|
||||||
|
} else {
|
||||||
|
state = .download(statusForegroundColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .playbackStatus(playbackStatus):
|
||||||
|
self.waveformScrubbingNode?.enableScrubbing = true
|
||||||
|
switch playbackStatus {
|
||||||
|
case .playing:
|
||||||
|
state = .pause(statusForegroundColor)
|
||||||
|
case .paused:
|
||||||
|
state = .play(statusForegroundColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != .none && self.statusNode == nil {
|
||||||
|
let backgroundNodeColor: UIColor
|
||||||
|
if self.iconNode != nil {
|
||||||
|
backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor
|
||||||
|
} else if incoming {
|
||||||
|
backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor
|
||||||
|
} else {
|
||||||
|
backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor
|
||||||
|
}
|
||||||
|
let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor)
|
||||||
|
self.statusNode = statusNode
|
||||||
|
statusNode.frame = progressFrame
|
||||||
|
self.addSubnode(statusNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamingState != .none && self.streamingStatusNode == nil {
|
||||||
|
let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||||
|
self.streamingStatusNode = streamingStatusNode
|
||||||
|
streamingStatusNode.frame = streamingCacheStatusFrame
|
||||||
|
self.addSubnode(streamingStatusNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let statusNode = self.statusNode {
|
||||||
|
if state == .none {
|
||||||
|
self.statusNode = nil
|
||||||
|
}
|
||||||
|
statusNode.transitionToState(state, completion: { [weak statusNode] in
|
||||||
|
if state == .none {
|
||||||
|
statusNode?.removeFromSupernode()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let streamingStatusNode = self.streamingStatusNode {
|
||||||
|
if streamingState == .none {
|
||||||
|
self.streamingStatusNode = nil
|
||||||
|
streamingStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak streamingStatusNode] _ in
|
||||||
|
if streamingState == .none {
|
||||||
|
streamingStatusNode?.removeFromSupernode()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
streamingStatusNode.transitionToState(streamingState, completion: {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) {
|
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) {
|
||||||
let currentAsyncLayout = node?.asyncLayout()
|
let currentAsyncLayout = node?.asyncLayout()
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
private let infoBackgroundNode: ASImageNode
|
private let infoBackgroundNode: ASImageNode
|
||||||
private let muteIconNode: ASImageNode
|
private let muteIconNode: ASImageNode
|
||||||
|
|
||||||
private var status: FileMediaResourceStatus?
|
private var status: FileMediaResourceMediaStatus?
|
||||||
private let playbackStatusDisposable = MetaDisposable()
|
private let playbackStatusDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var shouldAcquireVideoContext: Bool {
|
private var shouldAcquireVideoContext: Bool {
|
||||||
@ -178,16 +178,16 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
var updatedPlaybackStatus: Signal<FileMediaResourceStatus, NoError>?
|
var updatedPlaybackStatus: Signal<FileMediaResourceStatus, NoError>?
|
||||||
if let updatedFile = updatedFile, updatedMedia {
|
if let updatedFile = updatedFile, updatedMedia {
|
||||||
updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message, isRecentActions: item.associatedData.isRecentActions), item.account.pendingMessageManager.pendingMessageStatus(item.message.id))
|
updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message, isRecentActions: item.associatedData.isRecentActions), item.account.pendingMessageManager.pendingMessageStatus(item.message.id))
|
||||||
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
||||||
if let pendingStatus = pendingStatus {
|
if let pendingStatus = pendingStatus {
|
||||||
var progress = pendingStatus.progress
|
var progress = pendingStatus.progress
|
||||||
if pendingStatus.isRunning {
|
if pendingStatus.isRunning {
|
||||||
progress = max(progress, 0.27)
|
progress = max(progress, 0.27)
|
||||||
}
|
|
||||||
return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress))
|
|
||||||
} else {
|
|
||||||
return resourceStatus
|
|
||||||
}
|
}
|
||||||
|
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress)), fetchStatus: resourceStatus.fetchStatus)
|
||||||
|
} else {
|
||||||
|
return resourceStatus
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,7 +280,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.status = status
|
strongSelf.status = status.mediaStatus
|
||||||
strongSelf.updateStatus()
|
strongSelf.updateStatus()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -238,6 +238,8 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto
|
|||||||
|
|
||||||
super.init(frame: CGRect())
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||||
|
|
||||||
let inputPanelTheme = theme.chat.inputPanel
|
let inputPanelTheme = theme.chat.inputPanel
|
||||||
|
|
||||||
self.pallete = TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor)
|
self.pallete = TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import SwiftSignalKit
|
|||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
|
|
||||||
enum ChatTitleContent {
|
enum ChatTitleContent {
|
||||||
case peer(PeerView)
|
case peer(peerView: PeerView, onlineMemberCount: Int32?)
|
||||||
case group([Peer])
|
case group([Peer])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,13 +184,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout) {
|
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) {
|
||||||
var isOnline = false
|
var isOnline = false
|
||||||
if case .online = networkState {
|
if case .online = networkState {
|
||||||
isOnline = true
|
isOnline = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if isOnline || layout.metrics.widthClass == .regular {
|
if isOnline || layout?.metrics.widthClass == .regular {
|
||||||
self.contentContainer.isHidden = false
|
self.contentContainer.isHidden = false
|
||||||
if let networkStatusNode = self.networkStatusNode {
|
if let networkStatusNode = self.networkStatusNode {
|
||||||
self.networkStatusNode = nil
|
self.networkStatusNode = nil
|
||||||
@ -210,7 +210,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
case .waitingForNetwork:
|
case .waitingForNetwork:
|
||||||
statusNode.title = self.strings.State_WaitingForNetwork
|
statusNode.title = self.strings.State_WaitingForNetwork
|
||||||
case let .connecting(proxy):
|
case let .connecting(proxy):
|
||||||
if proxy != nil && layout.size.width > 320.0 {
|
if let layout = layout, proxy != nil && layout.size.width > 320.0 {
|
||||||
statusNode.title = self.strings.State_ConnectingToProxy
|
statusNode.title = self.strings.State_ConnectingToProxy
|
||||||
} else {
|
} else {
|
||||||
statusNode.title = self.strings.State_Connecting
|
statusNode.title = self.strings.State_Connecting
|
||||||
@ -233,7 +233,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var layout: ContainerViewLayout = ContainerViewLayout()() {
|
var layout: ContainerViewLayout? {
|
||||||
didSet {
|
didSet {
|
||||||
if self.layout != oldValue {
|
if self.layout != oldValue {
|
||||||
updateNetworkStatusNode(networkState: self.networkState, layout: self.layout)
|
updateNetworkStatusNode(networkState: self.networkState, layout: self.layout)
|
||||||
@ -250,7 +250,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
var titleLeftIcon: ChatTitleIcon = .none
|
var titleLeftIcon: ChatTitleIcon = .none
|
||||||
var titleRightIcon: ChatTitleIcon = .none
|
var titleRightIcon: ChatTitleIcon = .none
|
||||||
switch titleContent {
|
switch titleContent {
|
||||||
case let .peer(peerView):
|
case let .peer(peerView, _):
|
||||||
if let peer = peerViewMainPeer(peerView) {
|
if let peer = peerViewMainPeer(peerView) {
|
||||||
if peerView.peerId == self.account.peerId {
|
if peerView.peerId == self.account.peerId {
|
||||||
string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||||
@ -306,7 +306,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
var shouldUpdateLayout = false
|
var shouldUpdateLayout = false
|
||||||
if let titleContent = self.titleContent {
|
if let titleContent = self.titleContent {
|
||||||
switch titleContent {
|
switch titleContent {
|
||||||
case let .peer(peerView):
|
case let .peer(peerView, onlineMemberCount):
|
||||||
if let peer = peerViewMainPeer(peerView) {
|
if let peer = peerViewMainPeer(peerView) {
|
||||||
if peer.id == self.account.peerId {
|
if peer.id == self.account.peerId {
|
||||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||||
@ -383,17 +383,27 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
}
|
}
|
||||||
} else if let channel = peer as? TelegramChannel {
|
} else if let channel = peer as? TelegramChannel {
|
||||||
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
|
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
|
||||||
let membersString: String
|
if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 {
|
||||||
if case .group = channel.info {
|
let string = NSMutableAttributedString()
|
||||||
membersString = strings.Conversation_StatusMembers(memberCount)
|
|
||||||
|
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
|
||||||
|
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
|
||||||
|
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
|
||||||
|
self.infoNode.attributedText = string
|
||||||
|
shouldUpdateLayout = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
membersString = strings.Conversation_StatusSubscribers(memberCount)
|
let membersString: String
|
||||||
}
|
if case .group = channel.info {
|
||||||
|
membersString = strings.Conversation_StatusMembers(memberCount)
|
||||||
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
} else {
|
||||||
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
|
membersString = strings.Conversation_StatusSubscribers(memberCount)
|
||||||
self.infoNode.attributedText = string
|
}
|
||||||
shouldUpdateLayout = true
|
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||||
|
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
|
||||||
|
self.infoNode.attributedText = string
|
||||||
|
shouldUpdateLayout = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch channel.info {
|
switch channel.info {
|
||||||
|
|||||||
@ -620,7 +620,7 @@ final class ContactListNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get())
|
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get())
|
||||||
|> mapToQueue { localPeers, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal<ContactsListNodeTransition, NoError> in
|
|> mapToQueue { localPeersAndStatuses, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal<ContactsListNodeTransition, NoError> in
|
||||||
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
|
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
|
||||||
var existingPeerIds = Set<PeerId>()
|
var existingPeerIds = Set<PeerId>()
|
||||||
var disabledPeerIds = Set<PeerId>()
|
var disabledPeerIds = Set<PeerId>()
|
||||||
@ -628,17 +628,17 @@ final class ContactListNode: ASDisplayNode {
|
|||||||
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
||||||
for filter in filters {
|
for filter in filters {
|
||||||
switch filter {
|
switch filter {
|
||||||
case .excludeSelf:
|
case .excludeSelf:
|
||||||
existingPeerIds.insert(account.peerId)
|
existingPeerIds.insert(account.peerId)
|
||||||
case let .exclude(peerIds):
|
case let .exclude(peerIds):
|
||||||
existingPeerIds = existingPeerIds.union(peerIds)
|
existingPeerIds = existingPeerIds.union(peerIds)
|
||||||
case let .disable(peerIds):
|
case let .disable(peerIds):
|
||||||
disabledPeerIds = disabledPeerIds.union(peerIds)
|
disabledPeerIds = disabledPeerIds.union(peerIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var peers: [ContactListPeer] = []
|
var peers: [ContactListPeer] = []
|
||||||
for peer in localPeers {
|
for peer in localPeersAndStatuses.0 {
|
||||||
if !existingPeerIds.contains(peer.id) {
|
if !existingPeerIds.contains(peer.id) {
|
||||||
existingPeerIds.insert(peer.id)
|
existingPeerIds.insert(peer.id)
|
||||||
peers.append(.peer(peer: peer, isGlobal: false))
|
peers.append(.peer(peer: peer, isGlobal: false))
|
||||||
@ -680,7 +680,7 @@ final class ContactListNode: ASDisplayNode {
|
|||||||
peers.append(.deviceContact(stableId, contact))
|
peers.append(.deviceContact(stableId, contact))
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: [:], presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds)
|
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds)
|
||||||
let previous = previousEntries.swap(entries)
|
let previous = previousEntries.swap(entries)
|
||||||
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false))
|
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ private enum ContactListSearchGroup {
|
|||||||
private struct ContactListSearchEntry: Identifiable, Comparable {
|
private struct ContactListSearchEntry: Identifiable, Comparable {
|
||||||
let index: Int
|
let index: Int
|
||||||
let peer: ContactListPeer
|
let peer: ContactListPeer
|
||||||
|
let presence: PeerPresence?
|
||||||
let group: ContactListSearchGroup
|
let group: ContactListSearchGroup
|
||||||
let enabled: Bool
|
let enabled: Bool
|
||||||
|
|
||||||
@ -28,6 +29,13 @@ private struct ContactListSearchEntry: Identifiable, Comparable {
|
|||||||
if lhs.peer != rhs.peer {
|
if lhs.peer != rhs.peer {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence {
|
||||||
|
if !lhsPresence.isEqual(to: rhsPresence) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (lhs.presence != nil) != (rhs.presence != nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.group != rhs.group {
|
if lhs.group != rhs.group {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -41,13 +49,17 @@ private struct ContactListSearchEntry: Identifiable, Comparable {
|
|||||||
return lhs.index < rhs.index
|
return lhs.index < rhs.index
|
||||||
}
|
}
|
||||||
|
|
||||||
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, openPeer: @escaping (ContactListPeer) -> Void) -> ListViewItem {
|
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void) -> ListViewItem {
|
||||||
let header: ListViewItemHeader
|
let header: ListViewItemHeader
|
||||||
let status: ContactsPeerItemStatus
|
let status: ContactsPeerItemStatus
|
||||||
switch self.group {
|
switch self.group {
|
||||||
case .contacts:
|
case .contacts:
|
||||||
header = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil)
|
header = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil)
|
||||||
status = .none
|
if let presence = self.presence {
|
||||||
|
status = .presence(presence, timeFormat)
|
||||||
|
} else {
|
||||||
|
status = .none
|
||||||
|
}
|
||||||
case .global:
|
case .global:
|
||||||
header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil)
|
header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil)
|
||||||
if case let .peer(peer, _) = self.peer, let _ = peer.addressName {
|
if case let .peer(peer, _) = self.peer, let _ = peer.addressName {
|
||||||
@ -80,12 +92,12 @@ struct ContactListSearchContainerTransition {
|
|||||||
let isSearching: Bool
|
let isSearching: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, openPeer: @escaping (ContactListPeer) -> Void) -> ContactListSearchContainerTransition {
|
private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void) -> ContactListSearchContainerTransition {
|
||||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||||
|
|
||||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, openPeer: openPeer), directionHint: nil) }
|
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer), directionHint: nil) }
|
||||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, openPeer: openPeer), directionHint: nil) }
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer), directionHint: nil) }
|
||||||
|
|
||||||
return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
|
return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
|
||||||
}
|
}
|
||||||
@ -170,7 +182,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
|||||||
|
|
||||||
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, themeAndStringsPromise.get())
|
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, themeAndStringsPromise.get())
|
||||||
|> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
|> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||||
|> map { localPeers, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in
|
|> map { localPeersAndPresences, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in
|
||||||
var entries: [ContactListSearchEntry] = []
|
var entries: [ContactListSearchEntry] = []
|
||||||
var existingPeerIds = Set<PeerId>()
|
var existingPeerIds = Set<PeerId>()
|
||||||
var disabledPeerIds = Set<PeerId>()
|
var disabledPeerIds = Set<PeerId>()
|
||||||
@ -186,7 +198,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
|||||||
}
|
}
|
||||||
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
||||||
var index = 0
|
var index = 0
|
||||||
for peer in localPeers {
|
for peer in localPeersAndPresences.0 {
|
||||||
if existingPeerIds.contains(peer.id) {
|
if existingPeerIds.contains(peer.id) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -195,7 +207,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
|||||||
if onlyWriteable {
|
if onlyWriteable {
|
||||||
enabled = canSendMessagesToPeer(peer)
|
enabled = canSendMessagesToPeer(peer)
|
||||||
}
|
}
|
||||||
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer, isGlobal: false), group: .contacts, enabled: enabled))
|
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer, isGlobal: false), presence: localPeersAndPresences.1[peer.id], group: .contacts, enabled: enabled))
|
||||||
if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone {
|
if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone {
|
||||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||||
}
|
}
|
||||||
@ -214,7 +226,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
|||||||
enabled = canSendMessagesToPeer(peer.peer)
|
enabled = canSendMessagesToPeer(peer.peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), group: .global, enabled: enabled))
|
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), presence: nil, group: .global, enabled: enabled))
|
||||||
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
||||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||||
}
|
}
|
||||||
@ -233,7 +245,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
|||||||
enabled = canSendMessagesToPeer(peer.peer)
|
enabled = canSendMessagesToPeer(peer.peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), group: .global, enabled: enabled))
|
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), presence: nil, group: .global, enabled: enabled))
|
||||||
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
||||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||||
}
|
}
|
||||||
@ -249,7 +261,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
|||||||
continue outer
|
continue outer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries.append(ContactListSearchEntry(index: index, peer: .deviceContact(stableId, contact), group: .deviceContacts, enabled: true))
|
entries.append(ContactListSearchEntry(index: index, peer: .deviceContact(stableId, contact), presence: nil, group: .deviceContacts, enabled: true))
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -263,17 +275,17 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
|||||||
let previousSearchItems = Atomic<[ContactListSearchEntry]>(value: [])
|
let previousSearchItems = Atomic<[ContactListSearchEntry]>(value: [])
|
||||||
|
|
||||||
self.searchDisposable.set((searchItems
|
self.searchDisposable.set((searchItems
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let previousItems = previousSearchItems.swap(items ?? [])
|
let previousItems = previousSearchItems.swap(items ?? [])
|
||||||
|
|
||||||
let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, openPeer: { peer in self?.listNode.clearHighlightAnimated(true)
|
let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, openPeer: { peer in self?.listNode.clearHighlightAnimated(true)
|
||||||
self?.openPeer(peer)
|
self?.openPeer(peer)
|
||||||
})
|
})
|
||||||
|
|
||||||
strongSelf.enqueueTransition(transition)
|
strongSelf.enqueueTransition(transition)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.listNode.beganInteractiveDragging = { [weak self] in
|
self.listNode.beganInteractiveDragging = { [weak self] in
|
||||||
self?.dismissInput?()
|
self?.dismissInput?()
|
||||||
|
|||||||
@ -172,7 +172,7 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe
|
|||||||
context.requestedCompleteFetch = false
|
context.requestedCompleteFetch = false
|
||||||
} else {
|
} else {
|
||||||
if streamable {
|
if streamable {
|
||||||
context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: context.readingOffset ..< resourceSize, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start())
|
context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: context.readingOffset ..< Int(Int32.max), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start())
|
||||||
} else if !context.requestedCompleteFetch && context.fetchAutomatically {
|
} else if !context.requestedCompleteFetch && context.fetchAutomatically {
|
||||||
context.requestedCompleteFetch = true
|
context.requestedCompleteFetch = true
|
||||||
context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start())
|
context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start())
|
||||||
@ -236,7 +236,7 @@ final class FFMpegMediaFrameSourceContext: NSObject {
|
|||||||
let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1)
|
let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1)
|
||||||
|
|
||||||
if streamable {
|
if streamable {
|
||||||
self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: 0 ..< resourceSize, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
|
self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: 0 ..< Int(Int32.max), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
|
||||||
} else if !self.requestedCompleteFetch && self.fetchAutomatically {
|
} else if !self.requestedCompleteFetch && self.fetchAutomatically {
|
||||||
self.requestedCompleteFetch = true
|
self.requestedCompleteFetch = true
|
||||||
self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
|
self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
|
||||||
@ -308,6 +308,26 @@ final class FFMpegMediaFrameSourceContext: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if codecPar.pointee.codec_id == AV_CODEC_ID_MPEG4 {
|
||||||
|
if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromMpeg4CodecData(UInt32(kCMVideoCodecType_MPEG4Video), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size) {
|
||||||
|
let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000))
|
||||||
|
|
||||||
|
let duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale)
|
||||||
|
|
||||||
|
var rotationAngle: Double = 0.0
|
||||||
|
if let rotationInfo = av_dict_get(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.metadata, "rotate", nil, 0), let value = rotationInfo.pointee.value {
|
||||||
|
if strcmp(value, "0") != 0 {
|
||||||
|
if let angle = Double(String(cString: value)) {
|
||||||
|
rotationAngle = angle * Double.pi / 180.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let aspect = Double(codecPar.pointee.width) / Double(codecPar.pointee.height)
|
||||||
|
|
||||||
|
videoStream = StreamContext(index: streamIndex, codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect)
|
||||||
|
break
|
||||||
|
}
|
||||||
} else if codecPar.pointee.codec_id == AV_CODEC_ID_H264 {
|
} else if codecPar.pointee.codec_id == AV_CODEC_ID_H264 {
|
||||||
if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAVCCodecData(UInt32(kCMVideoCodecType_H264), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size) {
|
if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAVCCodecData(UInt32(kCMVideoCodecType_H264), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size) {
|
||||||
let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000))
|
let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000))
|
||||||
|
|||||||
@ -40,6 +40,35 @@ final class FFMpegMediaFrameSourceContextHelpers {
|
|||||||
return formatDescription
|
return formatDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func createFormatDescriptionFromMpeg4CodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: UnsafePointer<UInt8>, _ extradata_size: Int32) -> CMFormatDescription? {
|
||||||
|
let par = NSMutableDictionary()
|
||||||
|
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
|
||||||
|
par.setObject(1 as NSNumber, forKey: "VerticalSpacing" as NSString)
|
||||||
|
|
||||||
|
let atoms = NSMutableDictionary()
|
||||||
|
atoms.setObject(NSData(bytes: extradata, length: Int(extradata_size)), forKey: "esds" as NSString)
|
||||||
|
|
||||||
|
let extensions = NSMutableDictionary()
|
||||||
|
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationBottomField" as NSString)
|
||||||
|
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationTopField" as NSString)
|
||||||
|
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
|
||||||
|
extensions.setObject(par, forKey: "CVPixelAspectRatio" as NSString)
|
||||||
|
extensions.setObject(atoms, forKey: "SampleDescriptionExtensionAtoms" as NSString)
|
||||||
|
extensions.setObject("mp4v" as NSString, forKey: "FormatName" as NSString)
|
||||||
|
extensions.setObject(0 as NSNumber, forKey: "SpatialQuality" as NSString)
|
||||||
|
//extensions.setObject(0 as NSNumber, forKey: "Version" as NSString)
|
||||||
|
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
|
||||||
|
extensions.setObject(1 as NSNumber, forKey: "CVFieldCount" as NSString)
|
||||||
|
extensions.setObject(24 as NSNumber, forKey: "Depth" as NSString)
|
||||||
|
|
||||||
|
var formatDescription: CMFormatDescription?
|
||||||
|
guard CMVideoFormatDescriptionCreate(nil, kCMVideoCodecType_MPEG4Video, width, height, extensions, &formatDescription) == noErr else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatDescription
|
||||||
|
}
|
||||||
|
|
||||||
static func createFormatDescriptionFromHEVCCodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: UnsafePointer<UInt8>, _ extradata_size: Int32) -> CMFormatDescription? {
|
static func createFormatDescriptionFromHEVCCodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: UnsafePointer<UInt8>, _ extradata_size: Int32) -> CMFormatDescription? {
|
||||||
let par = NSMutableDictionary()
|
let par = NSMutableDictionary()
|
||||||
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
|
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
|
||||||
|
|||||||
@ -208,6 +208,7 @@ public func fetchVideoLibraryMediaResourceHash(resource: VideoLibraryMediaResour
|
|||||||
if fetchResult.count != 0 {
|
if fetchResult.count != 0 {
|
||||||
let asset = fetchResult.object(at: 0)
|
let asset = fetchResult.object(at: 0)
|
||||||
let option = PHVideoRequestOptions()
|
let option = PHVideoRequestOptions()
|
||||||
|
option.isNetworkAccessAllowed = true
|
||||||
option.deliveryMode = .highQualityFormat
|
option.deliveryMode = .highQualityFormat
|
||||||
|
|
||||||
let alreadyReceivedAsset = Atomic<Bool>(value: false)
|
let alreadyReceivedAsset = Atomic<Bool>(value: false)
|
||||||
|
|||||||
@ -8,7 +8,12 @@ enum FileMediaResourcePlaybackStatus {
|
|||||||
case paused
|
case paused
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FileMediaResourceStatus {
|
struct FileMediaResourceStatus {
|
||||||
|
let mediaStatus: FileMediaResourceMediaStatus
|
||||||
|
let fetchStatus: MediaResourceStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FileMediaResourceMediaStatus {
|
||||||
case fetchStatus(MediaResourceStatus)
|
case fetchStatus(MediaResourceStatus)
|
||||||
case playbackStatus(FileMediaResourcePlaybackStatus)
|
case playbackStatus(FileMediaResourcePlaybackStatus)
|
||||||
}
|
}
|
||||||
@ -50,45 +55,49 @@ func messageFileMediaResourceStatus(account: Account, file: TelegramMediaFile, m
|
|||||||
|
|
||||||
if message.flags.isSending {
|
if message.flags.isSending {
|
||||||
return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), account.pendingMessageManager.pendingMessageStatus(message.id), playbackStatus)
|
return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), account.pendingMessageManager.pendingMessageStatus(message.id), playbackStatus)
|
||||||
|> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in
|
|> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in
|
||||||
if let playbackStatus = playbackStatus {
|
let mediaStatus: FileMediaResourceMediaStatus
|
||||||
switch playbackStatus {
|
if let playbackStatus = playbackStatus {
|
||||||
case .playing:
|
switch playbackStatus {
|
||||||
return .playbackStatus(.playing)
|
case .playing:
|
||||||
case .paused:
|
mediaStatus = .playbackStatus(.playing)
|
||||||
return .playbackStatus(.paused)
|
case .paused:
|
||||||
case let .buffering(_, whilePlaying):
|
mediaStatus = .playbackStatus(.paused)
|
||||||
if whilePlaying {
|
case let .buffering(_, whilePlaying):
|
||||||
return .playbackStatus(.playing)
|
if whilePlaying {
|
||||||
} else {
|
mediaStatus = .playbackStatus(.playing)
|
||||||
return .playbackStatus(.paused)
|
} else {
|
||||||
}
|
mediaStatus = .playbackStatus(.paused)
|
||||||
}
|
}
|
||||||
} else if let pendingStatus = pendingStatus {
|
|
||||||
return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress))
|
|
||||||
} else {
|
|
||||||
return .fetchStatus(resourceStatus)
|
|
||||||
}
|
}
|
||||||
|
} else if let pendingStatus = pendingStatus {
|
||||||
|
mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress))
|
||||||
|
} else {
|
||||||
|
mediaStatus = .fetchStatus(resourceStatus)
|
||||||
|
}
|
||||||
|
return FileMediaResourceStatus(mediaStatus: mediaStatus, fetchStatus: resourceStatus)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), playbackStatus)
|
return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), playbackStatus)
|
||||||
|> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in
|
|> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in
|
||||||
if let playbackStatus = playbackStatus {
|
let mediaStatus: FileMediaResourceMediaStatus
|
||||||
switch playbackStatus {
|
if let playbackStatus = playbackStatus {
|
||||||
case .playing:
|
switch playbackStatus {
|
||||||
return .playbackStatus(.playing)
|
case .playing:
|
||||||
case .paused:
|
mediaStatus = .playbackStatus(.playing)
|
||||||
return .playbackStatus(.paused)
|
case .paused:
|
||||||
case let .buffering(_, whilePlaying):
|
mediaStatus = .playbackStatus(.paused)
|
||||||
if whilePlaying {
|
case let .buffering(_, whilePlaying):
|
||||||
return .playbackStatus(.playing)
|
if whilePlaying {
|
||||||
} else {
|
mediaStatus = .playbackStatus(.playing)
|
||||||
return .playbackStatus(.paused)
|
} else {
|
||||||
}
|
mediaStatus = .playbackStatus(.paused)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return .fetchStatus(resourceStatus)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
mediaStatus = .fetchStatus(resourceStatus)
|
||||||
|
}
|
||||||
|
return FileMediaResourceStatus(mediaStatus: mediaStatus, fetchStatus: resourceStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,9 +131,13 @@ func galleryItemForEntry(account: Account, presentationData: PresentationData, e
|
|||||||
if file.isVideo || supportedVideoMimeTypes.contains(file.mimeType) {
|
if file.isVideo || supportedVideoMimeTypes.contains(file.mimeType) {
|
||||||
let content: UniversalVideoContent
|
let content: UniversalVideoContent
|
||||||
if file.isAnimated {
|
if file.isAnimated {
|
||||||
content = NativeVideoContent(id: .message(message.id, message.stableId + 1, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: true)
|
content = NativeVideoContent(id: .message(message.id, message.stableId + 1, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: true, enableSound: false)
|
||||||
} else {
|
} else {
|
||||||
content = NativeVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
||||||
|
content = NativeVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||||
|
} else {
|
||||||
|
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text, hideControls: hideControls, playbackCompleted: playbackCompleted)
|
return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text, hideControls: hideControls, playbackCompleted: playbackCompleted)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
|||||||
self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0
|
self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0
|
||||||
self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||||
self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("tds", orElse: 0)) ?? .filtered
|
self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("tds", orElse: 0)) ?? .filtered
|
||||||
self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 0)) ?? .chats
|
self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 0)) ?? .messages
|
||||||
self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0
|
self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -157,7 +157,7 @@ final class LegacyStickerImageDataSource: TGImageDataSource {
|
|||||||
attributes.append(.Sticker(displayText: "", packReference: .id(id: stickerPackId, accessHash: stickerPackAccessHash), maskData: nil))
|
attributes.append(.Sticker(displayText: "", packReference: .id(id: stickerPackId, accessHash: stickerPackAccessHash), maskData: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: attributes), small: !highQuality, fitSize: fitSize, completion: { image in
|
return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil, fileName: fileNameFromFileAttributes(attributes)), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: attributes), small: !highQuality, fitSize: fitSize, completion: { image in
|
||||||
if let image = image {
|
if let image = image {
|
||||||
sharedImageCache.setImage(image, forKey: uri, attributes: nil)
|
sharedImageCache.setImage(image, forKey: uri, attributes: nil)
|
||||||
completion?(TGDataResource(image: image, decoded: true))
|
completion?(TGDataResource(image: image, decoded: true))
|
||||||
|
|||||||
@ -92,6 +92,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect,
|
|||||||
legacyController.bind(controller: baseController)
|
legacyController.bind(controller: baseController)
|
||||||
legacyController.presentationCompleted = { [weak legacyController, weak baseController] in
|
legacyController.presentationCompleted = { [weak legacyController, weak baseController] in
|
||||||
if let legacyController = legacyController, let baseController = baseController {
|
if let legacyController = legacyController, let baseController = baseController {
|
||||||
|
legacyController.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||||
let inputPanelTheme = theme.chat.inputPanel
|
let inputPanelTheme = theme.chat.inputPanel
|
||||||
var uploadInterface: LegacyLiveUploadInterface?
|
var uploadInterface: LegacyLiveUploadInterface?
|
||||||
if peerId.namespace != Namespaces.Peer.SecretChat {
|
if peerId.namespace != Namespaces.Peer.SecretChat {
|
||||||
|
|||||||
@ -154,7 +154,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
|
|
||||||
private let statusDisposable = MetaDisposable()
|
private let statusDisposable = MetaDisposable()
|
||||||
private let fetchControls = Atomic<FetchControls?>(value: nil)
|
private let fetchControls = Atomic<FetchControls?>(value: nil)
|
||||||
private var resourceStatus: FileMediaResourceStatus?
|
private var resourceStatus: FileMediaResourceMediaStatus?
|
||||||
private let fetchDisposable = MetaDisposable()
|
private let fetchDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var downloadStatusIconNode: ASImageNode
|
private var downloadStatusIconNode: ASImageNode
|
||||||
@ -396,10 +396,11 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
|
|
||||||
if isAudio {
|
if isAudio {
|
||||||
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
||||||
updatedStatusSignal = currentUpdatedStatusSignal |> map { status in
|
updatedStatusSignal = currentUpdatedStatusSignal
|
||||||
switch status {
|
|> map { status in
|
||||||
|
switch status.mediaStatus {
|
||||||
case .fetchStatus:
|
case .fetchStatus:
|
||||||
return .fetchStatus(.Local)
|
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
|
||||||
case .playbackStatus:
|
case .playbackStatus:
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
@ -571,7 +572,9 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let updatedStatusSignal = updatedStatusSignal {
|
if let updatedStatusSignal = updatedStatusSignal {
|
||||||
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
|
strongSelf.statusDisposable.set((updatedStatusSignal
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in
|
||||||
|
let status = fileStatus.mediaStatus
|
||||||
displayLinkDispatcher.dispatch {
|
displayLinkDispatcher.dispatch {
|
||||||
if let strongSelf = strongSelf {
|
if let strongSelf = strongSelf {
|
||||||
strongSelf.resourceStatus = status
|
strongSelf.resourceStatus = status
|
||||||
|
|||||||
@ -9,7 +9,7 @@ public enum NavigateToChatKeepStack {
|
|||||||
case never
|
case never
|
||||||
}
|
}
|
||||||
|
|
||||||
public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (()-> Void)? = nil, animated: Bool = true) {
|
public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, animated: Bool = true, completion: @escaping () -> Void = {}) {
|
||||||
var found = false
|
var found = false
|
||||||
var isFirst = true
|
var isFirst = true
|
||||||
for controller in navigationController.viewControllers.reversed() {
|
for controller in navigationController.viewControllers.reversed() {
|
||||||
@ -24,6 +24,7 @@ public func navigateToChatController(navigationController: NavigationController,
|
|||||||
} else {
|
} else {
|
||||||
let _ = navigationController.popToViewController(controller, animated: animated)
|
let _ = navigationController.popToViewController(controller, animated: animated)
|
||||||
}
|
}
|
||||||
|
completion()
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -48,9 +49,9 @@ public func navigateToChatController(navigationController: NavigationController,
|
|||||||
resolvedKeepStack = false
|
resolvedKeepStack = false
|
||||||
}
|
}
|
||||||
if resolvedKeepStack {
|
if resolvedKeepStack {
|
||||||
navigationController.pushViewController(controller)
|
navigationController.pushViewController(controller, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
navigationController.replaceAllButRootController(controller, animated: animated)
|
navigationController.replaceAllButRootController(controller, animated: animated, completion: completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,6 +137,49 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
|
|||||||
}
|
}
|
||||||
|
|
||||||
let continueHandling: () -> Void = {
|
let continueHandling: () -> Void = {
|
||||||
|
let handleInternalUrl: (String) -> Void = { url in
|
||||||
|
let _ = (resolveUrl(account: account, url: url)
|
||||||
|
|> deliverOnMainQueue).start(next: { resolved in
|
||||||
|
if case let .externalUrl(value) = resolved {
|
||||||
|
applicationContext.applicationBindings.openUrl(value)
|
||||||
|
} else {
|
||||||
|
openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in
|
||||||
|
switch navigation {
|
||||||
|
case .info:
|
||||||
|
let _ = (account.postbox.loadedPeerWithId(peerId)
|
||||||
|
|> deliverOnMainQueue).start(next: { peer in
|
||||||
|
if let infoController = peerInfoController(account: account, peer: peer) {
|
||||||
|
if let navigationController = navigationController {
|
||||||
|
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
navigationController?.pushViewController(infoController)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case .chat:
|
||||||
|
if let navigationController = navigationController {
|
||||||
|
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||||
|
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId))
|
||||||
|
}
|
||||||
|
case let .withBotStartPayload(payload):
|
||||||
|
if let navigationController = navigationController {
|
||||||
|
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||||
|
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), botStart: payload)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, present: { c, a in
|
||||||
|
if let navigationController = navigationController {
|
||||||
|
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||||
|
(navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a)
|
||||||
|
}
|
||||||
|
}, dismissInput: {
|
||||||
|
dismissInput()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if parsedUrl.scheme == "tg", let query = parsedUrl.query {
|
if parsedUrl.scheme == "tg", let query = parsedUrl.query {
|
||||||
var convertedUrl: String?
|
var convertedUrl: String?
|
||||||
if parsedUrl.host == "localpeer" {
|
if parsedUrl.host == "localpeer" {
|
||||||
@ -418,64 +461,29 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let convertedUrl = convertedUrl {
|
if let convertedUrl = convertedUrl {
|
||||||
let _ = (resolveUrl(account: account, url: convertedUrl)
|
handleInternalUrl(convertedUrl)
|
||||||
|> deliverOnMainQueue).start(next: { resolved in
|
|
||||||
if case let .externalUrl(value) = resolved {
|
|
||||||
applicationContext.applicationBindings.openUrl(value)
|
|
||||||
} else {
|
|
||||||
openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in
|
|
||||||
switch navigation {
|
|
||||||
case .info:
|
|
||||||
let _ = (account.postbox.loadedPeerWithId(peerId)
|
|
||||||
|> deliverOnMainQueue).start(next: { peer in
|
|
||||||
if let infoController = peerInfoController(account: account, peer: peer) {
|
|
||||||
if let navigationController = navigationController {
|
|
||||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
navigationController?.pushViewController(infoController)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
case .chat:
|
|
||||||
if let navigationController = navigationController {
|
|
||||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
|
||||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId))
|
|
||||||
}
|
|
||||||
case let .withBotStartPayload(payload):
|
|
||||||
if let navigationController = navigationController {
|
|
||||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
|
||||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), botStart: payload)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}, present: { c, a in
|
|
||||||
if let navigationController = navigationController {
|
|
||||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
|
||||||
(navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a)
|
|
||||||
}
|
|
||||||
}, dismissInput: {
|
|
||||||
dismissInput()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
|
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
|
||||||
if #available(iOSApplicationExtension 9.0, *) {
|
if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" {
|
||||||
if let window = navigationController?.view.window {
|
handleInternalUrl(parsedUrl.absoluteString)
|
||||||
let controller = SFSafariViewController(url: parsedUrl)
|
|
||||||
if #available(iOSApplicationExtension 10.0, *) {
|
|
||||||
controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor
|
|
||||||
controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor
|
|
||||||
}
|
|
||||||
window.rootViewController?.present(controller, animated: true)
|
|
||||||
} else {
|
|
||||||
applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
applicationContext.applicationBindings.openUrl(url)
|
if #available(iOSApplicationExtension 9.0, *) {
|
||||||
|
if let window = navigationController?.view.window {
|
||||||
|
let controller = SFSafariViewController(url: parsedUrl)
|
||||||
|
if #available(iOSApplicationExtension 10.0, *) {
|
||||||
|
controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor
|
||||||
|
controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor
|
||||||
|
}
|
||||||
|
window.rootViewController?.present(controller, animated: true)
|
||||||
|
} else {
|
||||||
|
applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
applicationContext.applicationBindings.openUrl(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
applicationContext.applicationBindings.openUrl(url)
|
applicationContext.applicationBindings.openUrl(url)
|
||||||
@ -483,11 +491,16 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
|
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
|
||||||
applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in
|
let nativeHosts = ["t.me", "telegram.me"]
|
||||||
if !success {
|
if let host = parsedUrl.host, nativeHosts.contains(host) {
|
||||||
continueHandling()
|
continueHandling()
|
||||||
}
|
} else {
|
||||||
}))
|
applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in
|
||||||
|
if !success {
|
||||||
|
continueHandling()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
continueHandling()
|
continueHandling()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import Display
|
|||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
|
|
||||||
enum OverlayStatusControllerType {
|
enum OverlayStatusControllerType {
|
||||||
case loading
|
case loading(cancelled: (() -> Void)?)
|
||||||
case success
|
case success
|
||||||
case proxySettingSuccess
|
case proxySettingSuccess
|
||||||
}
|
}
|
||||||
@ -47,12 +47,14 @@ private enum OverlayStatusContentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dismiss() {
|
func dismiss(completion: @escaping () -> Void) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .loading(controller):
|
case let .loading(controller):
|
||||||
controller.dismiss(true) {}
|
controller.dismiss(true, completion: {
|
||||||
|
completion()
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
break
|
completion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,8 +66,12 @@ private final class OverlayStatusControllerNode: ViewControllerTracingNode {
|
|||||||
init(theme: PresentationTheme, type: OverlayStatusControllerType, dismissed: @escaping () -> Void) {
|
init(theme: PresentationTheme, type: OverlayStatusControllerType, dismissed: @escaping () -> Void) {
|
||||||
self.dismissed = dismissed
|
self.dismissed = dismissed
|
||||||
switch type {
|
switch type {
|
||||||
case .loading:
|
case let .loading(cancelled):
|
||||||
self.contentController = .loading(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light))
|
let controller = TGProgressWindowController(light: theme.actionSheet.backgroundType == .light)!
|
||||||
|
controller.cancelled = {
|
||||||
|
cancelled?()
|
||||||
|
}
|
||||||
|
self.contentController = .loading(controller)
|
||||||
case .success:
|
case .success:
|
||||||
self.contentController = .progress(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light))
|
self.contentController = .progress(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light))
|
||||||
case .proxySettingSuccess:
|
case .proxySettingSuccess:
|
||||||
@ -92,8 +98,9 @@ private final class OverlayStatusControllerNode: ViewControllerTracingNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func dismiss() {
|
func dismiss() {
|
||||||
self.contentController.dismiss()
|
self.contentController.dismiss(completion: { [weak self] in
|
||||||
self.dismissed()
|
self?.dismissed()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -102,6 +102,56 @@ final class PeerChannelMemberCategoriesContextsManager {
|
|||||||
return self.getContext(postbox: postbox, network: network, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated)
|
return self.getContext(postbox: postbox, network: network, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recentOnline(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<Int32, NoError> {
|
||||||
|
return Signal { [weak self] subscriber in
|
||||||
|
var previousIds: Set<PeerId>?
|
||||||
|
let statusesDisposable = MetaDisposable()
|
||||||
|
let disposableAndControl = self?.recent(postbox: postbox, network: network, peerId: peerId, updated: { state in
|
||||||
|
var idList: [PeerId] = []
|
||||||
|
for item in state.list {
|
||||||
|
idList.append(item.peer.id)
|
||||||
|
if idList.count >= 200 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let updatedIds = Set(idList)
|
||||||
|
if previousIds != updatedIds {
|
||||||
|
previousIds = updatedIds
|
||||||
|
let key: PostboxViewKey = .peerPresences(peerIds: updatedIds)
|
||||||
|
statusesDisposable.set((postbox.combinedView(keys: [key])
|
||||||
|
|> map { view -> Int32 in
|
||||||
|
var count: Int32 = 0
|
||||||
|
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||||
|
if let presences = (view.views[key] as? PeerPresencesView)?.presences {
|
||||||
|
for (_, presence) in presences {
|
||||||
|
if let presence = presence as? TelegramUserPresence {
|
||||||
|
let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp))
|
||||||
|
switch relativeStatus {
|
||||||
|
case .online:
|
||||||
|
count += 1
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|> distinctUntilChanged
|
||||||
|
|> deliverOnMainQueue).start(next: { count in
|
||||||
|
subscriber.putNext(count)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return ActionDisposable {
|
||||||
|
disposableAndControl?.0.dispose()
|
||||||
|
statusesDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func admins(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) {
|
func admins(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) {
|
||||||
return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated)
|
return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated)
|
||||||
}
|
}
|
||||||
|
|||||||
330
TelegramUI/PlatformVideoContent.swift
Normal file
330
TelegramUI/PlatformVideoContent.swift
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
import Foundation
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
enum PlatformVideoContentId: Hashable {
|
||||||
|
case message(MessageId, UInt32, MediaId)
|
||||||
|
case instantPage(MediaId, MediaId)
|
||||||
|
|
||||||
|
static func ==(lhs: PlatformVideoContentId, rhs: PlatformVideoContentId) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .message(messageId, stableId, mediaId):
|
||||||
|
if case .message(messageId, stableId, mediaId) = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .instantPage(pageId, mediaId):
|
||||||
|
if case .instantPage(pageId, mediaId) = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashValue: Int {
|
||||||
|
switch self {
|
||||||
|
case let .message(messageId, _, mediaId):
|
||||||
|
return messageId.hashValue &* 31 &+ mediaId.hashValue
|
||||||
|
case let .instantPage(pageId, mediaId):
|
||||||
|
return pageId.hashValue &* 31 &+ mediaId.hashValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PlatformVideoContent: UniversalVideoContent {
|
||||||
|
let id: AnyHashable
|
||||||
|
let nativeId: PlatformVideoContentId
|
||||||
|
let fileReference: FileMediaReference
|
||||||
|
let dimensions: CGSize
|
||||||
|
let duration: Int32
|
||||||
|
let streamVideo: Bool
|
||||||
|
let loopVideo: Bool
|
||||||
|
let enableSound: Bool
|
||||||
|
let baseRate: Double
|
||||||
|
let fetchAutomatically: Bool
|
||||||
|
|
||||||
|
init(id: PlatformVideoContentId, fileReference: FileMediaReference, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) {
|
||||||
|
self.id = id
|
||||||
|
self.nativeId = id
|
||||||
|
self.fileReference = fileReference
|
||||||
|
self.dimensions = fileReference.media.dimensions ?? CGSize(width: 128.0, height: 128.0)
|
||||||
|
self.duration = fileReference.media.duration ?? 0
|
||||||
|
self.streamVideo = streamVideo
|
||||||
|
self.loopVideo = loopVideo
|
||||||
|
self.enableSound = enableSound
|
||||||
|
self.baseRate = baseRate
|
||||||
|
self.fetchAutomatically = fetchAutomatically
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||||
|
return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEqual(to other: UniversalVideoContent) -> Bool {
|
||||||
|
if let other = other as? PlatformVideoContent {
|
||||||
|
if case let .message(_, stableId, _) = self.nativeId {
|
||||||
|
if case .message(_, stableId, _) = other.nativeId {
|
||||||
|
if self.fileReference.media.isInstantVideo {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
||||||
|
private let postbox: Postbox
|
||||||
|
private let fileReference: FileMediaReference
|
||||||
|
private let approximateDuration: Double
|
||||||
|
private let intrinsicDimensions: CGSize
|
||||||
|
|
||||||
|
private let audioSessionManager: ManagedAudioSession
|
||||||
|
private let audioSessionDisposable = MetaDisposable()
|
||||||
|
private var hasAudioSession = false
|
||||||
|
|
||||||
|
private let playbackCompletedListeners = Bag<() -> Void>()
|
||||||
|
|
||||||
|
private var initializedStatus = false
|
||||||
|
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused)
|
||||||
|
private var isBuffering = false
|
||||||
|
private let _status = ValuePromise<MediaPlayerStatus>()
|
||||||
|
var status: Signal<MediaPlayerStatus, NoError> {
|
||||||
|
return self._status.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let _bufferingStatus = Promise<(IndexSet, Int)?>()
|
||||||
|
var bufferingStatus: Signal<(IndexSet, Int)?, NoError> {
|
||||||
|
return self._bufferingStatus.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let _ready = Promise<Void>()
|
||||||
|
var ready: Signal<Void, NoError> {
|
||||||
|
return self._ready.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let _preloadCompleted = ValuePromise<Bool>()
|
||||||
|
var preloadCompleted: Signal<Bool, NoError> {
|
||||||
|
return self._preloadCompleted.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let imageNode: TransformImageNode
|
||||||
|
|
||||||
|
private let playerItem: AVPlayerItem
|
||||||
|
private let player: AVPlayer
|
||||||
|
private let playerNode: ASDisplayNode
|
||||||
|
|
||||||
|
private var loadProgressDisposable: Disposable?
|
||||||
|
private var statusDisposable: Disposable?
|
||||||
|
|
||||||
|
private var didPlayToEndTimeObserver: NSObjectProtocol?
|
||||||
|
|
||||||
|
private let fetchDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
private var dimensions: CGSize?
|
||||||
|
private let dimensionsPromise = ValuePromise<CGSize>(CGSize())
|
||||||
|
|
||||||
|
private var validLayout: CGSize?
|
||||||
|
|
||||||
|
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) {
|
||||||
|
self.postbox = postbox
|
||||||
|
self.fileReference = fileReference
|
||||||
|
self.approximateDuration = Double(fileReference.media.duration ?? 1)
|
||||||
|
self.audioSessionManager = audioSessionManager
|
||||||
|
|
||||||
|
self.imageNode = TransformImageNode()
|
||||||
|
|
||||||
|
self.playerItem = AVPlayerItem(url: URL(string: postbox.mediaBox.completedResourcePath(fileReference.media.resource, pathExtension: "mov") ?? "")!)
|
||||||
|
let player = AVPlayer(playerItem: self.playerItem)
|
||||||
|
self.player = player
|
||||||
|
|
||||||
|
self.playerNode = ASDisplayNode()
|
||||||
|
self.playerNode.setLayerBlock({
|
||||||
|
return AVPlayerLayer(player: player)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.intrinsicDimensions = fileReference.media.dimensions ?? CGSize()
|
||||||
|
|
||||||
|
self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, videoReference: fileReference) |> map { [weak self] getSize, getData in
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
if let strongSelf = self, strongSelf.dimensions == nil {
|
||||||
|
if let dimensions = getSize() {
|
||||||
|
strongSelf.dimensions = dimensions
|
||||||
|
strongSelf.dimensionsPromise.set(dimensions)
|
||||||
|
if let size = strongSelf.validLayout {
|
||||||
|
strongSelf.updateLayout(size: size, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getData
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addSubnode(self.imageNode)
|
||||||
|
self.addSubnode(self.playerNode)
|
||||||
|
self.player.actionAtItemEnd = .pause
|
||||||
|
|
||||||
|
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { [weak self] notification in
|
||||||
|
self?.performActionAtEnd()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.imageNode.imageUpdated = { [weak self] in
|
||||||
|
self?._ready.set(.single(Void()))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.player.addObserver(self, forKeyPath: "rate", options: [], context: nil)
|
||||||
|
playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
|
||||||
|
playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
|
||||||
|
playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil)
|
||||||
|
|
||||||
|
self._bufferingStatus.set(.single(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.player.removeObserver(self, forKeyPath: "rate")
|
||||||
|
self.playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty")
|
||||||
|
self.playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
|
||||||
|
self.playerItem.removeObserver(self, forKeyPath: "playbackBufferFull")
|
||||||
|
|
||||||
|
self.audioSessionDisposable.dispose()
|
||||||
|
|
||||||
|
self.loadProgressDisposable?.dispose()
|
||||||
|
self.statusDisposable?.dispose()
|
||||||
|
|
||||||
|
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
|
||||||
|
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "rate" {
|
||||||
|
let isPlaying = !self.player.rate.isZero
|
||||||
|
let status: MediaPlayerPlaybackStatus
|
||||||
|
if self.isBuffering {
|
||||||
|
status = .buffering(initial: false, whilePlaying: isPlaying)
|
||||||
|
} else {
|
||||||
|
status = isPlaying ? .playing : .paused
|
||||||
|
}
|
||||||
|
self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: status)
|
||||||
|
self._status.set(self.statusValue)
|
||||||
|
} else if keyPath == "playbackBufferEmpty" {
|
||||||
|
let isPlaying = !self.player.rate.isZero
|
||||||
|
let status: MediaPlayerPlaybackStatus
|
||||||
|
self.isBuffering = true
|
||||||
|
if self.isBuffering {
|
||||||
|
status = .buffering(initial: false, whilePlaying: isPlaying)
|
||||||
|
} else {
|
||||||
|
status = isPlaying ? .playing : .paused
|
||||||
|
}
|
||||||
|
self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: status)
|
||||||
|
self._status.set(self.statusValue)
|
||||||
|
} else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" {
|
||||||
|
let isPlaying = !self.player.rate.isZero
|
||||||
|
let status: MediaPlayerPlaybackStatus
|
||||||
|
self.isBuffering = false
|
||||||
|
if self.isBuffering {
|
||||||
|
status = .buffering(initial: false, whilePlaying: isPlaying)
|
||||||
|
} else {
|
||||||
|
status = isPlaying ? .playing : .paused
|
||||||
|
}
|
||||||
|
self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: status)
|
||||||
|
self._status.set(self.statusValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performActionAtEnd() {
|
||||||
|
for listener in self.playbackCompletedListeners.copyItems() {
|
||||||
|
listener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||||
|
transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||||
|
transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
let makeImageLayout = self.imageNode.asyncLayout()
|
||||||
|
let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets()))
|
||||||
|
applyImageLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func play() {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
if !self.initializedStatus {
|
||||||
|
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true)))
|
||||||
|
}
|
||||||
|
if !self.hasAudioSession {
|
||||||
|
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] _ in
|
||||||
|
self?.hasAudioSession = true
|
||||||
|
self?.player.play()
|
||||||
|
}, deactivate: { [weak self] in
|
||||||
|
self?.hasAudioSession = false
|
||||||
|
self?.player.pause()
|
||||||
|
return .complete()
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
self.player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pause() {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
if !self.initializedStatus {
|
||||||
|
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused))
|
||||||
|
}
|
||||||
|
self.player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
func togglePlayPause() {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
if self.player.rate.isZero {
|
||||||
|
self.play()
|
||||||
|
} else {
|
||||||
|
self.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSoundEnabled(_ value: Bool) {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
}
|
||||||
|
|
||||||
|
func seek(_ timestamp: Double) {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30))
|
||||||
|
}
|
||||||
|
|
||||||
|
func playOnceWithSound(playAndRecord: Bool) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func continuePlayingWithoutSound() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBaseRate(_ baseRate: Double) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int {
|
||||||
|
return self.playbackCompletedListeners.add(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePlaybackCompleted(_ index: Int) {
|
||||||
|
self.playbackCompletedListeners.remove(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -110,6 +110,9 @@ enum PresentationResourceKey: Int32 {
|
|||||||
case chatBubbleActionButtonOutgoingBottomRightImage
|
case chatBubbleActionButtonOutgoingBottomRightImage
|
||||||
case chatBubbleActionButtonOutgoingBottomSingleImage
|
case chatBubbleActionButtonOutgoingBottomSingleImage
|
||||||
|
|
||||||
|
case chatBubbleFileCloudFetchIncomingIcon
|
||||||
|
case chatBubbleFileCloudFetchOutgoingIcon
|
||||||
|
|
||||||
case chatBubbleReplyThumbnailPlayImage
|
case chatBubbleReplyThumbnailPlayImage
|
||||||
|
|
||||||
case chatInfoItemBackgroundImageWithWallpaper
|
case chatInfoItemBackgroundImageWithWallpaper
|
||||||
|
|||||||
@ -1011,4 +1011,16 @@ struct PresentationResourcesChat {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func chatBubbleFileCloudFetchIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||||
|
return theme.image(PresentationResourceKey.chatBubbleFileCloudFetchIncomingIcon.rawValue, { theme in
|
||||||
|
generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: theme.chat.bubble.incomingAccentControlColor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static func chatBubbleFileCloudFetchOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||||
|
return theme.image(PresentationResourceKey.chatBubbleFileCloudFetchOutgoingIcon.rawValue, { theme in
|
||||||
|
generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: theme.chat.bubble.outgoingAccentControlColor)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
301
TelegramUI/RadialCloudProgressContentNode.swift
Normal file
301
TelegramUI/RadialCloudProgressContentNode.swift
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import Foundation
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import LegacyComponents
|
||||||
|
|
||||||
|
private final class RadialCloudProgressContentCancelNodeParameters: NSObject {
|
||||||
|
let color: UIColor
|
||||||
|
|
||||||
|
init(color: UIColor) {
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class RadialCloudProgressContentSpinnerNodeParameters: NSObject {
|
||||||
|
let color: UIColor
|
||||||
|
let backgroundStrokeColor: UIColor
|
||||||
|
let progress: CGFloat
|
||||||
|
let lineWidth: CGFloat?
|
||||||
|
|
||||||
|
init(color: UIColor, backgroundStrokeColor: UIColor, progress: CGFloat, lineWidth: CGFloat?) {
|
||||||
|
self.color = color
|
||||||
|
self.backgroundStrokeColor = backgroundStrokeColor
|
||||||
|
self.progress = progress
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class RadialCloudProgressContentSpinnerNode: ASDisplayNode {
|
||||||
|
var progressAnimationCompleted: (() -> Void)?
|
||||||
|
|
||||||
|
var color: UIColor {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var backgroundStrokeColor: UIColor {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var effectiveProgress: CGFloat = 0.0 {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress: CGFloat? {
|
||||||
|
didSet {
|
||||||
|
self.pop_removeAnimation(forKey: "progress")
|
||||||
|
if let progress = self.progress {
|
||||||
|
self.pop_removeAnimation(forKey: "indefiniteProgress")
|
||||||
|
|
||||||
|
let animation = POPBasicAnimation()
|
||||||
|
animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in
|
||||||
|
property?.readBlock = { node, values in
|
||||||
|
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
|
||||||
|
}
|
||||||
|
property?.writeBlock = { node, values in
|
||||||
|
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
|
||||||
|
}
|
||||||
|
property?.threshold = 0.01
|
||||||
|
}) as! POPAnimatableProperty
|
||||||
|
animation.fromValue = CGFloat(self.effectiveProgress) as NSNumber
|
||||||
|
animation.toValue = CGFloat(progress) as NSNumber
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
|
||||||
|
animation.duration = 0.2
|
||||||
|
animation.completionBlock = { [weak self] _, _ in
|
||||||
|
self?.progressAnimationCompleted?()
|
||||||
|
}
|
||||||
|
self.pop_add(animation, forKey: "progress")
|
||||||
|
} else if self.pop_animation(forKey: "indefiniteProgress") == nil {
|
||||||
|
let animation = POPBasicAnimation()
|
||||||
|
animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in
|
||||||
|
property?.readBlock = { node, values in
|
||||||
|
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
|
||||||
|
}
|
||||||
|
property?.writeBlock = { node, values in
|
||||||
|
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
|
||||||
|
}
|
||||||
|
property?.threshold = 0.01
|
||||||
|
}) as! POPAnimatableProperty
|
||||||
|
animation.fromValue = CGFloat(0.0) as NSNumber
|
||||||
|
animation.toValue = CGFloat(2.0) as NSNumber
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
|
||||||
|
animation.duration = 2.5
|
||||||
|
animation.repeatForever = true
|
||||||
|
self.pop_add(animation, forKey: "indefiniteProgress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isAnimatingProgress: Bool {
|
||||||
|
return self.pop_animation(forKey: "progress") != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let lineWidth: CGFloat?
|
||||||
|
|
||||||
|
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
|
||||||
|
self.color = color
|
||||||
|
self.backgroundStrokeColor = backgroundStrokeColor
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.isLayerBacked = true
|
||||||
|
self.displaysAsynchronously = true
|
||||||
|
self.isOpaque = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||||
|
return RadialCloudProgressContentSpinnerNodeParameters(color: self.color, backgroundStrokeColor: self.backgroundStrokeColor, progress: self.effectiveProgress, lineWidth: self.lineWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||||
|
let context = UIGraphicsGetCurrentContext()!
|
||||||
|
|
||||||
|
if !isRasterizing {
|
||||||
|
context.setBlendMode(.copy)
|
||||||
|
context.setFillColor(UIColor.clear.cgColor)
|
||||||
|
context.fill(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let parameters = parameters as? RadialCloudProgressContentSpinnerNodeParameters {
|
||||||
|
let factor = bounds.size.width / 50.0
|
||||||
|
|
||||||
|
var progress = parameters.progress
|
||||||
|
var startAngle = -CGFloat.pi / 2.0
|
||||||
|
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
|
||||||
|
|
||||||
|
if progress > 1.0 {
|
||||||
|
progress = 2.0 - progress
|
||||||
|
let tmp = startAngle
|
||||||
|
startAngle = endAngle
|
||||||
|
endAngle = tmp
|
||||||
|
}
|
||||||
|
progress = min(1.0, progress)
|
||||||
|
|
||||||
|
let lineWidth: CGFloat = parameters.lineWidth ?? max(1.6, 2.25 * factor)
|
||||||
|
|
||||||
|
let pathDiameter: CGFloat
|
||||||
|
if parameters.lineWidth != nil {
|
||||||
|
pathDiameter = bounds.size.width - lineWidth
|
||||||
|
} else {
|
||||||
|
pathDiameter = bounds.size.width - lineWidth - 2.5 * 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setStrokeColor(parameters.backgroundStrokeColor.cgColor)
|
||||||
|
let backgroundPath = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: 0.0, endAngle: 2.0 * CGFloat.pi, clockwise:true)
|
||||||
|
backgroundPath.lineWidth = lineWidth
|
||||||
|
backgroundPath.stroke()
|
||||||
|
|
||||||
|
context.setStrokeColor(parameters.color.cgColor)
|
||||||
|
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true)
|
||||||
|
path.lineWidth = lineWidth
|
||||||
|
path.lineCapStyle = .round
|
||||||
|
path.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func willEnterHierarchy() {
|
||||||
|
super.willEnterHierarchy()
|
||||||
|
|
||||||
|
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||||
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||||
|
basicAnimation.duration = 2.0
|
||||||
|
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
||||||
|
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
|
||||||
|
basicAnimation.repeatCount = Float.infinity
|
||||||
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
|
||||||
|
basicAnimation.beginTime = 1.0
|
||||||
|
|
||||||
|
self.layer.add(basicAnimation, forKey: "progressRotation")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didExitHierarchy() {
|
||||||
|
super.didExitHierarchy()
|
||||||
|
|
||||||
|
self.layer.removeAnimation(forKey: "progressRotation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class RadialCloudProgressContentCancelNode: ASDisplayNode {
|
||||||
|
var color: UIColor {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(color: UIColor) {
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.isLayerBacked = true
|
||||||
|
self.displaysAsynchronously = true
|
||||||
|
self.isOpaque = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||||
|
return RadialCloudProgressContentCancelNodeParameters(color: self.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||||
|
let context = UIGraphicsGetCurrentContext()!
|
||||||
|
|
||||||
|
if !isRasterizing {
|
||||||
|
context.setBlendMode(.copy)
|
||||||
|
context.setFillColor(UIColor.clear.cgColor)
|
||||||
|
context.fill(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let parameters = parameters as? RadialCloudProgressContentCancelNodeParameters {
|
||||||
|
let size: CGFloat = 8.0
|
||||||
|
context.setFillColor(parameters.color.cgColor)
|
||||||
|
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: floor((bounds.size.width - size) / 2.0), y: floor((bounds.size.height - size) / 2.0)), size: CGSize(width: size, height: size)), cornerRadius: 1.0)
|
||||||
|
path.fill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class RadialCloudProgressContentNode: RadialStatusContentNode {
|
||||||
|
private let spinnerNode: RadialCloudProgressContentSpinnerNode
|
||||||
|
private let cancelNode: RadialCloudProgressContentCancelNode
|
||||||
|
|
||||||
|
var color: UIColor {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsDisplay()
|
||||||
|
self.spinnerNode.color = self.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var backgroundStrokeColor: UIColor {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsDisplay()
|
||||||
|
self.spinnerNode.backgroundStrokeColor = self.backgroundStrokeColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress: CGFloat? = 0.0 {
|
||||||
|
didSet {
|
||||||
|
self.spinnerNode.progress = self.progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var enqueuedReadyForTransition: (() -> Void)?
|
||||||
|
|
||||||
|
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
|
||||||
|
self.color = color
|
||||||
|
self.backgroundStrokeColor = backgroundStrokeColor
|
||||||
|
|
||||||
|
self.spinnerNode = RadialCloudProgressContentSpinnerNode(color: color, backgroundStrokeColor: backgroundStrokeColor, lineWidth: lineWidth)
|
||||||
|
self.cancelNode = RadialCloudProgressContentCancelNode(color: color)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.isLayerBacked = true
|
||||||
|
|
||||||
|
self.addSubnode(self.spinnerNode)
|
||||||
|
self.addSubnode(self.cancelNode)
|
||||||
|
|
||||||
|
self.spinnerNode.progressAnimationCompleted = { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition {
|
||||||
|
strongSelf.enqueuedReadyForTransition = nil
|
||||||
|
enqueuedReadyForTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func enqueueReadyForTransition(_ f: @escaping () -> Void) {
|
||||||
|
if self.spinnerNode.isAnimatingProgress {
|
||||||
|
self.enqueuedReadyForTransition = f
|
||||||
|
} else {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layout() {
|
||||||
|
super.layout()
|
||||||
|
|
||||||
|
let bounds = self.bounds
|
||||||
|
self.spinnerNode.bounds = bounds
|
||||||
|
self.spinnerNode.position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
|
||||||
|
self.cancelNode.frame = bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
override func animateOut(completion: @escaping () -> Void) {
|
||||||
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
self.cancelNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func animateIn() {
|
||||||
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
|
self.cancelNode.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ public enum RadialStatusNodeState: Equatable {
|
|||||||
case play(UIColor)
|
case play(UIColor)
|
||||||
case pause(UIColor)
|
case pause(UIColor)
|
||||||
case progress(color: UIColor, lineWidth: CGFloat?, value: CGFloat?, cancelEnabled: Bool)
|
case progress(color: UIColor, lineWidth: CGFloat?, value: CGFloat?, cancelEnabled: Bool)
|
||||||
|
case cloudProgress(color: UIColor, strokeBackgroundColor: UIColor, lineWidth: CGFloat, value: CGFloat?)
|
||||||
case check(UIColor)
|
case check(UIColor)
|
||||||
case customIcon(UIImage)
|
case customIcon(UIImage)
|
||||||
case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double)
|
case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double)
|
||||||
@ -43,6 +44,12 @@ public enum RadialStatusNodeState: Equatable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
case let .cloudProgress(lhsColor, lhsStrokeBackgroundColor, lhsLineWidth, lhsValue):
|
||||||
|
if case let .cloudProgress(rhsColor, rhsStrokeBackgroundColor, rhsLineWidth, rhsValue) = rhs, lhsColor.isEqual(rhsColor), lhsStrokeBackgroundColor.isEqual(rhsStrokeBackgroundColor), lhsLineWidth.isEqual(to: rhsLineWidth), lhsValue == rhsValue {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
case let .check(lhsColor):
|
case let .check(lhsColor):
|
||||||
if case let .check(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
|
if case let .check(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
|
||||||
return true
|
return true
|
||||||
@ -99,6 +106,18 @@ public enum RadialStatusNodeState: Equatable {
|
|||||||
node.progress = value
|
node.progress = value
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
case let .cloudProgress(color, strokeLineColor, lineWidth, value):
|
||||||
|
if let current = current as? RadialCloudProgressContentNode {
|
||||||
|
if !current.color.isEqual(color) {
|
||||||
|
current.color = color
|
||||||
|
}
|
||||||
|
current.progress = value
|
||||||
|
return current
|
||||||
|
} else {
|
||||||
|
let node = RadialCloudProgressContentNode(color: color, backgroundStrokeColor: strokeLineColor, lineWidth: lineWidth)
|
||||||
|
node.progress = value
|
||||||
|
return node
|
||||||
|
}
|
||||||
case let .secretTimeout(color, icon, beginTime, timeout):
|
case let .secretTimeout(color, icon, beginTime, timeout):
|
||||||
return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon)
|
return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,15 +9,20 @@ import Display
|
|||||||
func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal<Void, NoError> {
|
func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal<Void, NoError> {
|
||||||
var resource: MediaResource?
|
var resource: MediaResource?
|
||||||
var isImage = true
|
var isImage = true
|
||||||
|
var fileExtension: String?
|
||||||
if let image = mediaReference.media as? TelegramMediaImage {
|
if let image = mediaReference.media as? TelegramMediaImage {
|
||||||
if let representation = largestImageRepresentation(image.representations) {
|
if let representation = largestImageRepresentation(image.representations) {
|
||||||
resource = representation.resource
|
resource = representation.resource
|
||||||
}
|
}
|
||||||
} else if let file = mediaReference.media as? TelegramMediaFile {
|
} else if let file = mediaReference.media as? TelegramMediaFile {
|
||||||
resource = file.resource
|
resource = file.resource
|
||||||
if file.isVideo {
|
if file.isVideo || file.mimeType.hasPrefix("video/") {
|
||||||
isImage = false
|
isImage = false
|
||||||
}
|
}
|
||||||
|
let maybeExtension = ((file.fileName ?? "") as NSString).pathExtension
|
||||||
|
if !maybeExtension.isEmpty {
|
||||||
|
fileExtension = maybeExtension
|
||||||
|
}
|
||||||
} else if let webpage = mediaReference.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
} else if let webpage = mediaReference.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||||
if let file = content.file {
|
if let file = content.file {
|
||||||
resource = file.resource
|
resource = file.resource
|
||||||
@ -34,7 +39,7 @@ func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: P
|
|||||||
if let resource = resource {
|
if let resource = resource {
|
||||||
let fetchedData: Signal<MediaResourceData, NoError> = Signal { subscriber in
|
let fetchedData: Signal<MediaResourceData, NoError> = Signal { subscriber in
|
||||||
let fetched = fetchedMediaResource(postbox: postbox, reference: mediaReference.resourceReference(resource)).start()
|
let fetched = fetchedMediaResource(postbox: postbox, reference: mediaReference.resourceReference(resource)).start()
|
||||||
let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: true)).start(next: { next in
|
let data = postbox.mediaBox.resourceData(resource, pathExtension: fileExtension, option: .complete(waitUntilFetchStatus: true)).start(next: { next in
|
||||||
subscriber.putNext(next)
|
subscriber.putNext(next)
|
||||||
}, completed: {
|
}, completed: {
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
|
|||||||
@ -443,7 +443,7 @@ public func settingsController(account: Account, accountManager: AccountManager)
|
|||||||
let archivedPacks = Promise<[ArchivedStickerPackItem]?>()
|
let archivedPacks = Promise<[ArchivedStickerPackItem]?>()
|
||||||
|
|
||||||
let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in
|
let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in
|
||||||
let controller = OverlayStatusController(theme: account.telegramApplicationContext.currentPresentationData.with { $0 }.theme, type: .loading)
|
let controller = OverlayStatusController(theme: account.telegramApplicationContext.currentPresentationData.with { $0 }.theme, type: .loading(cancelled: nil))
|
||||||
presentControllerImpl?(controller, nil)
|
presentControllerImpl?(controller, nil)
|
||||||
let _ = (resolvedUrl.get()
|
let _ = (resolvedUrl.get()
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|
|||||||
@ -1251,7 +1251,35 @@ public func userInfoController(account: Account, peerId: PeerId, mode: UserInfoC
|
|||||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(currentPeerId))
|
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(currentPeerId))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
createSecretChatDisposable.set((createSecretChat(account: account, peerId: peerId) |> deliverOnMainQueue).start(next: { peerId in
|
var createSignal = createSecretChat(account: account, peerId: peerId)
|
||||||
|
var cancelImpl: (() -> Void)?
|
||||||
|
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||||
|
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||||
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||||
|
cancelImpl?()
|
||||||
|
}))
|
||||||
|
presentControllerImpl?(controller, nil)
|
||||||
|
return ActionDisposable { [weak controller] in
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|> delay(0.15, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = progressSignal.start()
|
||||||
|
|
||||||
|
createSignal = createSignal
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelImpl = {
|
||||||
|
createSecretChatDisposable.set(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
createSecretChatDisposable.set((createSignal |> deliverOnMainQueue).start(next: { peerId in
|
||||||
if let navigationController = (controller?.navigationController as? NavigationController) {
|
if let navigationController = (controller?.navigationController as? NavigationController) {
|
||||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId))
|
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -201,6 +201,11 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate {
|
|||||||
if !self.ignoreZoom {
|
if !self.ignoreZoom {
|
||||||
self.centerScrollViewContents(transition: self.ignoreZoomTransition ?? .immediate)
|
self.centerScrollViewContents(transition: self.ignoreZoomTransition ?? .immediate)
|
||||||
}
|
}
|
||||||
|
if self.scrollNode.view.zoomScale.isEqual(to: self.scrollNode.view.minimumZoomScale) {
|
||||||
|
self.scrollNode.view.isScrollEnabled = false
|
||||||
|
} else {
|
||||||
|
self.scrollNode.view.isScrollEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func contentSize() -> CGSize? {
|
override func contentSize() -> CGSize? {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user