Fixed Apple Pay

Added ability to download music without streaming
Added progress indicators for various blocking tasks
Fixed image gallery swipe to dismiss after zooming
Added online member count indication in supergroups
Fixed contact statuses in contact search
This commit is contained in:
Peter 2018-10-13 03:31:39 +03:00
parent d204d9f117
commit fc8fa045a6
38 changed files with 1534 additions and 365 deletions

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "cloud.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

View File

@ -128,6 +128,8 @@
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383EDB207D1A1600C45548 /* emoji_suggestions.h */; }; D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383EDB207D1A1600C45548 /* emoji_suggestions.h */; };
D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */; }; D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */; };
D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */; }; D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */; };
D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */; };
D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */; };
D03AA4DF202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */; }; D03AA4DF202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */; };
D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */; }; D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */; };
D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */; }; D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */; };
@ -1257,6 +1259,8 @@
D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = "<group>"; }; D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = "<group>"; };
D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = "<group>"; }; D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = "<group>"; };
D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingCancelIndicator.swift; sourceTree = "<group>"; }; D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingCancelIndicator.swift; sourceTree = "<group>"; };
D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialCloudProgressContentNode.swift; sourceTree = "<group>"; };
D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformVideoContent.swift; sourceTree = "<group>"; };
D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatContextResultPeekContentNode.swift; sourceTree = "<group>"; }; D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatContextResultPeekContentNode.swift; sourceTree = "<group>"; };
D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewPeekContent.swift; sourceTree = "<group>"; }; D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewPeekContent.swift; sourceTree = "<group>"; };
D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListEditableReorderControlNode.swift; sourceTree = "<group>"; }; D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListEditableReorderControlNode.swift; sourceTree = "<group>"; };
@ -2291,6 +2295,7 @@
D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */, D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */,
D01776B41F1D6CCC0044446D /* RadialStatusContentNode.swift */, D01776B41F1D6CCC0044446D /* RadialStatusContentNode.swift */,
D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */, D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */,
D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */,
D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */, D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */,
D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */, D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */,
D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */, D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */,
@ -2555,6 +2560,7 @@
D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */, D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */,
D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */, D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */,
D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */, D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */,
D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */,
D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */, D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */,
D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */, D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */,
D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */, D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */,
@ -5122,6 +5128,7 @@
D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */, D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */,
D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */, D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */,
D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */, D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */,
D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */,
D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */, D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */,
D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */, D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */,
D0430B001FF4570500A35ADD /* WebController.swift in Sources */, D0430B001FF4570500A35ADD /* WebController.swift in Sources */,
@ -5330,6 +5337,7 @@
D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */, D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */,
D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */, D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */,
D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */, D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */,
D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */,
D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */, D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */,
D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */, D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */,
D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */, D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */,

View File

@ -696,6 +696,26 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
case let .webToken(token): case let .webToken(token):
credentials = .generic(data: token.data, saveOnServer: token.saveOnServer) credentials = .generic(data: token.data, saveOnServer: token.saveOnServer)
case .applePayStripe: case .applePayStripe:
guard let paymentForm = self.paymentFormValue, let nativeProvider = paymentForm.nativeProvider else {
return
}
//NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[strongSelf->_paymentForm.nativeParams dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
guard let nativeParamsData = nativeProvider.params.data(using: .utf8) else {
return
}
guard let nativeParams = (try? JSONSerialization.jsonObject(with: nativeParamsData, options: [])) as? [String: Any] else {
return
}
let merchantId: String
if nativeProvider.name == "stripe" {
merchantId = "merchant.ph.telegra.Telegraph"
} else if let paramsId = nativeParams["apple_pay_merchant_id"] as? String {
merchantId = paramsId
} else {
return
}
let botPeerId = self.messageId.peerId let botPeerId = self.messageId.peerId
let _ = (self.account.postbox.transaction({ transaction -> Peer? in let _ = (self.account.postbox.transaction({ transaction -> Peer? in
return transaction.getPeer(botPeerId) return transaction.getPeer(botPeerId)
@ -703,7 +723,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
if let strongSelf = self, let botPeer = botPeer { if let strongSelf = self, let botPeer = botPeer {
let request = PKPaymentRequest() let request = PKPaymentRequest()
request.merchantIdentifier = "merchant.ph.telegra.Telegraph" request.merchantIdentifier = merchantId
request.supportedNetworks = [.visa, .amex, .masterCard] request.supportedNetworks = [.visa, .amex, .masterCard]
request.merchantCapabilities = [.capability3DS] request.merchantCapabilities = [.capability3DS]
request.countryCode = "US" request.countryCode = "US"
@ -898,41 +918,50 @@ final class BotCheckoutControllerNode: ItemListControllerNode<BotCheckoutEntry>,
guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else {
return return
} }
guard let publishableKey = nativeParams["publishable_key"] as? String else {
return
}
let signal: Signal<STPToken, Error> = Signal { subscriber in if nativeProvider.name == "stripe" {
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration guard let publishableKey = nativeParams["publishable_key"] as? String else {
configuration.smsAutofillDisabled = true return
configuration.publishableKey = publishableKey }
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
let apiClient = STPAPIClient(configuration: configuration) let signal: Signal<STPToken, Error> = Signal { subscriber in
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
apiClient.createToken(with: payment, completion: { token, error in configuration.smsAutofillDisabled = true
if let token = token { configuration.publishableKey = publishableKey
subscriber.putNext(token) configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
subscriber.putCompletion()
} else if let error = error { let apiClient = STPAPIClient(configuration: configuration)
subscriber.putError(error)
apiClient.createToken(with: payment, completion: { token, error in
if let token = token {
subscriber.putNext(token)
subscriber.putCompletion()
} else if let error = error {
subscriber.putError(error)
}
})
return ActionDisposable {
} }
}) }
return ActionDisposable { self.paymentAuthDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] token in
} if let strongSelf = self {
} strongSelf.applePayAuthrorizationCompletion = completion
strongSelf.pay(liabilityNoticeAccepted: true, receivedCredentials: .generic(data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: false))
self.paymentAuthDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] token in } else {
if let strongSelf = self { completion(.failure)
strongSelf.applePayAuthrorizationCompletion = completion }
strongSelf.pay(liabilityNoticeAccepted: true, receivedCredentials: .generic(data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: false)) }, error: { _ in
} else {
completion(.failure) completion(.failure)
}))
} else {
self.applePayAuthrorizationCompletion = completion
guard let paymentString = String(data: payment.token.paymentData, encoding: .utf8) else {
return
} }
}, error: { _ in self.pay(liabilityNoticeAccepted: true, receivedCredentials: .applePay(data: paymentString))
completion(.failure) }
}))
} }
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) { func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {

View File

@ -214,7 +214,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
foundMembers = .single([]) foundMembers = .single([])
} }
let foundContacts: Signal<[Peer], NoError> let foundContacts: Signal<([Peer], [PeerId: PeerPresence]), NoError>
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError>
switch mode { switch mode {
case .inviteActions, .banAndPromoteActions: case .inviteActions, .banAndPromoteActions:
@ -222,7 +222,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query) foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query)
|> delay(0.2, queue: Queue.concurrentDefaultQueue())) |> delay(0.2, queue: Queue.concurrentDefaultQueue()))
case .searchMembers, .searchBanned, .searchAdmins: case .searchMembers, .searchBanned, .searchAdmins:
foundContacts = .single([]) foundContacts = .single(([], [:]))
foundRemotePeers = .single(([], [])) foundRemotePeers = .single(([], []))
} }
@ -322,7 +322,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
} }
} }
for peer in foundContacts { for peer in foundContacts.0 {
if !existingPeerIds.contains(peer.id) { if !existingPeerIds.contains(peer.id) {
existingPeerIds.insert(peer.id) existingPeerIds.insert(peer.id)
entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts)) entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts))

View File

