mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 03:09:56 +00:00
Merge branch 'master' of github.com:peter-iakovlev/TelegramUI
# Conflicts: # TelegramUI/ChatTitleView.swift # TelegramUI/OpenUrl.swift
This commit is contained in:
commit
454146cb80
12
Images.xcassets/Chat/Message/FileCloudFetch.imageset/Contents.json
vendored
Normal file
12
Images.xcassets/Chat/Message/FileCloudFetch.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "cloud.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/FileCloudFetch.imageset/cloud.pdf
vendored
Normal file
BIN
Images.xcassets/Chat/Message/FileCloudFetch.imageset/cloud.pdf
vendored
Normal file
Binary file not shown.
@ -128,6 +128,8 @@
|
||||
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383EDB207D1A1600C45548 /* emoji_suggestions.h */; };
|
||||
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 {
|
||||
@ -4305,7 +4339,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 {
|
||||
strongSelf.openResolved(.peer(peerId, .default))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -372,8 +372,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])
|
||||
}
|
||||
|
||||
@ -184,13 +184,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout) {
|
||||
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) {
|
||||
var isOnline = false
|
||||
if case .online = networkState {
|
||||
isOnline = true
|
||||
}
|
||||
|
||||
if isOnline || layout.metrics.widthClass == .regular {
|
||||
if isOnline || layout?.metrics.widthClass == .regular {
|
||||
self.contentContainer.isHidden = false
|
||||
if let networkStatusNode = self.networkStatusNode {
|
||||
self.networkStatusNode = nil
|
||||
@ -210,7 +210,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
case .waitingForNetwork:
|
||||
statusNode.title = self.strings.State_WaitingForNetwork
|
||||
case let .connecting(proxy):
|
||||
if proxy != nil && layout.size.width > 320.0 {
|
||||
if let layout = layout, proxy != nil && layout.size.width > 320.0 {
|
||||
statusNode.title = self.strings.State_ConnectingToProxy
|
||||
} else {
|
||||
statusNode.title = self.strings.State_Connecting
|
||||
@ -233,7 +233,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
}
|
||||
}
|
||||
|
||||
var layout: ContainerViewLayout = ContainerViewLayout()() {
|
||||
var layout: ContainerViewLayout? {
|
||||
didSet {
|
||||
if self.layout != oldValue {
|
||||
updateNetworkStatusNode(networkState: self.networkState, layout: self.layout)
|
||||
@ -250,7 +250,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)
|
||||
@ -306,7 +306,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)
|
||||
@ -383,17 +383,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,49 @@ 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)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, present: { c, a in
|
||||
if let navigationController = navigationController {
|
||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
(navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
}, dismissInput: {
|
||||
dismissInput()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if parsedUrl.scheme == "tg", let query = parsedUrl.query {
|
||||
var convertedUrl: String?
|
||||
if parsedUrl.host == "localpeer" {
|
||||
@ -418,64 +461,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)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, present: { c, a in
|
||||
if let navigationController = navigationController {
|
||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
(navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
}, dismissInput: {
|
||||
dismissInput()
|
||||
})
|
||||
}
|
||||
})
|
||||
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)
|
||||
@ -483,11 +491,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