mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-24 09:06:30 +00:00
Fixed Apple Pay
Added ability to download music without streaming Added progress indicators for various blocking tasks Fixed image gallery swipe to dismiss after zooming Added online member count indication in supergroups Fixed contact statuses in contact search
This commit is contained in:
parent
d204d9f117
commit
fc8fa045a6
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 */; };
|
||||
D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.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 */; };
|
||||
D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -2291,6 +2295,7 @@
|
||||
D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */,
|
||||
D01776B41F1D6CCC0044446D /* RadialStatusContentNode.swift */,
|
||||
D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */,
|
||||
D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */,
|
||||
D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */,
|
||||
D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */,
|
||||
D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */,
|
||||
@ -2555,6 +2560,7 @@
|
||||
D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */,
|
||||
D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */,
|
||||
D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */,
|
||||
D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */,
|
||||
D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */,
|
||||
D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */,
|
||||
D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */,
|
||||
@ -5122,6 +5128,7 @@
|
||||
D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */,
|
||||
D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */,
|
||||
D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */,
|
||||
D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */,
|
||||
D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */,
|
||||
D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */,
|
||||
D0430B001FF4570500A35ADD /* WebController.swift in Sources */,
|
||||
@ -5330,6 +5337,7 @@
|
||||
D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */,
|
||||
D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */,
|
||||
D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */,
|
||||
D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */,
|
||||
D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */,
|
||||
D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */,
|
||||
D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */,
|
||||
|
||||
@ -696,6 +696,26 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
|
||||
case let .webToken(token):
|
||||
credentials = .generic(data: token.data, saveOnServer: token.saveOnServer)
|
||||
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 _ = (self.account.postbox.transaction({ transaction -> Peer? in
|
||||
return transaction.getPeer(botPeerId)
|
||||
@ -703,7 +723,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
|
||||
if let strongSelf = self, let botPeer = botPeer {
|
||||
let request = PKPaymentRequest()
|
||||
|
||||
request.merchantIdentifier = "merchant.ph.telegra.Telegraph"
|
||||
request.merchantIdentifier = merchantId
|
||||
request.supportedNetworks = [.visa, .amex, .masterCard]
|
||||
request.merchantCapabilities = [.capability3DS]
|
||||
request.countryCode = "US"
|
||||
@ -898,41 +918,50 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
|
||||
guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else {
|
||||
return
|
||||
}
|
||||
guard let publishableKey = nativeParams["publishable_key"] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
let signal: Signal<STPToken, Error> = Signal { subscriber in
|
||||
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
|
||||
configuration.smsAutofillDisabled = true
|
||||
configuration.publishableKey = publishableKey
|
||||
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
|
||||
if nativeProvider.name == "stripe" {
|
||||
guard let publishableKey = nativeParams["publishable_key"] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
let apiClient = STPAPIClient(configuration: configuration)
|
||||
|
||||
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)
|
||||
let signal: Signal<STPToken, Error> = Signal { subscriber in
|
||||
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
|
||||
configuration.smsAutofillDisabled = true
|
||||
configuration.publishableKey = publishableKey
|
||||
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
|
||||
|
||||
let apiClient = STPAPIClient(configuration: configuration)
|
||||
|
||||
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))
|
||||
} else {
|
||||
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))
|
||||
} else {
|
||||
completion(.failure)
|
||||
}
|
||||
}, error: { _ in
|
||||
completion(.failure)
|
||||
}))
|
||||
} else {
|
||||
self.applePayAuthrorizationCompletion = completion
|
||||
guard let paymentString = String(data: payment.token.paymentData, encoding: .utf8) else {
|
||||
return
|
||||
}
|
||||
}, error: { _ in
|
||||
completion(.failure)
|
||||
}))
|
||||
self.pay(liabilityNoticeAccepted: true, receivedCredentials: .applePay(data: paymentString))
|
||||
}
|
||||
}
|
||||
|
||||
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
|
||||
|
||||
@ -214,7 +214,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
|
||||
foundMembers = .single([])
|
||||
}
|
||||
|
||||
let foundContacts: Signal<[Peer], NoError>
|
||||
let foundContacts: Signal<([Peer], [PeerId: PeerPresence]), NoError>
|
||||
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError>
|
||||
switch mode {
|
||||
case .inviteActions, .banAndPromoteActions:
|
||||
@ -222,7 +222,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
|
||||
foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query)
|
||||
|> delay(0.2, queue: Queue.concurrentDefaultQueue()))
|
||||
case .searchMembers, .searchBanned, .searchAdmins:
|
||||
foundContacts = .single([])
|
||||
foundContacts = .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) {
|
||||
existingPeerIds.insert(peer.id)
|
||||
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 {
|
||||
strongSelf.resolvePeerByNameDisposable = MetaDisposable()
|
||||
}
|
||||
let resolveSignal: Signal<Peer?, NoError>
|
||||
var resolveSignal: Signal<Peer?, NoError>
|
||||
if let peerName = peerName {
|
||||
resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName)
|
||||
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
||||
@ -646,6 +646,32 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
|
||||
} else {
|
||||
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
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
if let strongSelf = self, !hashtag.isEmpty {
|
||||
@ -994,77 +1020,85 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
|
||||
case let .peer(peerId):
|
||||
if case let .peer(peerView) = self.chatLocationInfoData {
|
||||
peerView.set(account.viewTracker.peerView(peerId))
|
||||
self.peerDisposable.set((peerView.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerView in
|
||||
if let strongSelf = self {
|
||||
if let peer = peerViewMainPeer(peerView) {
|
||||
strongSelf.chatTitleView?.titleContent = .peer(peerView)
|
||||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
|
||||
}
|
||||
var wasGroupChannel: Bool?
|
||||
if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info {
|
||||
if case .group = info {
|
||||
wasGroupChannel = true
|
||||
} else {
|
||||
wasGroupChannel = false
|
||||
}
|
||||
}
|
||||
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))
|
||||
var onlineMemberCount: Signal<Int32?, NoError> = .single(nil)
|
||||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
onlineMemberCount = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recentOnline(postbox: account.postbox, network: account.network, peerId: peerId)
|
||||
|> map(Optional.init)
|
||||
}
|
||||
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount in
|
||||
if let strongSelf = self {
|
||||
if let peer = peerViewMainPeer(peerView) {
|
||||
strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount)
|
||||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
|
||||
}
|
||||
if strongSelf.peerView === peerView {
|
||||
return
|
||||
}
|
||||
var wasGroupChannel: Bool?
|
||||
if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info {
|
||||
if case .group = info {
|
||||
wasGroupChannel = true
|
||||
} else {
|
||||
wasGroupChannel = false
|
||||
}
|
||||
}
|
||||
}))
|
||||
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):
|
||||
if case let .group(topPeersView) = self.chatLocationInfoData {
|
||||
@ -4288,7 +4322,35 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
|
||||
disposable = MetaDisposable()
|
||||
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 peerId = peerId {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: nil))
|
||||
|
||||
@ -348,6 +348,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
self.panRecognizer = 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) {
|
||||
@ -1377,6 +1387,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
func dismissInput() {
|
||||
if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
||||
return
|
||||
}
|
||||
|
||||
switch self.chatPresentationInterfaceState.inputMode {
|
||||
case .none:
|
||||
break
|
||||
|
||||
@ -383,8 +383,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
|
||||
surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, 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.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)
|
||||
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()
|
||||
})
|
||||
|
||||
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()
|
||||
self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
|
||||
@ -371,8 +371,9 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
|
||||
}
|
||||
})*/
|
||||
|
||||
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated)
|
||||
strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
|
||||
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated, completion: { [weak self] in
|
||||
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) {
|
||||
|
||||
@ -25,6 +25,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
private var iconNode: TransformImageNode?
|
||||
private var statusNode: RadialStatusNode?
|
||||
private var streamingStatusNode: RadialStatusNode?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
private let statusDisposable = MetaDisposable()
|
||||
@ -35,11 +36,16 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
var activateLocalContent: () -> Void = { }
|
||||
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
||||
|
||||
private var account: Account?
|
||||
private var message: Message?
|
||||
private var themeAndStrings: (ChatPresentationThemeData, PresentationStrings)?
|
||||
private var file: TelegramMediaFile?
|
||||
private var progressFrame: CGRect?
|
||||
private var streamingCacheStatusFrame: CGRect?
|
||||
private var fileIconImage: UIImage?
|
||||
private var cloudFetchIconImage: UIImage?
|
||||
|
||||
override init() {
|
||||
self.titleNode = TextNode()
|
||||
@ -77,9 +83,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
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() {
|
||||
if let resourceStatus = self.resourceStatus {
|
||||
switch resourceStatus {
|
||||
switch resourceStatus.mediaStatus {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
if let account = self.account, let message = self.message, message.flags.isSending {
|
||||
let _ = account.postbox.transaction({ transaction -> Void in
|
||||
@ -109,7 +133,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
@objc func fileTap(_ recognizer: UITapGestureRecognizer) {
|
||||
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 currentTheme = self.themeAndStrings?.0
|
||||
let currentResourceStatus = self.resourceStatus
|
||||
|
||||
return { account, presentationData, message, file, automaticDownload, incoming, isRecentActions, dateAndStatusType, constrainedSize in
|
||||
var updatedTheme: ChatPresentationThemeData?
|
||||
@ -224,13 +253,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if case let .Audio(voice, duration, title, performer, waveform) = attribute {
|
||||
isAudio = true
|
||||
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
||||
updatedStatusSignal = currentUpdatedStatusSignal |> map { status in
|
||||
switch status {
|
||||
updatedStatusSignal = currentUpdatedStatusSignal
|
||||
|> map { status in
|
||||
switch status.mediaStatus {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
if !voice {
|
||||
return .fetchStatus(.Local)
|
||||
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
|
||||
} else {
|
||||
return .fetchStatus(fetchStatus)
|
||||
return FileMediaResourceStatus(mediaStatus: .fetchStatus(fetchStatus), fetchStatus: status.fetchStatus)
|
||||
}
|
||||
case .playbackStatus:
|
||||
return status
|
||||
@ -289,6 +319,23 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
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 (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)
|
||||
}
|
||||
|
||||
var cloudFetchIconImage: UIImage?
|
||||
if hasStreamingProgress {
|
||||
minLayoutWidth += streamingProgressDiameter + 4.0
|
||||
cloudFetchIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchOutgoingIcon(presentationData.theme.theme)
|
||||
}
|
||||
|
||||
let fileIconImage: UIImage?
|
||||
if hasThumbnail {
|
||||
fileIconImage = nil
|
||||
@ -325,6 +378,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
var iconFrame: CGRect?
|
||||
let progressFrame: CGRect
|
||||
let streamingCacheStatusFrame: CGRect
|
||||
let controlAreaWidth: CGFloat
|
||||
|
||||
if hasThumbnail {
|
||||
@ -362,7 +416,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0)
|
||||
} else {
|
||||
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?
|
||||
@ -376,6 +430,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
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
|
||||
if let strongSelf = self {
|
||||
strongSelf.account = account
|
||||
@ -468,78 +531,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
|
||||
displayLinkDispatcher.dispatch {
|
||||
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
|
||||
|
||||
if strongSelf.statusNode == nil {
|
||||
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
|
||||
if isAudio && !isVoice && previousHadCacheStatus != hasCacheStatus {
|
||||
strongSelf.requestUpdateLayout(false)
|
||||
} else {
|
||||
statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill
|
||||
}
|
||||
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()
|
||||
}
|
||||
})
|
||||
strongSelf.updateStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -551,6 +563,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
strongSelf.statusNode?.frame = progressFrame
|
||||
strongSelf.progressFrame = progressFrame
|
||||
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
||||
strongSelf.fileIconImage = fileIconImage
|
||||
strongSelf.cloudFetchIconImage = cloudFetchIconImage
|
||||
|
||||
if let updatedFetchControls = updatedFetchControls {
|
||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||
@ -558,6 +574,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
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))) {
|
||||
let currentAsyncLayout = node?.asyncLayout()
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
private let infoBackgroundNode: ASImageNode
|
||||
private let muteIconNode: ASImageNode
|
||||
|
||||
private var status: FileMediaResourceStatus?
|
||||
private var status: FileMediaResourceMediaStatus?
|
||||
private let playbackStatusDisposable = MetaDisposable()
|
||||
|
||||
private var shouldAcquireVideoContext: Bool {
|
||||
@ -178,16 +178,16 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
var updatedPlaybackStatus: Signal<FileMediaResourceStatus, NoError>?
|
||||
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))
|
||||
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
||||
if let pendingStatus = pendingStatus {
|
||||
var progress = pendingStatus.progress
|
||||
if pendingStatus.isRunning {
|
||||
progress = max(progress, 0.27)
|
||||
}
|
||||
return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress))
|
||||
} else {
|
||||
return resourceStatus
|
||||
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
||||
if let pendingStatus = pendingStatus {
|
||||
var progress = pendingStatus.progress
|
||||
if pendingStatus.isRunning {
|
||||
progress = max(progress, 0.27)
|
||||
}
|
||||
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 {
|
||||
return
|
||||
}
|
||||
strongSelf.status = status
|
||||
strongSelf.status = status.mediaStatus
|
||||
strongSelf.updateStatus()
|
||||
}))
|
||||
}
|
||||
|
||||
@ -238,6 +238,8 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
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)
|
||||
|
||||
@ -7,7 +7,7 @@ import SwiftSignalKit
|
||||
import LegacyComponents
|
||||
|
||||
enum ChatTitleContent {
|
||||
case peer(PeerView)
|
||||
case peer(peerView: PeerView, onlineMemberCount: Int32?)
|
||||
case group([Peer])
|
||||
}
|
||||
|
||||
@ -207,14 +207,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
self.insertSubview(statusNode.view, belowSubview: self.button.view)
|
||||
}
|
||||
switch self.networkState {
|
||||
case .waitingForNetwork:
|
||||
statusNode.title = self.strings.State_WaitingForNetwork
|
||||
case .connecting:
|
||||
statusNode.title = self.strings.State_Connecting
|
||||
case .updating:
|
||||
statusNode.title = self.strings.State_Updating
|
||||
case .online:
|
||||
break
|
||||
case .waitingForNetwork:
|
||||
statusNode.title = self.strings.State_WaitingForNetwork
|
||||
case .connecting:
|
||||
statusNode.title = self.strings.State_Connecting
|
||||
case .updating:
|
||||
statusNode.title = self.strings.State_Updating
|
||||
case .online:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,7 +246,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
var titleLeftIcon: ChatTitleIcon = .none
|
||||
var titleRightIcon: ChatTitleIcon = .none
|
||||
switch titleContent {
|
||||
case let .peer(peerView):
|
||||
case let .peer(peerView, _):
|
||||
if let peer = peerViewMainPeer(peerView) {
|
||||
if peerView.peerId == self.account.peerId {
|
||||
string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
@ -302,7 +302,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
var shouldUpdateLayout = false
|
||||
if let titleContent = self.titleContent {
|
||||
switch titleContent {
|
||||
case let .peer(peerView):
|
||||
case let .peer(peerView, onlineMemberCount):
|
||||
if let peer = peerViewMainPeer(peerView) {
|
||||
if peer.id == self.account.peerId {
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
@ -379,17 +379,27 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
|
||||
let membersString: String
|
||||
if case .group = channel.info {
|
||||
membersString = strings.Conversation_StatusMembers(memberCount)
|
||||
if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 {
|
||||
let string = NSMutableAttributedString()
|
||||
|
||||
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 {
|
||||
membersString = strings.Conversation_StatusSubscribers(memberCount)
|
||||
}
|
||||
|
||||
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
|
||||
let membersString: String
|
||||
if case .group = channel.info {
|
||||
membersString = strings.Conversation_StatusMembers(memberCount)
|
||||
} else {
|
||||
membersString = strings.Conversation_StatusSubscribers(memberCount)
|
||||
}
|
||||
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 {
|
||||
switch channel.info {
|
||||
|
||||
@ -620,7 +620,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
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
|
||||
var existingPeerIds = Set<PeerId>()
|
||||
var disabledPeerIds = Set<PeerId>()
|
||||
@ -628,17 +628,17 @@ final class ContactListNode: ASDisplayNode {
|
||||
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
||||
for filter in filters {
|
||||
switch filter {
|
||||
case .excludeSelf:
|
||||
existingPeerIds.insert(account.peerId)
|
||||
case let .exclude(peerIds):
|
||||
existingPeerIds = existingPeerIds.union(peerIds)
|
||||
case let .disable(peerIds):
|
||||
disabledPeerIds = disabledPeerIds.union(peerIds)
|
||||
case .excludeSelf:
|
||||
existingPeerIds.insert(account.peerId)
|
||||
case let .exclude(peerIds):
|
||||
existingPeerIds = existingPeerIds.union(peerIds)
|
||||
case let .disable(peerIds):
|
||||
disabledPeerIds = disabledPeerIds.union(peerIds)
|
||||
}
|
||||
}
|
||||
|
||||
var peers: [ContactListPeer] = []
|
||||
for peer in localPeers {
|
||||
for peer in localPeersAndStatuses.0 {
|
||||
if !existingPeerIds.contains(peer.id) {
|
||||
existingPeerIds.insert(peer.id)
|
||||
peers.append(.peer(peer: peer, isGlobal: false))
|
||||
@ -680,7 +680,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
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)
|
||||
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 {
|
||||
let index: Int
|
||||
let peer: ContactListPeer
|
||||
let presence: PeerPresence?
|
||||
let group: ContactListSearchGroup
|
||||
let enabled: Bool
|
||||
|
||||
@ -28,6 +29,13 @@ private struct ContactListSearchEntry: Identifiable, Comparable {
|
||||
if lhs.peer != rhs.peer {
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
@ -41,13 +49,17 @@ private struct ContactListSearchEntry: Identifiable, Comparable {
|
||||
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 status: ContactsPeerItemStatus
|
||||
switch self.group {
|
||||
case .contacts:
|
||||
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:
|
||||
header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil)
|
||||
if case let .peer(peer, _) = self.peer, let _ = peer.addressName {
|
||||
@ -80,12 +92,12 @@ struct ContactListSearchContainerTransition {
|
||||
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 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 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 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, timeFormat: timeFormat, openPeer: openPeer), directionHint: nil) }
|
||||
|
||||
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())
|
||||
|> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||
|> map { localPeers, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in
|
||||
|> map { localPeersAndPresences, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in
|
||||
var entries: [ContactListSearchEntry] = []
|
||||
var existingPeerIds = Set<PeerId>()
|
||||
var disabledPeerIds = Set<PeerId>()
|
||||
@ -186,7 +198,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
}
|
||||
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
||||
var index = 0
|
||||
for peer in localPeers {
|
||||
for peer in localPeersAndPresences.0 {
|
||||
if existingPeerIds.contains(peer.id) {
|
||||
continue
|
||||
}
|
||||
@ -195,7 +207,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
if onlyWriteable {
|
||||
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 {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
@ -214,7 +226,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
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 {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
@ -233,7 +245,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
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 {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
@ -249,7 +261,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -263,17 +275,17 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
let previousSearchItems = Atomic<[ContactListSearchEntry]>(value: [])
|
||||
|
||||
self.searchDisposable.set((searchItems
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
if let strongSelf = self {
|
||||
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)
|
||||
self?.openPeer(peer)
|
||||
})
|
||||
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
}))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
if let strongSelf = self {
|
||||
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, timeFormat: strongSelf.presentationData.dateTimeFormat, openPeer: { peer in self?.listNode.clearHighlightAnimated(true)
|
||||
self?.openPeer(peer)
|
||||
})
|
||||
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
}))
|
||||
|
||||
self.listNode.beganInteractiveDragging = { [weak self] in
|
||||
self?.dismissInput?()
|
||||
|
||||
@ -172,7 +172,7 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe
|
||||
context.requestedCompleteFetch = false
|
||||
} else {
|
||||
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 {
|
||||
context.requestedCompleteFetch = true
|
||||
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)
|
||||
|
||||
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 {
|
||||
self.requestedCompleteFetch = true
|
||||
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 {
|
||||
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))
|
||||
|
||||
@ -40,6 +40,35 @@ final class FFMpegMediaFrameSourceContextHelpers {
|
||||
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? {
|
||||
let par = NSMutableDictionary()
|
||||
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
|
||||
|
||||
@ -208,6 +208,7 @@ public func fetchVideoLibraryMediaResourceHash(resource: VideoLibraryMediaResour
|
||||
if fetchResult.count != 0 {
|
||||
let asset = fetchResult.object(at: 0)
|
||||
let option = PHVideoRequestOptions()
|
||||
option.isNetworkAccessAllowed = true
|
||||
option.deliveryMode = .highQualityFormat
|
||||
|
||||
let alreadyReceivedAsset = Atomic<Bool>(value: false)
|
||||
|
||||
@ -8,7 +8,12 @@ enum FileMediaResourcePlaybackStatus {
|
||||
case paused
|
||||
}
|
||||
|
||||
enum FileMediaResourceStatus {
|
||||
struct FileMediaResourceStatus {
|
||||
let mediaStatus: FileMediaResourceMediaStatus
|
||||
let fetchStatus: MediaResourceStatus
|
||||
}
|
||||
|
||||
enum FileMediaResourceMediaStatus {
|
||||
case fetchStatus(MediaResourceStatus)
|
||||
case playbackStatus(FileMediaResourcePlaybackStatus)
|
||||
}
|
||||
@ -50,45 +55,49 @@ func messageFileMediaResourceStatus(account: Account, file: TelegramMediaFile, m
|
||||
|
||||
if message.flags.isSending {
|
||||
return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), account.pendingMessageManager.pendingMessageStatus(message.id), playbackStatus)
|
||||
|> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in
|
||||
if let playbackStatus = playbackStatus {
|
||||
switch playbackStatus {
|
||||
case .playing:
|
||||
return .playbackStatus(.playing)
|
||||
case .paused:
|
||||
return .playbackStatus(.paused)
|
||||
case let .buffering(_, whilePlaying):
|
||||
if whilePlaying {
|
||||
return .playbackStatus(.playing)
|
||||
} else {
|
||||
return .playbackStatus(.paused)
|
||||
}
|
||||
}
|
||||
} else if let pendingStatus = pendingStatus {
|
||||
return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress))
|
||||
} else {
|
||||
return .fetchStatus(resourceStatus)
|
||||
|> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in
|
||||
let mediaStatus: FileMediaResourceMediaStatus
|
||||
if let playbackStatus = playbackStatus {
|
||||
switch playbackStatus {
|
||||
case .playing:
|
||||
mediaStatus = .playbackStatus(.playing)
|
||||
case .paused:
|
||||
mediaStatus = .playbackStatus(.paused)
|
||||
case let .buffering(_, whilePlaying):
|
||||
if whilePlaying {
|
||||
mediaStatus = .playbackStatus(.playing)
|
||||
} else {
|
||||
mediaStatus = .playbackStatus(.paused)
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), playbackStatus)
|
||||
|> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in
|
||||
if let playbackStatus = playbackStatus {
|
||||
switch playbackStatus {
|
||||
case .playing:
|
||||
return .playbackStatus(.playing)
|
||||
case .paused:
|
||||
return .playbackStatus(.paused)
|
||||
case let .buffering(_, whilePlaying):
|
||||
if whilePlaying {
|
||||
return .playbackStatus(.playing)
|
||||
} else {
|
||||
return .playbackStatus(.paused)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .fetchStatus(resourceStatus)
|
||||
|> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in
|
||||
let mediaStatus: FileMediaResourceMediaStatus
|
||||
if let playbackStatus = playbackStatus {
|
||||
switch playbackStatus {
|
||||
case .playing:
|
||||
mediaStatus = .playbackStatus(.playing)
|
||||
case .paused:
|
||||
mediaStatus = .playbackStatus(.paused)
|
||||
case let .buffering(_, whilePlaying):
|
||||
if whilePlaying {
|
||||
mediaStatus = .playbackStatus(.playing)
|
||||
} else {
|
||||
mediaStatus = .playbackStatus(.paused)
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
let content: UniversalVideoContent
|
||||
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 {
|
||||
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)
|
||||
} else {
|
||||
|
||||
@ -38,7 +38,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0
|
||||
self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -157,7 +157,7 @@ final class LegacyStickerImageDataSource: TGImageDataSource {
|
||||
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 {
|
||||
sharedImageCache.setImage(image, forKey: uri, attributes: nil)
|
||||
completion?(TGDataResource(image: image, decoded: true))
|
||||
|
||||
@ -92,6 +92,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect,
|
||||
legacyController.bind(controller: baseController)
|
||||
legacyController.presentationCompleted = { [weak legacyController, weak baseController] in
|
||||
if let legacyController = legacyController, let baseController = baseController {
|
||||
legacyController.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
let inputPanelTheme = theme.chat.inputPanel
|
||||
var uploadInterface: LegacyLiveUploadInterface?
|
||||
if peerId.namespace != Namespaces.Peer.SecretChat {
|
||||
|
||||
@ -154,7 +154,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
private let statusDisposable = MetaDisposable()
|
||||
private let fetchControls = Atomic<FetchControls?>(value: nil)
|
||||
private var resourceStatus: FileMediaResourceStatus?
|
||||
private var resourceStatus: FileMediaResourceMediaStatus?
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
private var downloadStatusIconNode: ASImageNode
|
||||
@ -396,10 +396,11 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
if isAudio {
|
||||
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
||||
updatedStatusSignal = currentUpdatedStatusSignal |> map { status in
|
||||
switch status {
|
||||
updatedStatusSignal = currentUpdatedStatusSignal
|
||||
|> map { status in
|
||||
switch status.mediaStatus {
|
||||
case .fetchStatus:
|
||||
return .fetchStatus(.Local)
|
||||
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
|
||||
case .playbackStatus:
|
||||
return status
|
||||
}
|
||||
@ -571,7 +572,9 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
|
||||
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 {
|
||||
if let strongSelf = strongSelf {
|
||||
strongSelf.resourceStatus = status
|
||||
|
||||
@ -9,7 +9,7 @@ public enum NavigateToChatKeepStack {
|
||||
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 isFirst = true
|
||||
for controller in navigationController.viewControllers.reversed() {
|
||||
@ -24,6 +24,7 @@ public func navigateToChatController(navigationController: NavigationController,
|
||||
} else {
|
||||
let _ = navigationController.popToViewController(controller, animated: animated)
|
||||
}
|
||||
completion()
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@ -48,9 +49,9 @@ public func navigateToChatController(navigationController: NavigationController,
|
||||
resolvedKeepStack = false
|
||||
}
|
||||
if resolvedKeepStack {
|
||||
navigationController.pushViewController(controller)
|
||||
navigationController.pushViewController(controller, completion: completion)
|
||||
} else {
|
||||
navigationController.replaceAllButRootController(controller, animated: animated)
|
||||
navigationController.replaceAllButRootController(controller, animated: animated, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +137,47 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}, 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 {
|
||||
var convertedUrl: String?
|
||||
if parsedUrl.host == "localpeer" {
|
||||
@ -418,62 +459,29 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
|
||||
}
|
||||
|
||||
if let convertedUrl = convertedUrl {
|
||||
let _ = (resolveUrl(account: account, url: 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)
|
||||
}
|
||||
}
|
||||
}, 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()
|
||||
})
|
||||
}
|
||||
})
|
||||
handleInternalUrl(convertedUrl)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
|
||||
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)
|
||||
}
|
||||
if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" {
|
||||
handleInternalUrl(parsedUrl.absoluteString)
|
||||
} 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 {
|
||||
applicationContext.applicationBindings.openUrl(url)
|
||||
@ -481,11 +489,16 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
|
||||
}
|
||||
|
||||
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
|
||||
applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in
|
||||
if !success {
|
||||
continueHandling()
|
||||
}
|
||||
}))
|
||||
let nativeHosts = ["t.me", "telegram.me"]
|
||||
if let host = parsedUrl.host, nativeHosts.contains(host) {
|
||||
continueHandling()
|
||||
} else {
|
||||
applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in
|
||||
if !success {
|
||||
continueHandling()
|
||||
}
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
continueHandling()
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import Display
|
||||
import LegacyComponents
|
||||
|
||||
enum OverlayStatusControllerType {
|
||||
case loading
|
||||
case loading(cancelled: (() -> Void)?)
|
||||
case success
|
||||
case proxySettingSuccess
|
||||
}
|
||||
@ -47,12 +47,14 @@ private enum OverlayStatusContentController {
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
func dismiss(completion: @escaping () -> Void) {
|
||||
switch self {
|
||||
case let .loading(controller):
|
||||
controller.dismiss(true) {}
|
||||
controller.dismiss(true, completion: {
|
||||
completion()
|
||||
})
|
||||
default:
|
||||
break
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,8 +66,12 @@ private final class OverlayStatusControllerNode: ViewControllerTracingNode {
|
||||
init(theme: PresentationTheme, type: OverlayStatusControllerType, dismissed: @escaping () -> Void) {
|
||||
self.dismissed = dismissed
|
||||
switch type {
|
||||
case .loading:
|
||||
self.contentController = .loading(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light))
|
||||
case let .loading(cancelled):
|
||||
let controller = TGProgressWindowController(light: theme.actionSheet.backgroundType == .light)!
|
||||
controller.cancelled = {
|
||||
cancelled?()
|
||||
}
|
||||
self.contentController = .loading(controller)
|
||||
case .success:
|
||||
self.contentController = .progress(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light))
|
||||
case .proxySettingSuccess:
|
||||
@ -92,8 +98,9 @@ private final class OverlayStatusControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
self.contentController.dismiss()
|
||||
self.dismissed()
|
||||
self.contentController.dismiss(completion: { [weak self] in
|
||||
self?.dismissed()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -102,6 +102,56 @@ final class PeerChannelMemberCategoriesContextsManager {
|
||||
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?) {
|
||||
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 chatBubbleActionButtonOutgoingBottomSingleImage
|
||||
|
||||
case chatBubbleFileCloudFetchIncomingIcon
|
||||
case chatBubbleFileCloudFetchOutgoingIcon
|
||||
|
||||
case chatBubbleReplyThumbnailPlayImage
|
||||
|
||||
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 pause(UIColor)
|
||||
case progress(color: UIColor, lineWidth: CGFloat?, value: CGFloat?, cancelEnabled: Bool)
|
||||
case cloudProgress(color: UIColor, strokeBackgroundColor: UIColor, lineWidth: CGFloat, value: CGFloat?)
|
||||
case check(UIColor)
|
||||
case customIcon(UIImage)
|
||||
case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double)
|
||||
@ -43,6 +44,12 @@ public enum RadialStatusNodeState: Equatable {
|
||||
} else {
|
||||
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):
|
||||
if case let .check(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
|
||||
return true
|
||||
@ -99,6 +106,18 @@ public enum RadialStatusNodeState: Equatable {
|
||||
node.progress = value
|
||||
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):
|
||||
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> {
|
||||
var resource: MediaResource?
|
||||
var isImage = true
|
||||
var fileExtension: String?
|
||||
if let image = mediaReference.media as? TelegramMediaImage {
|
||||
if let representation = largestImageRepresentation(image.representations) {
|
||||
resource = representation.resource
|
||||
}
|
||||
} else if let file = mediaReference.media as? TelegramMediaFile {
|
||||
resource = file.resource
|
||||
if file.isVideo {
|
||||
if file.isVideo || file.mimeType.hasPrefix("video/") {
|
||||
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 {
|
||||
if let file = content.file {
|
||||
resource = file.resource
|
||||
@ -34,7 +39,7 @@ func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: P
|
||||
if let resource = resource {
|
||||
let fetchedData: Signal<MediaResourceData, NoError> = Signal { subscriber in
|
||||
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)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
|
||||
@ -443,7 +443,7 @@ public func settingsController(account: Account, accountManager: AccountManager)
|
||||
let archivedPacks = Promise<[ArchivedStickerPackItem]?>()
|
||||
|
||||
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)
|
||||
let _ = (resolvedUrl.get()
|
||||
|> take(1)
|
||||
|
||||
@ -1251,7 +1251,35 @@ public func userInfoController(account: Account, peerId: PeerId, mode: UserInfoC
|
||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(currentPeerId))
|
||||
}
|
||||
} 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) {
|
||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId))
|
||||
}
|
||||
|
||||
@ -201,6 +201,11 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate {
|
||||
if !self.ignoreZoom {
|
||||
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? {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user