@ -629,7 +629,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
if strongSelf.resolvePeerByNameDisposable == nil { if strongSelf.resolvePeerByNameDisposable == nil {
strongSelf.resolvePeerByNameDisposable = MetaDisposable() strongSelf.resolvePeerByNameDisposable = MetaDisposable()
} }
let resolveSignal: Signal<Peer?, NoError> var resolveSignal: Signal<Peer?, NoError>
if let peerName = peerName { if let peerName = peerName {
resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName) resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName)
|> mapToSignal { peerId -> Signal<Peer?, NoError> in |> mapToSignal { peerId -> Signal<Peer?, NoError> in
@ -646,6 +646,32 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
} else { } else {
resolveSignal = .single(nil) resolveSignal = .single(nil)
} }
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.presentationData
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
resolveSignal = resolveSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
self?.resolvePeerByNameDisposable?.set(nil)
}
strongSelf.resolvePeerByNameDisposable?.set((resolveSignal strongSelf.resolvePeerByNameDisposable?.set((resolveSignal
|> deliverOnMainQueue).start(next: { peer in |> deliverOnMainQueue).start(next: { peer in
if let strongSelf = self, !hashtag.isEmpty { if let strongSelf = self, !hashtag.isEmpty {
@ -994,77 +1020,85 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
case let .peer(peerId): case let .peer(peerId):
if case let .peer(peerView) = self.chatLocationInfoData { if case let .peer(peerView) = self.chatLocationInfoData {
peerView.set(account.viewTracker.peerView(peerId)) peerView.set(account.viewTracker.peerView(peerId))
self.peerDisposable.set((peerView.get() var onlineMemberCount: Signal<Int32?, NoError> = .single(nil)
|> deliverOnMainQueue).start(next: { [weak self] peerView in if peerId.namespace == Namespaces.Peer.CloudChannel {
if let strongSelf = self { onlineMemberCount = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recentOnline(postbox: account.postbox, network: account.network, peerId: peerId)
if let peer = peerViewMainPeer(peerView) { |> map(Optional.init)
strongSelf.chatTitleView?.titleContent = .peer(peerView) }
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount)
} |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount in
var wasGroupChannel: Bool? if let strongSelf = self {
if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info { if let peer = peerViewMainPeer(peerView) {
if case .group = info { strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount)
wasGroupChannel = true (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
} else { }
wasGroupChannel = false if strongSelf.peerView === peerView {
} return
} }
var isGroupChannel: Bool? var wasGroupChannel: Bool?
if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info {
if case .group = info { if case .group = info {
isGroupChannel = true wasGroupChannel = true
} else { } else {
isGroupChannel = false wasGroupChannel = false
}
}
strongSelf.peerView = peerView
if wasGroupChannel != isGroupChannel {
if let isGroupChannel = isGroupChannel, isGroupChannel {
let (recentDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in })
let (adminsDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in })
let disposable = DisposableSet()
disposable.add(recentDisposable)
disposable.add(adminsDisposable)
strongSelf.chatAdditionalDataDisposable.set(disposable)
} else {
strongSelf.chatAdditionalDataDisposable.set(nil)
}
}
if strongSelf.isNodeLoaded {
strongSelf.chatDisplayNode.peerView = peerView
}
var peerIsMuted = false
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
if case .muted = notificationSettings.muteState {
peerIsMuted = true
}
}
var renderedPeer: RenderedPeer?
var isContact: Bool = false
if let peer = peerView.peers[peerView.peerId] {
isContact = peerView.peerIsContact
var peers = SimpleDictionary<PeerId, Peer>()
peers[peer.id] = peer
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
peers[associatedPeer.id] = associatedPeer
}
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers)
}
var animated = false
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState {
animated = true
}
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
return $0.updatedPeer { _ in return renderedPeer
}.updatedIsContact(isContact).updatedPeerIsMuted(peerIsMuted)
})
if !strongSelf.didSetChatLocationInfoReady {
strongSelf.didSetChatLocationInfoReady = true
strongSelf._chatLocationInfoReady.set(.single(true))
} }
} }
})) var isGroupChannel: Bool?
if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info {
if case .group = info {
isGroupChannel = true
} else {
isGroupChannel = false
}
}
strongSelf.peerView = peerView
if wasGroupChannel != isGroupChannel {
if let isGroupChannel = isGroupChannel, isGroupChannel {
let (recentDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in })
let (adminsDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in })
let disposable = DisposableSet()
disposable.add(recentDisposable)
disposable.add(adminsDisposable)
strongSelf.chatAdditionalDataDisposable.set(disposable)
} else {
strongSelf.chatAdditionalDataDisposable.set(nil)
}
}
if strongSelf.isNodeLoaded {
strongSelf.chatDisplayNode.peerView = peerView
}
var peerIsMuted = false
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
if case .muted = notificationSettings.muteState {
peerIsMuted = true
}
}
var renderedPeer: RenderedPeer?
var isContact: Bool = false
if let peer = peerView.peers[peerView.peerId] {
isContact = peerView.peerIsContact
var peers = SimpleDictionary<PeerId, Peer>()
peers[peer.id] = peer
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
peers[associatedPeer.id] = associatedPeer
}
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers)
}
var animated = false
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState {
animated = true
}
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
return $0.updatedPeer { _ in return renderedPeer
}.updatedIsContact(isContact).updatedPeerIsMuted(peerIsMuted)
})
if !strongSelf.didSetChatLocationInfoReady {
strongSelf.didSetChatLocationInfoReady = true
strongSelf._chatLocationInfoReady.set(.single(true))
}
}
}))
} }
case let .group(groupId): case let .group(groupId):
if case let .group(topPeersView) = self.chatLocationInfoData { if case let .group(topPeersView) = self.chatLocationInfoData {
@ -4288,7 +4322,35 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
disposable = MetaDisposable() disposable = MetaDisposable()
self.resolvePeerByNameDisposable = disposable self.resolvePeerByNameDisposable = disposable
} }
disposable.set((resolvePeerByName(account: self.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in var resolveSignal = resolvePeerByName(account: self.account, name: name, ageLimit: 10)
var cancelImpl: (() -> Void)?
let presentationData = self.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
resolveSignal = resolveSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = { [weak self] in
self?.resolvePeerByNameDisposable?.set(nil)
}
disposable.set((resolveSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in
if let strongSelf = self { if let strongSelf = self {
if let peerId = peerId { if let peerId = peerId {
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: nil)) (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: nil))

View File

@ -348,6 +348,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
} }
self.panRecognizer = recognizer self.panRecognizer = recognizer
self.view.addGestureRecognizer(recognizer) self.view.addGestureRecognizer(recognizer)
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
guard let strongSelf = self else {
return false
}
if let _ = strongSelf.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
return true
}
return false
}
} }
private func updateIsEmpty(_ isEmpty: Bool, animated: Bool) { private func updateIsEmpty(_ isEmpty: Bool, animated: Bool) {
@ -1377,6 +1387,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
} }
func dismissInput() { func dismissInput() {
if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
return
}
switch self.chatPresentationInterfaceState.inputMode { switch self.chatPresentationInterfaceState.inputMode {
case .none: case .none:
break break

View File

@ -383,8 +383,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
} }
} }
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false)
surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.025, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
@ -404,7 +404,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
intermediateCompletion() intermediateCompletion()
}) })
self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false)
transformedFrame.origin = CGPoint() transformedFrame.origin = CGPoint()
self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in

View File

@ -371,8 +371,9 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
} }
})*/ })*/
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated) navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated, completion: { [weak self] in
strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) self?.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
})
} }
} }
} }

View File

@ -22,6 +22,14 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
} }
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
if let strongSelf = self {
if let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
}
}
}
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {

View File

@ -25,6 +25,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var iconNode: TransformImageNode? private var iconNode: TransformImageNode?
private var statusNode: RadialStatusNode? private var statusNode: RadialStatusNode?
private var streamingStatusNode: RadialStatusNode?
private var tapRecognizer: UITapGestureRecognizer? private var tapRecognizer: UITapGestureRecognizer?
private let statusDisposable = MetaDisposable() private let statusDisposable = MetaDisposable()
@ -35,11 +36,16 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
var activateLocalContent: () -> Void = { } var activateLocalContent: () -> Void = { }
var requestUpdateLayout: (Bool) -> Void = { _ in }
private var account: Account? private var account: Account?
private var message: Message? private var message: Message?
private var themeAndStrings: (ChatPresentationThemeData, PresentationStrings)? private var themeAndStrings: (ChatPresentationThemeData, PresentationStrings)?
private var file: TelegramMediaFile? private var file: TelegramMediaFile?
private var progressFrame: CGRect?
private var streamingCacheStatusFrame: CGRect?
private var fileIconImage: UIImage?
private var cloudFetchIconImage: UIImage?
override init() { override init() {
self.titleNode = TextNode() self.titleNode = TextNode()
@ -77,9 +83,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.tapRecognizer = tapRecognizer self.tapRecognizer = tapRecognizer
} }
@objc func cacheProgressPressed() {
guard let resourceStatus = self.resourceStatus else {
return
}
switch resourceStatus.fetchStatus {
case .Fetching:
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
cancel()
}
case .Remote:
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
fetch()
}
case .Local:
break
}
}
@objc func progressPressed() { @objc func progressPressed() {
if let resourceStatus = self.resourceStatus { if let resourceStatus = self.resourceStatus {
switch resourceStatus { switch resourceStatus.mediaStatus {
case let .fetchStatus(fetchStatus): case let .fetchStatus(fetchStatus):
if let account = self.account, let message = self.message, message.flags.isSending { if let account = self.account, let message = self.message, message.flags.isSending {
let _ = account.postbox.transaction({ transaction -> Void in let _ = account.postbox.transaction({ transaction -> Void in
@ -109,7 +133,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
@objc func fileTap(_ recognizer: UITapGestureRecognizer) { @objc func fileTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state { if case .ended = recognizer.state {
self.progressPressed() if let streamingCacheStatusFrame = self.streamingCacheStatusFrame, streamingCacheStatusFrame.contains(recognizer.location(in: self.view)) {
self.cacheProgressPressed()
} else {
self.progressPressed()
}
} }
} }
@ -122,6 +150,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let currentMessage = self.message let currentMessage = self.message
let currentTheme = self.themeAndStrings?.0 let currentTheme = self.themeAndStrings?.0
let currentResourceStatus = self.resourceStatus
return { account, presentationData, message, file, automaticDownload, incoming, isRecentActions, dateAndStatusType, constrainedSize in return { account, presentationData, message, file, automaticDownload, incoming, isRecentActions, dateAndStatusType, constrainedSize in
var updatedTheme: ChatPresentationThemeData? var updatedTheme: ChatPresentationThemeData?
@ -224,13 +253,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
if case let .Audio(voice, duration, title, performer, waveform) = attribute { if case let .Audio(voice, duration, title, performer, waveform) = attribute {
isAudio = true isAudio = true
if let currentUpdatedStatusSignal = updatedStatusSignal { if let currentUpdatedStatusSignal = updatedStatusSignal {
updatedStatusSignal = currentUpdatedStatusSignal |> map { status in updatedStatusSignal = currentUpdatedStatusSignal
switch status { |> map { status in
switch status.mediaStatus {
case let .fetchStatus(fetchStatus): case let .fetchStatus(fetchStatus):
if !voice { if !voice {
return .fetchStatus(.Local) return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
} else { } else {
return .fetchStatus(fetchStatus) return FileMediaResourceStatus(mediaStatus: .fetchStatus(fetchStatus), fetchStatus: status.fetchStatus)
} }
case .playbackStatus: case .playbackStatus:
return status return status
@ -289,6 +319,23 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
textConstrainedSize.width -= 80.0 textConstrainedSize.width -= 80.0
} }
let streamingProgressDiameter: CGFloat = 28.0
var hasStreamingProgress = false
if isAudio && !isVoice {
if let resourceStatus = currentResourceStatus {
switch resourceStatus.fetchStatus {
case .Fetching, .Remote:
hasStreamingProgress = true
case .Local:
break
}
}
if hasStreamingProgress {
textConstrainedSize.width -= streamingProgressDiameter + 4.0
}
}
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -311,6 +358,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
minLayoutWidth = max(minLayoutWidth, statusSize.width) minLayoutWidth = max(minLayoutWidth, statusSize.width)
} }
var cloudFetchIconImage: UIImage?
if hasStreamingProgress {
minLayoutWidth += streamingProgressDiameter + 4.0
cloudFetchIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchOutgoingIcon(presentationData.theme.theme)
}
let fileIconImage: UIImage? let fileIconImage: UIImage?
if hasThumbnail { if hasThumbnail {
fileIconImage = nil fileIconImage = nil
@ -325,6 +378,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
var iconFrame: CGRect? var iconFrame: CGRect?
let progressFrame: CGRect let progressFrame: CGRect
let streamingCacheStatusFrame: CGRect
let controlAreaWidth: CGFloat let controlAreaWidth: CGFloat
if hasThumbnail { if hasThumbnail {
@ -362,7 +416,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0) fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0)
} else { } else {
let unionSize = titleFrame.union(descriptionFrame).union(progressFrame).size let unionSize = titleFrame.union(descriptionFrame).union(progressFrame).size
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 4.0) fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 6.0)
} }
var statusFrame: CGRect? var statusFrame: CGRect?
@ -376,6 +430,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0) statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0)
} }
if isAudio && !isVoice {
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: fittedLayoutSize.width + 6.0, y: 4.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
if hasStreamingProgress {
fittedLayoutSize.width += streamingProgressDiameter + 6.0
}
} else {
streamingCacheStatusFrame = CGRect()
}
return (fittedLayoutSize, { [weak self] in return (fittedLayoutSize, { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.account = account strongSelf.account = account
@ -468,78 +531,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
displayLinkDispatcher.dispatch { displayLinkDispatcher.dispatch {
if let strongSelf = strongSelf { if let strongSelf = strongSelf {
var previousHadCacheStatus = false
if let resourceStatus = strongSelf.resourceStatus {
switch resourceStatus.fetchStatus {
case .Fetching, .Remote:
previousHadCacheStatus = true
case .Local:
previousHadCacheStatus = false
}
}
var hasCacheStatus = false
switch status.fetchStatus {
case .Fetching, .Remote:
hasCacheStatus = true
case .Local:
hasCacheStatus = false
}
strongSelf.resourceStatus = status strongSelf.resourceStatus = status
if isAudio && !isVoice && previousHadCacheStatus != hasCacheStatus {
if strongSelf.statusNode == nil { strongSelf.requestUpdateLayout(false)
let backgroundNodeColor: UIColor
if strongSelf.iconNode != nil {
backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor
} else if incoming {
backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor
} else {
backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor
}
let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor)
strongSelf.statusNode = statusNode
statusNode.frame = progressFrame
strongSelf.addSubnode(statusNode)
} else if let _ = updatedTheme {
//strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage))
}
let state: RadialStatusNodeState
let statusForegroundColor: UIColor
if strongSelf.iconNode != nil {
statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor
} else if incoming {
statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.incoming.withoutWallpaper.fill : bubbleTheme.incoming.withWallpaper.fill
} else { } else {
statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill strongSelf.updateStatus()
}
switch status {
case let .fetchStatus(fetchStatus):
strongSelf.waveformScrubbingNode?.enableScrubbing = false
switch fetchStatus {
case let .Fetching(isActive, progress):
var adjustedProgress = progress
if isActive {
adjustedProgress = max(adjustedProgress, 0.027)
}
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
case .Local:
if isAudio {
state = .play(statusForegroundColor)
} else if let fileIconImage = fileIconImage {
state = .customIcon(fileIconImage)
} else {
state = .none
}
case .Remote:
if isAudio && !isVoice {
state = .play(statusForegroundColor)
} else {
state = .download(statusForegroundColor)
}
}
case let .playbackStatus(playbackStatus):
strongSelf.waveformScrubbingNode?.enableScrubbing = true
switch playbackStatus {
case .playing:
state = .pause(statusForegroundColor)
case .paused:
state = .play(statusForegroundColor)
}
}
if let statusNode = strongSelf.statusNode {
if state == .none {
strongSelf.statusNode = nil
}
statusNode.transitionToState(state, completion: { [weak statusNode] in
if state == .none {
statusNode?.removeFromSupernode()
}
})
} }
} }
} }
@ -551,6 +563,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
strongSelf.statusNode?.frame = progressFrame strongSelf.statusNode?.frame = progressFrame
strongSelf.progressFrame = progressFrame
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
strongSelf.fileIconImage = fileIconImage
strongSelf.cloudFetchIconImage = cloudFetchIconImage
if let updatedFetchControls = updatedFetchControls { if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls) let _ = strongSelf.fetchControls.swap(updatedFetchControls)
@ -558,6 +574,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
updatedFetchControls.fetch() updatedFetchControls.fetch()
} }
} }
strongSelf.updateStatus()
} }
}) })
}) })
@ -565,6 +583,158 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
} }
private func updateStatus() {
guard let resourceStatus = self.resourceStatus else {
return
}
guard let message = self.message else {
return
}
guard let account = self.account else {
return
}
guard let presentationData = self.themeAndStrings?.0 else {
return
}
guard let progressFrame = self.progressFrame, let streamingCacheStatusFrame = self.streamingCacheStatusFrame else {
return
}
guard let file = self.file else {
return
}
let incoming = message.effectivelyIncoming(account.peerId)
let bubbleTheme = presentationData.theme.chat.bubble
var isAudio = false
var isVoice = false
for attribute in file.attributes {
if case let .Audio(voice, _, _, _, _) = attribute {
isAudio = true
if voice {
isVoice = true
}
break
}
}
let state: RadialStatusNodeState
var streamingState: RadialStatusNodeState = .none
if isAudio && !isVoice {
let streamingStatusForegroundColor: UIColor = incoming ? bubbleTheme.incomingAccentControlColor : bubbleTheme.outgoingAccentControlColor
let streamingStatusBackgroundColor: UIColor = incoming ? bubbleTheme.incomingMediaInactiveControlColor : bubbleTheme.outgoingMediaInactiveControlColor
switch resourceStatus.fetchStatus {
case let .Fetching(isActive, progress):
var adjustedProgress = progress
if isActive {
adjustedProgress = max(adjustedProgress, 0.027)
}
streamingState = .cloudProgress(color: streamingStatusForegroundColor, strokeBackgroundColor: streamingStatusBackgroundColor, lineWidth: 2.0, value: CGFloat(adjustedProgress))
case .Local:
streamingState = .none
case .Remote:
if let cloudFetchIconImage = self.cloudFetchIconImage {
streamingState = .customIcon(cloudFetchIconImage)
} else {
streamingState = .none
}
}
} else {
streamingState = .none
}
let statusForegroundColor: UIColor
if self.iconNode != nil {
statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor
} else if incoming {
statusForegroundColor = presentationData.wallpaper.isEmpty ? bubbleTheme.incoming.withoutWallpaper.fill : bubbleTheme.incoming.withWallpaper.fill
} else {
statusForegroundColor = presentationData.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill
}
switch resourceStatus.mediaStatus {
case let .fetchStatus(fetchStatus):
self.waveformScrubbingNode?.enableScrubbing = false
switch fetchStatus {
case let .Fetching(isActive, progress):
var adjustedProgress = progress
if isActive {
adjustedProgress = max(adjustedProgress, 0.027)
}
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
case .Local:
if isAudio {
state = .play(statusForegroundColor)
} else if let fileIconImage = self.fileIconImage {
state = .customIcon(fileIconImage)
} else {
state = .none
}
case .Remote:
if isAudio && !isVoice {
state = .play(statusForegroundColor)
} else {
state = .download(statusForegroundColor)
}
}
case let .playbackStatus(playbackStatus):
self.waveformScrubbingNode?.enableScrubbing = true
switch playbackStatus {
case .playing:
state = .pause(statusForegroundColor)
case .paused:
state = .play(statusForegroundColor)
}
}
if state != .none && self.statusNode == nil {
let backgroundNodeColor: UIColor
if self.iconNode != nil {
backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor
} else if incoming {
backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor
} else {
backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor
}
let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor)
self.statusNode = statusNode
statusNode.frame = progressFrame
self.addSubnode(statusNode)
}
if streamingState != .none && self.streamingStatusNode == nil {
let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
self.streamingStatusNode = streamingStatusNode
streamingStatusNode.frame = streamingCacheStatusFrame
self.addSubnode(streamingStatusNode)
}
if let statusNode = self.statusNode {
if state == .none {
self.statusNode = nil
}
statusNode.transitionToState(state, completion: { [weak statusNode] in
if state == .none {
statusNode?.removeFromSupernode()
}
})
}
if let streamingStatusNode = self.streamingStatusNode {
if streamingState == .none {
self.streamingStatusNode = nil
streamingStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak streamingStatusNode] _ in
if streamingState == .none {
streamingStatusNode?.removeFromSupernode()
}
})
} else {
streamingStatusNode.transitionToState(streamingState, completion: {
})
}
}
}
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) {
let currentAsyncLayout = node?.asyncLayout() let currentAsyncLayout = node?.asyncLayout()

View File

@ -44,7 +44,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
private let infoBackgroundNode: ASImageNode private let infoBackgroundNode: ASImageNode
private let muteIconNode: ASImageNode private let muteIconNode: ASImageNode
private var status: FileMediaResourceStatus? private var status: FileMediaResourceMediaStatus?
private let playbackStatusDisposable = MetaDisposable() private let playbackStatusDisposable = MetaDisposable()
private var shouldAcquireVideoContext: Bool { private var shouldAcquireVideoContext: Bool {
@ -178,16 +178,16 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
var updatedPlaybackStatus: Signal<FileMediaResourceStatus, NoError>? var updatedPlaybackStatus: Signal<FileMediaResourceStatus, NoError>?
if let updatedFile = updatedFile, updatedMedia { if let updatedFile = updatedFile, updatedMedia {
updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message, isRecentActions: item.associatedData.isRecentActions), item.account.pendingMessageManager.pendingMessageStatus(item.message.id)) updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message, isRecentActions: item.associatedData.isRecentActions), item.account.pendingMessageManager.pendingMessageStatus(item.message.id))
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
if let pendingStatus = pendingStatus { if let pendingStatus = pendingStatus {
var progress = pendingStatus.progress var progress = pendingStatus.progress
if pendingStatus.isRunning { if pendingStatus.isRunning {
progress = max(progress, 0.27) progress = max(progress, 0.27)
}
return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress))
} else {
return resourceStatus
} }
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress)), fetchStatus: resourceStatus.fetchStatus)
} else {
return resourceStatus
}
} }
} }
@ -280,7 +280,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.status = status strongSelf.status = status.mediaStatus
strongSelf.updateStatus() strongSelf.updateStatus()
})) }))
} }

View File

@ -238,6 +238,8 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto
super.init(frame: CGRect()) super.init(frame: CGRect())
self.disablesInteractiveTransitionGestureRecognizer = true
let inputPanelTheme = theme.chat.inputPanel let inputPanelTheme = theme.chat.inputPanel
self.pallete = TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor) self.pallete = TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor)

View File

@ -7,7 +7,7 @@ import SwiftSignalKit
import LegacyComponents import LegacyComponents
enum ChatTitleContent { enum ChatTitleContent {
case peer(PeerView) case peer(peerView: PeerView, onlineMemberCount: Int32?)
case group([Peer]) case group([Peer])
} }
@ -207,14 +207,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
self.insertSubview(statusNode.view, belowSubview: self.button.view) self.insertSubview(statusNode.view, belowSubview: self.button.view)
} }
switch self.networkState { switch self.networkState {
case .waitingForNetwork: case .waitingForNetwork:
statusNode.title = self.strings.State_WaitingForNetwork statusNode.title = self.strings.State_WaitingForNetwork
case .connecting: case .connecting:
statusNode.title = self.strings.State_Connecting statusNode.title = self.strings.State_Connecting
case .updating: case .updating:
statusNode.title = self.strings.State_Updating statusNode.title = self.strings.State_Updating
case .online: case .online:
break break
} }
} }
@ -246,7 +246,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
var titleLeftIcon: ChatTitleIcon = .none var titleLeftIcon: ChatTitleIcon = .none
var titleRightIcon: ChatTitleIcon = .none var titleRightIcon: ChatTitleIcon = .none
switch titleContent { switch titleContent {
case let .peer(peerView): case let .peer(peerView, _):
if let peer = peerViewMainPeer(peerView) { if let peer = peerViewMainPeer(peerView) {
if peerView.peerId == self.account.peerId { if peerView.peerId == self.account.peerId {
string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
@ -302,7 +302,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
var shouldUpdateLayout = false var shouldUpdateLayout = false
if let titleContent = self.titleContent { if let titleContent = self.titleContent {
switch titleContent { switch titleContent {
case let .peer(peerView): case let .peer(peerView, onlineMemberCount):
if let peer = peerViewMainPeer(peerView) { if let peer = peerViewMainPeer(peerView) {
if peer.id == self.account.peerId { if peer.id == self.account.peerId {
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
@ -379,17 +379,27 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
} }
} else if let channel = peer as? TelegramChannel { } else if let channel = peer as? TelegramChannel {
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
let membersString: String if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 {
if case .group = channel.info { let string = NSMutableAttributedString()
membersString = strings.Conversation_StatusMembers(memberCount)
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
self.infoNode.attributedText = string
shouldUpdateLayout = true
}
} else { } else {
membersString = strings.Conversation_StatusSubscribers(memberCount) let membersString: String
} if case .group = channel.info {
membersString = strings.Conversation_StatusMembers(memberCount)
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) } else {
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { membersString = strings.Conversation_StatusSubscribers(memberCount)
self.infoNode.attributedText = string }
shouldUpdateLayout = true let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
self.infoNode.attributedText = string
shouldUpdateLayout = true
}
} }
} else { } else {
switch channel.info { switch channel.info {

View File

@ -620,7 +620,7 @@ final class ContactListNode: ASDisplayNode {
} }
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get()) return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get())
|> mapToQueue { localPeers, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal<ContactsListNodeTransition, NoError> in |> mapToQueue { localPeersAndStatuses, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal<ContactsListNodeTransition, NoError> in
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
var existingPeerIds = Set<PeerId>() var existingPeerIds = Set<PeerId>()
var disabledPeerIds = Set<PeerId>() var disabledPeerIds = Set<PeerId>()
@ -628,17 +628,17 @@ final class ContactListNode: ASDisplayNode {
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>() var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
for filter in filters { for filter in filters {
switch filter { switch filter {
case .excludeSelf: case .excludeSelf:
existingPeerIds.insert(account.peerId) existingPeerIds.insert(account.peerId)
case let .exclude(peerIds): case let .exclude(peerIds):
existingPeerIds = existingPeerIds.union(peerIds) existingPeerIds = existingPeerIds.union(peerIds)
case let .disable(peerIds): case let .disable(peerIds):
disabledPeerIds = disabledPeerIds.union(peerIds) disabledPeerIds = disabledPeerIds.union(peerIds)
} }
} }
var peers: [ContactListPeer] = [] var peers: [ContactListPeer] = []
for peer in localPeers { for peer in localPeersAndStatuses.0 {
if !existingPeerIds.contains(peer.id) { if !existingPeerIds.contains(peer.id) {
existingPeerIds.insert(peer.id) existingPeerIds.insert(peer.id)
peers.append(.peer(peer: peer, isGlobal: false)) peers.append(.peer(peer: peer, isGlobal: false))
@ -680,7 +680,7 @@ final class ContactListNode: ASDisplayNode {
peers.append(.deviceContact(stableId, contact)) peers.append(.deviceContact(stableId, contact))
} }
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: [:], presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds) let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds)
let previous = previousEntries.swap(entries) let previous = previousEntries.swap(entries)
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false)) return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false))
} }

View File

@ -14,6 +14,7 @@ private enum ContactListSearchGroup {
private struct ContactListSearchEntry: Identifiable, Comparable { private struct ContactListSearchEntry: Identifiable, Comparable {
let index: Int let index: Int
let peer: ContactListPeer let peer: ContactListPeer
let presence: PeerPresence?
let group: ContactListSearchGroup let group: ContactListSearchGroup
let enabled: Bool let enabled: Bool
@ -28,6 +29,13 @@ private struct ContactListSearchEntry: Identifiable, Comparable {
if lhs.peer != rhs.peer { if lhs.peer != rhs.peer {
return false return false
} }
if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence {
if !lhsPresence.isEqual(to: rhsPresence) {
return false
}
} else if (lhs.presence != nil) != (rhs.presence != nil) {
return false
}
if lhs.group != rhs.group { if lhs.group != rhs.group {
return false return false
} }
@ -41,13 +49,17 @@ private struct ContactListSearchEntry: Identifiable, Comparable {
return lhs.index < rhs.index return lhs.index < rhs.index
} }
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, openPeer: @escaping (ContactListPeer) -> Void) -> ListViewItem { func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void) -> ListViewItem {
let header: ListViewItemHeader let header: ListViewItemHeader
let status: ContactsPeerItemStatus let status: ContactsPeerItemStatus
switch self.group { switch self.group {
case .contacts: case .contacts:
header = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil) header = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil)
status = .none if let presence = self.presence {
status = .presence(presence, timeFormat)
} else {
status = .none
}
case .global: case .global:
header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil) header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil)
if case let .peer(peer, _) = self.peer, let _ = peer.addressName { if case let .peer(peer, _) = self.peer, let _ = peer.addressName {
@ -80,12 +92,12 @@ struct ContactListSearchContainerTransition {
let isSearching: Bool let isSearching: Bool
} }
private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, openPeer: @escaping (ContactListPeer) -> Void) -> ContactListSearchContainerTransition { private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void) -> ContactListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, openPeer: openPeer), directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, openPeer: openPeer), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer), directionHint: nil) }
return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
} }
@ -170,7 +182,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, themeAndStringsPromise.get()) return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, themeAndStringsPromise.get())
|> delay(0.1, queue: Queue.concurrentDefaultQueue()) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|> map { localPeers, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in |> map { localPeersAndPresences, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in
var entries: [ContactListSearchEntry] = [] var entries: [ContactListSearchEntry] = []
var existingPeerIds = Set<PeerId>() var existingPeerIds = Set<PeerId>()
var disabledPeerIds = Set<PeerId>() var disabledPeerIds = Set<PeerId>()
@ -186,7 +198,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
} }
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>() var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
var index = 0 var index = 0
for peer in localPeers { for peer in localPeersAndPresences.0 {
if existingPeerIds.contains(peer.id) { if existingPeerIds.contains(peer.id) {
continue continue
} }
@ -195,7 +207,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
if onlyWriteable { if onlyWriteable {
enabled = canSendMessagesToPeer(peer) enabled = canSendMessagesToPeer(peer)
} }
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer, isGlobal: false), group: .contacts, enabled: enabled)) entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer, isGlobal: false), presence: localPeersAndPresences.1[peer.id], group: .contacts, enabled: enabled))
if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone { if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
} }
@ -214,7 +226,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
enabled = canSendMessagesToPeer(peer.peer) enabled = canSendMessagesToPeer(peer.peer)
} }
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), group: .global, enabled: enabled)) entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), presence: nil, group: .global, enabled: enabled))
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
} }
@ -233,7 +245,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
enabled = canSendMessagesToPeer(peer.peer) enabled = canSendMessagesToPeer(peer.peer)
} }
entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), group: .global, enabled: enabled)) entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), presence: nil, group: .global, enabled: enabled))
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
} }
@ -249,7 +261,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
continue outer continue outer
} }
} }
entries.append(ContactListSearchEntry(index: index, peer: .deviceContact(stableId, contact), group: .deviceContacts, enabled: true)) entries.append(ContactListSearchEntry(index: index, peer: .deviceContact(stableId, contact), presence: nil, group: .deviceContacts, enabled: true))
index += 1 index += 1
} }
} }
@ -263,17 +275,17 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode {
let previousSearchItems = Atomic<[ContactListSearchEntry]>(value: []) let previousSearchItems = Atomic<[ContactListSearchEntry]>(value: [])
self.searchDisposable.set((searchItems self.searchDisposable.set((searchItems
|> deliverOnMainQueue).start(next: { [weak self] items in |> deliverOnMainQueue).start(next: { [weak self] items in
if let strongSelf = self { if let strongSelf = self {
let previousItems = previousSearchItems.swap(items ?? []) let previousItems = previousSearchItems.swap(items ?? [])
let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, openPeer: { peer in self?.listNode.clearHighlightAnimated(true) let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, openPeer: { peer in self?.listNode.clearHighlightAnimated(true)
self?.openPeer(peer) self?.openPeer(peer)
}) })
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
} }
})) }))
self.listNode.beganInteractiveDragging = { [weak self] in self.listNode.beganInteractiveDragging = { [weak self] in
self?.dismissInput?() self?.dismissInput?()

View File

@ -172,7 +172,7 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe
context.requestedCompleteFetch = false context.requestedCompleteFetch = false
} else { } else {
if streamable { if streamable {
context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: context.readingOffset ..< resourceSize, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: context.readingOffset ..< Int(Int32.max), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start())
} else if !context.requestedCompleteFetch && context.fetchAutomatically { } else if !context.requestedCompleteFetch && context.fetchAutomatically {
context.requestedCompleteFetch = true context.requestedCompleteFetch = true
context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start())
@ -236,7 +236,7 @@ final class FFMpegMediaFrameSourceContext: NSObject {
let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1) let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1)
if streamable { if streamable {
self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: 0 ..< resourceSize, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: 0 ..< Int(Int32.max), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
} else if !self.requestedCompleteFetch && self.fetchAutomatically { } else if !self.requestedCompleteFetch && self.fetchAutomatically {
self.requestedCompleteFetch = true self.requestedCompleteFetch = true
self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
@ -308,6 +308,26 @@ final class FFMpegMediaFrameSourceContext: NSObject {
} }
} }
} }
} else if codecPar.pointee.codec_id == AV_CODEC_ID_MPEG4 {
if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromMpeg4CodecData(UInt32(kCMVideoCodecType_MPEG4Video), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size) {
let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000))
let duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale)
var rotationAngle: Double = 0.0
if let rotationInfo = av_dict_get(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.metadata, "rotate", nil, 0), let value = rotationInfo.pointee.value {
if strcmp(value, "0") != 0 {
if let angle = Double(String(cString: value)) {
rotationAngle = angle * Double.pi / 180.0
}
}
}
let aspect = Double(codecPar.pointee.width) / Double(codecPar.pointee.height)
videoStream = StreamContext(index: streamIndex, codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect)
break
}
} else if codecPar.pointee.codec_id == AV_CODEC_ID_H264 { } else if codecPar.pointee.codec_id == AV_CODEC_ID_H264 {
if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAVCCodecData(UInt32(kCMVideoCodecType_H264), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size) { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAVCCodecData(UInt32(kCMVideoCodecType_H264), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size) {
let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000)) let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000))

View File

@ -40,6 +40,35 @@ final class FFMpegMediaFrameSourceContextHelpers {
return formatDescription return formatDescription
} }
static func createFormatDescriptionFromMpeg4CodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: UnsafePointer<UInt8>, _ extradata_size: Int32) -> CMFormatDescription? {
let par = NSMutableDictionary()
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
par.setObject(1 as NSNumber, forKey: "VerticalSpacing" as NSString)
let atoms = NSMutableDictionary()
atoms.setObject(NSData(bytes: extradata, length: Int(extradata_size)), forKey: "esds" as NSString)
let extensions = NSMutableDictionary()
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationBottomField" as NSString)
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationTopField" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(par, forKey: "CVPixelAspectRatio" as NSString)
extensions.setObject(atoms, forKey: "SampleDescriptionExtensionAtoms" as NSString)
extensions.setObject("mp4v" as NSString, forKey: "FormatName" as NSString)
extensions.setObject(0 as NSNumber, forKey: "SpatialQuality" as NSString)
//extensions.setObject(0 as NSNumber, forKey: "Version" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(1 as NSNumber, forKey: "CVFieldCount" as NSString)
extensions.setObject(24 as NSNumber, forKey: "Depth" as NSString)
var formatDescription: CMFormatDescription?
guard CMVideoFormatDescriptionCreate(nil, kCMVideoCodecType_MPEG4Video, width, height, extensions, &formatDescription) == noErr else {
return nil
}
return formatDescription
}
static func createFormatDescriptionFromHEVCCodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: UnsafePointer<UInt8>, _ extradata_size: Int32) -> CMFormatDescription? { static func createFormatDescriptionFromHEVCCodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: UnsafePointer<UInt8>, _ extradata_size: Int32) -> CMFormatDescription? {
let par = NSMutableDictionary() let par = NSMutableDictionary()
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString) par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)

View File

@ -208,6 +208,7 @@ public func fetchVideoLibraryMediaResourceHash(resource: VideoLibraryMediaResour
if fetchResult.count != 0 { if fetchResult.count != 0 {
let asset = fetchResult.object(at: 0) let asset = fetchResult.object(at: 0)
let option = PHVideoRequestOptions() let option = PHVideoRequestOptions()
option.isNetworkAccessAllowed = true
option.deliveryMode = .highQualityFormat option.deliveryMode = .highQualityFormat
let alreadyReceivedAsset = Atomic<Bool>(value: false) let alreadyReceivedAsset = Atomic<Bool>(value: false)

View File

@ -8,7 +8,12 @@ enum FileMediaResourcePlaybackStatus {
case paused case paused
} }
enum FileMediaResourceStatus { struct FileMediaResourceStatus {
let mediaStatus: FileMediaResourceMediaStatus
let fetchStatus: MediaResourceStatus
}
enum FileMediaResourceMediaStatus {
case fetchStatus(MediaResourceStatus) case fetchStatus(MediaResourceStatus)
case playbackStatus(FileMediaResourcePlaybackStatus) case playbackStatus(FileMediaResourcePlaybackStatus)
} }
@ -50,45 +55,49 @@ func messageFileMediaResourceStatus(account: Account, file: TelegramMediaFile, m
if message.flags.isSending { if message.flags.isSending {
return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), account.pendingMessageManager.pendingMessageStatus(message.id), playbackStatus) return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), account.pendingMessageManager.pendingMessageStatus(message.id), playbackStatus)
|> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in |> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in
if let playbackStatus = playbackStatus { let mediaStatus: FileMediaResourceMediaStatus
switch playbackStatus { if let playbackStatus = playbackStatus {
case .playing: switch playbackStatus {
return .playbackStatus(.playing) case .playing:
case .paused: mediaStatus = .playbackStatus(.playing)
return .playbackStatus(.paused) case .paused:
case let .buffering(_, whilePlaying): mediaStatus = .playbackStatus(.paused)
if whilePlaying { case let .buffering(_, whilePlaying):
return .playbackStatus(.playing) if whilePlaying {
} else { mediaStatus = .playbackStatus(.playing)
return .playbackStatus(.paused) } else {
} mediaStatus = .playbackStatus(.paused)
} }
} else if let pendingStatus = pendingStatus {
return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress))
} else {
return .fetchStatus(resourceStatus)
} }
} else if let pendingStatus = pendingStatus {
mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress))
} else {
mediaStatus = .fetchStatus(resourceStatus)
}
return FileMediaResourceStatus(mediaStatus: mediaStatus, fetchStatus: resourceStatus)
} }
} else { } else {
return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), playbackStatus) return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), playbackStatus)
|> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in |> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in
if let playbackStatus = playbackStatus { let mediaStatus: FileMediaResourceMediaStatus
switch playbackStatus { if let playbackStatus = playbackStatus {
case .playing: switch playbackStatus {
return .playbackStatus(.playing) case .playing:
case .paused: mediaStatus = .playbackStatus(.playing)
return .playbackStatus(.paused) case .paused:
case let .buffering(_, whilePlaying): mediaStatus = .playbackStatus(.paused)
if whilePlaying { case let .buffering(_, whilePlaying):
return .playbackStatus(.playing) if whilePlaying {
} else { mediaStatus = .playbackStatus(.playing)
return .playbackStatus(.paused) } else {
} mediaStatus = .playbackStatus(.paused)
} }
} else {
return .fetchStatus(resourceStatus)
} }
} else {
mediaStatus = .fetchStatus(resourceStatus)
}
return FileMediaResourceStatus(mediaStatus: mediaStatus, fetchStatus: resourceStatus)
} }
} }
} }

View File

@ -131,9 +131,13 @@ func galleryItemForEntry(account: Account, presentationData: PresentationData, e
if file.isVideo || supportedVideoMimeTypes.contains(file.mimeType) { if file.isVideo || supportedVideoMimeTypes.contains(file.mimeType) {
let content: UniversalVideoContent let content: UniversalVideoContent
if file.isAnimated { if file.isAnimated {
content = NativeVideoContent(id: .message(message.id, message.stableId + 1, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: true) content = NativeVideoContent(id: .message(message.id, message.stableId + 1, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: true, enableSound: false)
} else { } else {
content = NativeVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos) if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
content = NativeVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
} else {
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
}
} }
return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text, hideControls: hideControls, playbackCompleted: playbackCompleted) return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text, hideControls: hideControls, playbackCompleted: playbackCompleted)
} else { } else {

View File

@ -38,7 +38,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0 self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0
self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0 self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0
self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("tds", orElse: 0)) ?? .filtered self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("tds", orElse: 0)) ?? .filtered
self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 0)) ?? .chats self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 0)) ?? .messages
self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0 self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0
} }

View File

@ -157,7 +157,7 @@ final class LegacyStickerImageDataSource: TGImageDataSource {
attributes.append(.Sticker(displayText: "", packReference: .id(id: stickerPackId, accessHash: stickerPackAccessHash), maskData: nil)) attributes.append(.Sticker(displayText: "", packReference: .id(id: stickerPackId, accessHash: stickerPackAccessHash), maskData: nil))
} }
return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: attributes), small: !highQuality, fitSize: fitSize, completion: { image in return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil, fileName: fileNameFromFileAttributes(attributes)), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: attributes), small: !highQuality, fitSize: fitSize, completion: { image in
if let image = image { if let image = image {
sharedImageCache.setImage(image, forKey: uri, attributes: nil) sharedImageCache.setImage(image, forKey: uri, attributes: nil)
completion?(TGDataResource(image: image, decoded: true)) completion?(TGDataResource(image: image, decoded: true))

View File

@ -92,6 +92,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect,
legacyController.bind(controller: baseController) legacyController.bind(controller: baseController)
legacyController.presentationCompleted = { [weak legacyController, weak baseController] in legacyController.presentationCompleted = { [weak legacyController, weak baseController] in
if let legacyController = legacyController, let baseController = baseController { if let legacyController = legacyController, let baseController = baseController {
legacyController.view.disablesInteractiveTransitionGestureRecognizer = true
let inputPanelTheme = theme.chat.inputPanel let inputPanelTheme = theme.chat.inputPanel
var uploadInterface: LegacyLiveUploadInterface? var uploadInterface: LegacyLiveUploadInterface?
if peerId.namespace != Namespaces.Peer.SecretChat { if peerId.namespace != Namespaces.Peer.SecretChat {

View File

@ -154,7 +154,7 @@ final class ListMessageFileItemNode: ListMessageNode {
private let statusDisposable = MetaDisposable() private let statusDisposable = MetaDisposable()
private let fetchControls = Atomic<FetchControls?>(value: nil) private let fetchControls = Atomic<FetchControls?>(value: nil)
private var resourceStatus: FileMediaResourceStatus? private var resourceStatus: FileMediaResourceMediaStatus?
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
private var downloadStatusIconNode: ASImageNode private var downloadStatusIconNode: ASImageNode
@ -396,10 +396,11 @@ final class ListMessageFileItemNode: ListMessageNode {
if isAudio { if isAudio {
if let currentUpdatedStatusSignal = updatedStatusSignal { if let currentUpdatedStatusSignal = updatedStatusSignal {
updatedStatusSignal = currentUpdatedStatusSignal |> map { status in updatedStatusSignal = currentUpdatedStatusSignal
switch status { |> map { status in
switch status.mediaStatus {
case .fetchStatus: case .fetchStatus:
return .fetchStatus(.Local) return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
case .playbackStatus: case .playbackStatus:
return status return status
} }
@ -571,7 +572,9 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
if let updatedStatusSignal = updatedStatusSignal { if let updatedStatusSignal = updatedStatusSignal {
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in strongSelf.statusDisposable.set((updatedStatusSignal
|> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in
let status = fileStatus.mediaStatus
displayLinkDispatcher.dispatch { displayLinkDispatcher.dispatch {
if let strongSelf = strongSelf { if let strongSelf = strongSelf {
strongSelf.resourceStatus = status strongSelf.resourceStatus = status

View File

@ -9,7 +9,7 @@ public enum NavigateToChatKeepStack {
case never case never
} }
public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (()-> Void)? = nil, animated: Bool = true) { public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, animated: Bool = true, completion: @escaping () -> Void = {}) {
var found = false var found = false
var isFirst = true var isFirst = true
for controller in navigationController.viewControllers.reversed() { for controller in navigationController.viewControllers.reversed() {
@ -24,6 +24,7 @@ public func navigateToChatController(navigationController: NavigationController,
} else { } else {
let _ = navigationController.popToViewController(controller, animated: animated) let _ = navigationController.popToViewController(controller, animated: animated)
} }
completion()
found = true found = true
break break
} }
@ -48,9 +49,9 @@ public func navigateToChatController(navigationController: NavigationController,
resolvedKeepStack = false resolvedKeepStack = false
} }
if resolvedKeepStack { if resolvedKeepStack {
navigationController.pushViewController(controller) navigationController.pushViewController(controller, completion: completion)
} else { } else {
navigationController.replaceAllButRootController(controller, animated: animated) navigationController.replaceAllButRootController(controller, animated: animated, completion: completion)
} }
} }
} }

View File

@ -137,6 +137,47 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
} }
let continueHandling: () -> Void = { let continueHandling: () -> Void = {
let handleInternalUrl: (String) -> Void = { url in
let _ = (resolveUrl(account: account, url: url)
|> deliverOnMainQueue).start(next: { resolved in
if case let .externalUrl(value) = resolved {
applicationContext.applicationBindings.openUrl(value)
} else {
openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in
switch navigation {
case .info:
let _ = (account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { peer in
if let infoController = peerInfoController(account: account, peer: peer) {
if let navigationController = navigationController {
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
}
navigationController?.pushViewController(infoController)
}
})
case .chat:
if let navigationController = navigationController {
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId))
}
case let .withBotStartPayload(payload):
if let navigationController = navigationController {
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), botStart: payload)
}
}
}, present: { c, a in
if let navigationController = navigationController {
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
(navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a)
}
}, dismissInput: {
dismissInput()
})
}
})
}
if parsedUrl.scheme == "tg", let query = parsedUrl.query { if parsedUrl.scheme == "tg", let query = parsedUrl.query {
var convertedUrl: String? var convertedUrl: String?
if parsedUrl.host == "localpeer" { if parsedUrl.host == "localpeer" {
@ -418,62 +459,29 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
} }
if let convertedUrl = convertedUrl { if let convertedUrl = convertedUrl {
let _ = (resolveUrl(account: account, url: convertedUrl) handleInternalUrl(convertedUrl)
|> deliverOnMainQueue).start(next: { resolved in
if case let .externalUrl(value) = resolved {
applicationContext.applicationBindings.openUrl(value)
} else {
openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in
switch navigation {
case .info:
let _ = (account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { peer in
if let infoController = peerInfoController(account: account, peer: peer) {
if let navigationController = navigationController {
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
}
navigationController?.pushViewController(infoController)
}
})
case .chat:
if let navigationController = navigationController {
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId))
}
case let .withBotStartPayload(payload):
if let navigationController = navigationController {
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), botStart: payload)
}
}
}, present: { c, a in
if let navigationController = navigationController {
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
(navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a)
}
}, dismissInput: {
dismissInput()
})
}
})
} }
return return
} }
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
if #available(iOSApplicationExtension 9.0, *) { if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" {
if let window = navigationController?.view.window { handleInternalUrl(parsedUrl.absoluteString)
let controller = SFSafariViewController(url: parsedUrl)
if #available(iOSApplicationExtension 10.0, *) {
controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor
controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor
}
window.rootViewController?.present(controller, animated: true)
} else {
applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString)
}
} else { } else {
applicationContext.applicationBindings.openUrl(url) if #available(iOSApplicationExtension 9.0, *) {
if let window = navigationController?.view.window {
let controller = SFSafariViewController(url: parsedUrl)
if #available(iOSApplicationExtension 10.0, *) {
controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor
controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor
}
window.rootViewController?.present(controller, animated: true)
} else {
applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString)
}
} else {
applicationContext.applicationBindings.openUrl(url)
}
} }
} else { } else {
applicationContext.applicationBindings.openUrl(url) applicationContext.applicationBindings.openUrl(url)
@ -481,11 +489,16 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
} }
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in let nativeHosts = ["t.me", "telegram.me"]
if !success { if let host = parsedUrl.host, nativeHosts.contains(host) {
continueHandling() continueHandling()
} } else {
})) applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in
if !success {
continueHandling()
}
}))
}
} else { } else {
continueHandling() continueHandling()
} }

View File

@ -4,7 +4,7 @@ import Display
import LegacyComponents import LegacyComponents
enum OverlayStatusControllerType { enum OverlayStatusControllerType {
case loading case loading(cancelled: (() -> Void)?)
case success case success
case proxySettingSuccess case proxySettingSuccess
} }
@ -47,12 +47,14 @@ private enum OverlayStatusContentController {
} }
} }
func dismiss() { func dismiss(completion: @escaping () -> Void) {
switch self { switch self {
case let .loading(controller): case let .loading(controller):
controller.dismiss(true) {} controller.dismiss(true, completion: {
completion()
})
default: default:
break completion()
} }
} }
} }
@ -64,8 +66,12 @@ private final class OverlayStatusControllerNode: ViewControllerTracingNode {
init(theme: PresentationTheme, type: OverlayStatusControllerType, dismissed: @escaping () -> Void) { init(theme: PresentationTheme, type: OverlayStatusControllerType, dismissed: @escaping () -> Void) {
self.dismissed = dismissed self.dismissed = dismissed
switch type { switch type {
case .loading: case let .loading(cancelled):
self.contentController = .loading(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light)) let controller = TGProgressWindowController(light: theme.actionSheet.backgroundType == .light)!
controller.cancelled = {
cancelled?()
}
self.contentController = .loading(controller)
case .success: case .success:
self.contentController = .progress(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light)) self.contentController = .progress(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light))
case .proxySettingSuccess: case .proxySettingSuccess:
@ -92,8 +98,9 @@ private final class OverlayStatusControllerNode: ViewControllerTracingNode {
} }
func dismiss() { func dismiss() {
self.contentController.dismiss() self.contentController.dismiss(completion: { [weak self] in
self.dismissed() self?.dismissed()
})
} }
} }

View File

@ -102,6 +102,56 @@ final class PeerChannelMemberCategoriesContextsManager {
return self.getContext(postbox: postbox, network: network, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) return self.getContext(postbox: postbox, network: network, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated)
} }
func recentOnline(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<Int32, NoError> {
return Signal { [weak self] subscriber in
var previousIds: Set<PeerId>?
let statusesDisposable = MetaDisposable()
let disposableAndControl = self?.recent(postbox: postbox, network: network, peerId: peerId, updated: { state in
var idList: [PeerId] = []
for item in state.list {
idList.append(item.peer.id)
if idList.count >= 200 {
break
}
}
let updatedIds = Set(idList)
if previousIds != updatedIds {
previousIds = updatedIds
let key: PostboxViewKey = .peerPresences(peerIds: updatedIds)
statusesDisposable.set((postbox.combinedView(keys: [key])
|> map { view -> Int32 in
var count: Int32 = 0
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
if let presences = (view.views[key] as? PeerPresencesView)?.presences {
for (_, presence) in presences {
if let presence = presence as? TelegramUserPresence {
let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp))
switch relativeStatus {
case .online:
count += 1
default:
break
}
}
}
}
return count
}
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { count in
subscriber.putNext(count)
}))
}
})
return ActionDisposable {
disposableAndControl?.0.dispose()
statusesDisposable.dispose()
}
}
|> runOn(Queue.mainQueue())
}
func admins(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { func admins(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) {
return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated) return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated)
} }

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

View File

@ -110,6 +110,9 @@ enum PresentationResourceKey: Int32 {
case chatBubbleActionButtonOutgoingBottomRightImage case chatBubbleActionButtonOutgoingBottomRightImage
case chatBubbleActionButtonOutgoingBottomSingleImage case chatBubbleActionButtonOutgoingBottomSingleImage
case chatBubbleFileCloudFetchIncomingIcon
case chatBubbleFileCloudFetchOutgoingIcon
case chatBubbleReplyThumbnailPlayImage case chatBubbleReplyThumbnailPlayImage
case chatInfoItemBackgroundImageWithWallpaper case chatInfoItemBackgroundImageWithWallpaper

View File

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

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

View File

@ -7,6 +7,7 @@ public enum RadialStatusNodeState: Equatable {
case play(UIColor) case play(UIColor)
case pause(UIColor) case pause(UIColor)
case progress(color: UIColor, lineWidth: CGFloat?, value: CGFloat?, cancelEnabled: Bool) case progress(color: UIColor, lineWidth: CGFloat?, value: CGFloat?, cancelEnabled: Bool)
case cloudProgress(color: UIColor, strokeBackgroundColor: UIColor, lineWidth: CGFloat, value: CGFloat?)
case check(UIColor) case check(UIColor)
case customIcon(UIImage) case customIcon(UIImage)
case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double) case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double)
@ -43,6 +44,12 @@ public enum RadialStatusNodeState: Equatable {
} else { } else {
return false return false
} }
case let .cloudProgress(lhsColor, lhsStrokeBackgroundColor, lhsLineWidth, lhsValue):
if case let .cloudProgress(rhsColor, rhsStrokeBackgroundColor, rhsLineWidth, rhsValue) = rhs, lhsColor.isEqual(rhsColor), lhsStrokeBackgroundColor.isEqual(rhsStrokeBackgroundColor), lhsLineWidth.isEqual(to: rhsLineWidth), lhsValue == rhsValue {
return true
} else {
return false
}
case let .check(lhsColor): case let .check(lhsColor):
if case let .check(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { if case let .check(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
return true return true
@ -99,6 +106,18 @@ public enum RadialStatusNodeState: Equatable {
node.progress = value node.progress = value
return node return node
} }
case let .cloudProgress(color, strokeLineColor, lineWidth, value):
if let current = current as? RadialCloudProgressContentNode {
if !current.color.isEqual(color) {
current.color = color
}
current.progress = value
return current
} else {
let node = RadialCloudProgressContentNode(color: color, backgroundStrokeColor: strokeLineColor, lineWidth: lineWidth)
node.progress = value
return node
}
case let .secretTimeout(color, icon, beginTime, timeout): case let .secretTimeout(color, icon, beginTime, timeout):
return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon) return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon)
} }

View File

@ -9,15 +9,20 @@ import Display
func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal<Void, NoError> { func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal<Void, NoError> {
var resource: MediaResource? var resource: MediaResource?
var isImage = true var isImage = true
var fileExtension: String?
if let image = mediaReference.media as? TelegramMediaImage { if let image = mediaReference.media as? TelegramMediaImage {
if let representation = largestImageRepresentation(image.representations) { if let representation = largestImageRepresentation(image.representations) {
resource = representation.resource resource = representation.resource
} }
} else if let file = mediaReference.media as? TelegramMediaFile { } else if let file = mediaReference.media as? TelegramMediaFile {
resource = file.resource resource = file.resource
if file.isVideo { if file.isVideo || file.mimeType.hasPrefix("video/") {
isImage = false isImage = false
} }
let maybeExtension = ((file.fileName ?? "") as NSString).pathExtension
if !maybeExtension.isEmpty {
fileExtension = maybeExtension
}
} else if let webpage = mediaReference.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { } else if let webpage = mediaReference.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let file = content.file { if let file = content.file {
resource = file.resource resource = file.resource
@ -34,7 +39,7 @@ func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: P
if let resource = resource { if let resource = resource {
let fetchedData: Signal<MediaResourceData, NoError> = Signal { subscriber in let fetchedData: Signal<MediaResourceData, NoError> = Signal { subscriber in
let fetched = fetchedMediaResource(postbox: postbox, reference: mediaReference.resourceReference(resource)).start() let fetched = fetchedMediaResource(postbox: postbox, reference: mediaReference.resourceReference(resource)).start()
let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: true)).start(next: { next in let data = postbox.mediaBox.resourceData(resource, pathExtension: fileExtension, option: .complete(waitUntilFetchStatus: true)).start(next: { next in
subscriber.putNext(next) subscriber.putNext(next)
}, completed: { }, completed: {
subscriber.putCompletion() subscriber.putCompletion()

View File

@ -443,7 +443,7 @@ public func settingsController(account: Account, accountManager: AccountManager)
let archivedPacks = Promise<[ArchivedStickerPackItem]?>() let archivedPacks = Promise<[ArchivedStickerPackItem]?>()
let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in
let controller = OverlayStatusController(theme: account.telegramApplicationContext.currentPresentationData.with { $0 }.theme, type: .loading) let controller = OverlayStatusController(theme: account.telegramApplicationContext.currentPresentationData.with { $0 }.theme, type: .loading(cancelled: nil))
presentControllerImpl?(controller, nil) presentControllerImpl?(controller, nil)
let _ = (resolvedUrl.get() let _ = (resolvedUrl.get()
|> take(1) |> take(1)

View File

@ -1251,7 +1251,35 @@ public func userInfoController(account: Account, peerId: PeerId, mode: UserInfoC
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(currentPeerId)) navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(currentPeerId))
} }
} else { } else {
createSecretChatDisposable.set((createSecretChat(account: account, peerId: peerId) |> deliverOnMainQueue).start(next: { peerId in var createSignal = createSecretChat(account: account, peerId: peerId)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
createSignal = createSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
createSecretChatDisposable.set(nil)
}
createSecretChatDisposable.set((createSignal |> deliverOnMainQueue).start(next: { peerId in
if let navigationController = (controller?.navigationController as? NavigationController) { if let navigationController = (controller?.navigationController as? NavigationController) {
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId))
} }

View File

@ -201,6 +201,11 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate {
if !self.ignoreZoom { if !self.ignoreZoom {
self.centerScrollViewContents(transition: self.ignoreZoomTransition ?? .immediate) self.centerScrollViewContents(transition: self.ignoreZoomTransition ?? .immediate)
} }
if self.scrollNode.view.zoomScale.isEqual(to: self.scrollNode.view.minimumZoomScale) {
self.scrollNode.view.isScrollEnabled = false
} else {
self.scrollNode.view.isScrollEnabled = true
}
} }
override func contentSize() -> CGSize? { override func contentSize() -> CGSize? {