diff --git a/Images.xcassets/Chat/Message/FileCloudFetch.imageset/Contents.json b/Images.xcassets/Chat/Message/FileCloudFetch.imageset/Contents.json new file mode 100644 index 0000000000..dc6998b201 --- /dev/null +++ b/Images.xcassets/Chat/Message/FileCloudFetch.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "cloud.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/FileCloudFetch.imageset/cloud.pdf b/Images.xcassets/Chat/Message/FileCloudFetch.imageset/cloud.pdf new file mode 100644 index 0000000000..ae1fc2aa1f Binary files /dev/null and b/Images.xcassets/Chat/Message/FileCloudFetch.imageset/cloud.pdf differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 47e84e793e..bd156f692f 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -128,6 +128,8 @@ D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383EDB207D1A1600C45548 /* emoji_suggestions.h */; }; D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */; }; D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */; }; + D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */; }; + D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */; }; D03AA4DF202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */; }; D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */; }; D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */; }; @@ -1257,6 +1259,8 @@ D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = ""; }; D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = ""; }; D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingCancelIndicator.swift; sourceTree = ""; }; + D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialCloudProgressContentNode.swift; sourceTree = ""; }; + D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformVideoContent.swift; sourceTree = ""; }; D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatContextResultPeekContentNode.swift; sourceTree = ""; }; D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewPeekContent.swift; sourceTree = ""; }; D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListEditableReorderControlNode.swift; sourceTree = ""; }; @@ -2291,6 +2295,7 @@ D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */, D01776B41F1D6CCC0044446D /* RadialStatusContentNode.swift */, D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */, + D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */, D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */, D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */, D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */, @@ -2555,6 +2560,7 @@ D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */, D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */, D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */, + D039FB1821711B5D00BD1BAD /* PlatformVideoContent.swift */, D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */, D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */, D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */, @@ -5122,6 +5128,7 @@ D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */, D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */, D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */, + D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */, D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */, D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */, D0430B001FF4570500A35ADD /* WebController.swift in Sources */, @@ -5330,6 +5337,7 @@ D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */, D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */, D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */, + D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */, D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */, D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */, D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */, diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift index eb4cd11f33..b24e7a6ddd 100644 --- a/TelegramUI/BotCheckoutControllerNode.swift +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -696,6 +696,26 @@ final class BotCheckoutControllerNode: ItemListControllerNode, case let .webToken(token): credentials = .generic(data: token.data, saveOnServer: token.saveOnServer) case .applePayStripe: + guard let paymentForm = self.paymentFormValue, let nativeProvider = paymentForm.nativeProvider else { + return + } + //NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[strongSelf->_paymentForm.nativeParams dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; + guard let nativeParamsData = nativeProvider.params.data(using: .utf8) else { + return + } + guard let nativeParams = (try? JSONSerialization.jsonObject(with: nativeParamsData, options: [])) as? [String: Any] else { + return + } + + let merchantId: String + if nativeProvider.name == "stripe" { + merchantId = "merchant.ph.telegra.Telegraph" + } else if let paramsId = nativeParams["apple_pay_merchant_id"] as? String { + merchantId = paramsId + } else { + return + } + let botPeerId = self.messageId.peerId let _ = (self.account.postbox.transaction({ transaction -> Peer? in return transaction.getPeer(botPeerId) @@ -703,7 +723,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, if let strongSelf = self, let botPeer = botPeer { let request = PKPaymentRequest() - request.merchantIdentifier = "merchant.ph.telegra.Telegraph" + request.merchantIdentifier = merchantId request.supportedNetworks = [.visa, .amex, .masterCard] request.merchantCapabilities = [.capability3DS] request.countryCode = "US" @@ -898,41 +918,50 @@ final class BotCheckoutControllerNode: ItemListControllerNode, guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { return } - guard let publishableKey = nativeParams["publishable_key"] as? String else { - return - } - let signal: Signal = Signal { subscriber in - let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration - configuration.smsAutofillDisabled = true - configuration.publishableKey = publishableKey - configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" + if nativeProvider.name == "stripe" { + guard let publishableKey = nativeParams["publishable_key"] as? String else { + return + } - let apiClient = STPAPIClient(configuration: configuration) - - apiClient.createToken(with: payment, completion: { token, error in - if let token = token { - subscriber.putNext(token) - subscriber.putCompletion() - } else if let error = error { - subscriber.putError(error) + let signal: Signal = Signal { subscriber in + let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration + configuration.smsAutofillDisabled = true + configuration.publishableKey = publishableKey + configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" + + let apiClient = STPAPIClient(configuration: configuration) + + apiClient.createToken(with: payment, completion: { token, error in + if let token = token { + subscriber.putNext(token) + subscriber.putCompletion() + } else if let error = error { + subscriber.putError(error) + } + }) + + return ActionDisposable { } - }) + } - return ActionDisposable { - } - } - - self.paymentAuthDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] token in - if let strongSelf = self { - strongSelf.applePayAuthrorizationCompletion = completion - strongSelf.pay(liabilityNoticeAccepted: true, receivedCredentials: .generic(data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: false)) - } else { + self.paymentAuthDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] token in + if let strongSelf = self { + strongSelf.applePayAuthrorizationCompletion = completion + strongSelf.pay(liabilityNoticeAccepted: true, receivedCredentials: .generic(data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: false)) + } else { + completion(.failure) + } + }, error: { _ in completion(.failure) + })) + } else { + self.applePayAuthrorizationCompletion = completion + guard let paymentString = String(data: payment.token.paymentData, encoding: .utf8) else { + return } - }, error: { _ in - completion(.failure) - })) + self.pay(liabilityNoticeAccepted: true, receivedCredentials: .applePay(data: paymentString)) + } } func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) { diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index a53343345d..6af3a6ac1a 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -214,7 +214,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod foundMembers = .single([]) } - let foundContacts: Signal<[Peer], NoError> + let foundContacts: Signal<([Peer], [PeerId: PeerPresence]), NoError> let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> switch mode { case .inviteActions, .banAndPromoteActions: @@ -222,7 +222,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) case .searchMembers, .searchBanned, .searchAdmins: - foundContacts = .single([]) + foundContacts = .single(([], [:])) foundRemotePeers = .single(([], [])) } @@ -322,7 +322,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod } } - for peer in foundContacts { + for peer in foundContacts.0 { if !existingPeerIds.contains(peer.id) { existingPeerIds.insert(peer.id) entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts)) diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 159ff40912..8740d3f44a 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -629,7 +629,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV if strongSelf.resolvePeerByNameDisposable == nil { strongSelf.resolvePeerByNameDisposable = MetaDisposable() } - let resolveSignal: Signal + var resolveSignal: Signal if let peerName = peerName { resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName) |> mapToSignal { peerId -> Signal in @@ -646,6 +646,32 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV } else { resolveSignal = .single(nil) } + var cancelImpl: (() -> Void)? + let presentationData = strongSelf.presentationData + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + resolveSignal = resolveSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + self?.resolvePeerByNameDisposable?.set(nil) + } strongSelf.resolvePeerByNameDisposable?.set((resolveSignal |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self, !hashtag.isEmpty { @@ -994,77 +1020,85 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV case let .peer(peerId): if case let .peer(peerView) = self.chatLocationInfoData { peerView.set(account.viewTracker.peerView(peerId)) - self.peerDisposable.set((peerView.get() - |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self { - if let peer = peerViewMainPeer(peerView) { - strongSelf.chatTitleView?.titleContent = .peer(peerView) - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) - } - var wasGroupChannel: Bool? - if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info { - if case .group = info { - wasGroupChannel = true - } else { - wasGroupChannel = false - } - } - var isGroupChannel: Bool? - if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { - if case .group = info { - isGroupChannel = true - } else { - isGroupChannel = false - } - } - strongSelf.peerView = peerView - if wasGroupChannel != isGroupChannel { - if let isGroupChannel = isGroupChannel, isGroupChannel { - let (recentDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in }) - let (adminsDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in }) - let disposable = DisposableSet() - disposable.add(recentDisposable) - disposable.add(adminsDisposable) - strongSelf.chatAdditionalDataDisposable.set(disposable) - } else { - strongSelf.chatAdditionalDataDisposable.set(nil) - } - } - if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.peerView = peerView - } - var peerIsMuted = false - if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - if case .muted = notificationSettings.muteState { - peerIsMuted = true - } - } - var renderedPeer: RenderedPeer? - var isContact: Bool = false - if let peer = peerView.peers[peerView.peerId] { - isContact = peerView.peerIsContact - var peers = SimpleDictionary() - peers[peer.id] = peer - if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { - peers[associatedPeer.id] = associatedPeer - } - renderedPeer = RenderedPeer(peerId: peer.id, peers: peers) - } - - var animated = false - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState { - animated = true - } - strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { - return $0.updatedPeer { _ in return renderedPeer - }.updatedIsContact(isContact).updatedPeerIsMuted(peerIsMuted) - }) - if !strongSelf.didSetChatLocationInfoReady { - strongSelf.didSetChatLocationInfoReady = true - strongSelf._chatLocationInfoReady.set(.single(true)) + var onlineMemberCount: Signal = .single(nil) + if peerId.namespace == Namespaces.Peer.CloudChannel { + onlineMemberCount = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recentOnline(postbox: account.postbox, network: account.network, peerId: peerId) + |> map(Optional.init) + } + self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount) + |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount in + if let strongSelf = self { + if let peer = peerViewMainPeer(peerView) { + strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) + } + if strongSelf.peerView === peerView { + return + } + var wasGroupChannel: Bool? + if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info { + if case .group = info { + wasGroupChannel = true + } else { + wasGroupChannel = false } } - })) + var isGroupChannel: Bool? + if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { + if case .group = info { + isGroupChannel = true + } else { + isGroupChannel = false + } + } + strongSelf.peerView = peerView + if wasGroupChannel != isGroupChannel { + if let isGroupChannel = isGroupChannel, isGroupChannel { + let (recentDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in }) + let (adminsDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in }) + let disposable = DisposableSet() + disposable.add(recentDisposable) + disposable.add(adminsDisposable) + strongSelf.chatAdditionalDataDisposable.set(disposable) + } else { + strongSelf.chatAdditionalDataDisposable.set(nil) + } + } + if strongSelf.isNodeLoaded { + strongSelf.chatDisplayNode.peerView = peerView + } + var peerIsMuted = false + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + peerIsMuted = true + } + } + var renderedPeer: RenderedPeer? + var isContact: Bool = false + if let peer = peerView.peers[peerView.peerId] { + isContact = peerView.peerIsContact + var peers = SimpleDictionary() + peers[peer.id] = peer + if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { + peers[associatedPeer.id] = associatedPeer + } + renderedPeer = RenderedPeer(peerId: peer.id, peers: peers) + } + + var animated = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState { + animated = true + } + strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { + return $0.updatedPeer { _ in return renderedPeer + }.updatedIsContact(isContact).updatedPeerIsMuted(peerIsMuted) + }) + if !strongSelf.didSetChatLocationInfoReady { + strongSelf.didSetChatLocationInfoReady = true + strongSelf._chatLocationInfoReady.set(.single(true)) + } + } + })) } case let .group(groupId): if case let .group(topPeersView) = self.chatLocationInfoData { @@ -4288,7 +4322,35 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV disposable = MetaDisposable() self.resolvePeerByNameDisposable = disposable } - disposable.set((resolvePeerByName(account: self.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in + var resolveSignal = resolvePeerByName(account: self.account, name: name, ageLimit: 10) + + var cancelImpl: (() -> Void)? + let presentationData = self.presentationData + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + resolveSignal = resolveSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { [weak self] in + self?.resolvePeerByNameDisposable?.set(nil) + } + disposable.set((resolveSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in if let strongSelf = self { if let peerId = peerId { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: nil)) diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 629698aa01..b84ee5ae7f 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -348,6 +348,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.panRecognizer = recognizer self.view.addGestureRecognizer(recognizer) + + self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let strongSelf = self else { + return false + } + if let _ = strongSelf.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { + return true + } + return false + } } private func updateIsEmpty(_ isEmpty: Bool, animated: Bool) { @@ -1377,6 +1387,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } func dismissInput() { + if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { + return + } + switch self.chatPresentationInterfaceState.inputMode { case .none: break diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index e7247d11e7..17a9521b6f 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -383,8 +383,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } } - copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) - surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false) + surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.025, removeOnCompletion: false) copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) @@ -404,7 +404,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { intermediateCompletion() }) - self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false) transformedFrame.origin = CGPoint() self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index fe14fb2e6d..db24cf944d 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -371,8 +371,9 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie } })*/ - navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated) - strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated, completion: { [weak self] in + self?.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) + }) } } } diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index bc0d3b9bf6..2959603b5c 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -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) { diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index 19502b0d65..a80593dc5c 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -25,6 +25,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private var iconNode: TransformImageNode? private var statusNode: RadialStatusNode? + private var streamingStatusNode: RadialStatusNode? private var tapRecognizer: UITapGestureRecognizer? private let statusDisposable = MetaDisposable() @@ -35,11 +36,16 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private let fetchDisposable = MetaDisposable() var activateLocalContent: () -> Void = { } + var requestUpdateLayout: (Bool) -> Void = { _ in } private var account: Account? private var message: Message? private var themeAndStrings: (ChatPresentationThemeData, PresentationStrings)? private var file: TelegramMediaFile? + private var progressFrame: CGRect? + private var streamingCacheStatusFrame: CGRect? + private var fileIconImage: UIImage? + private var cloudFetchIconImage: UIImage? override init() { self.titleNode = TextNode() @@ -77,9 +83,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.tapRecognizer = tapRecognizer } + @objc func cacheProgressPressed() { + guard let resourceStatus = self.resourceStatus else { + return + } + switch resourceStatus.fetchStatus { + case .Fetching: + if let cancel = self.fetchControls.with({ return $0?.cancel }) { + cancel() + } + case .Remote: + if let fetch = self.fetchControls.with({ return $0?.fetch }) { + fetch() + } + case .Local: + break + } + } + @objc func progressPressed() { if let resourceStatus = self.resourceStatus { - switch resourceStatus { + switch resourceStatus.mediaStatus { case let .fetchStatus(fetchStatus): if let account = self.account, let message = self.message, message.flags.isSending { let _ = account.postbox.transaction({ transaction -> Void in @@ -109,7 +133,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { @objc func fileTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.progressPressed() + if let streamingCacheStatusFrame = self.streamingCacheStatusFrame, streamingCacheStatusFrame.contains(recognizer.location(in: self.view)) { + self.cacheProgressPressed() + } else { + self.progressPressed() + } } } @@ -122,6 +150,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let currentMessage = self.message let currentTheme = self.themeAndStrings?.0 + let currentResourceStatus = self.resourceStatus return { account, presentationData, message, file, automaticDownload, incoming, isRecentActions, dateAndStatusType, constrainedSize in var updatedTheme: ChatPresentationThemeData? @@ -224,13 +253,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if case let .Audio(voice, duration, title, performer, waveform) = attribute { isAudio = true if let currentUpdatedStatusSignal = updatedStatusSignal { - updatedStatusSignal = currentUpdatedStatusSignal |> map { status in - switch status { + updatedStatusSignal = currentUpdatedStatusSignal + |> map { status in + switch status.mediaStatus { case let .fetchStatus(fetchStatus): if !voice { - return .fetchStatus(.Local) + return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus) } else { - return .fetchStatus(fetchStatus) + return FileMediaResourceStatus(mediaStatus: .fetchStatus(fetchStatus), fetchStatus: status.fetchStatus) } case .playbackStatus: return status @@ -289,6 +319,23 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { textConstrainedSize.width -= 80.0 } + let streamingProgressDiameter: CGFloat = 28.0 + var hasStreamingProgress = false + if isAudio && !isVoice { + if let resourceStatus = currentResourceStatus { + switch resourceStatus.fetchStatus { + case .Fetching, .Remote: + hasStreamingProgress = true + case .Local: + break + } + } + + if hasStreamingProgress { + textConstrainedSize.width -= streamingProgressDiameter + 4.0 + } + } + let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -311,6 +358,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { minLayoutWidth = max(minLayoutWidth, statusSize.width) } + var cloudFetchIconImage: UIImage? + if hasStreamingProgress { + minLayoutWidth += streamingProgressDiameter + 4.0 + cloudFetchIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchOutgoingIcon(presentationData.theme.theme) + } + let fileIconImage: UIImage? if hasThumbnail { fileIconImage = nil @@ -325,6 +378,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { var iconFrame: CGRect? let progressFrame: CGRect + let streamingCacheStatusFrame: CGRect let controlAreaWidth: CGFloat if hasThumbnail { @@ -362,7 +416,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0) } else { let unionSize = titleFrame.union(descriptionFrame).union(progressFrame).size - fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 4.0) + fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 6.0) } var statusFrame: CGRect? @@ -376,6 +430,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0) } + if isAudio && !isVoice { + streamingCacheStatusFrame = CGRect(origin: CGPoint(x: fittedLayoutSize.width + 6.0, y: 4.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter)) + if hasStreamingProgress { + fittedLayoutSize.width += streamingProgressDiameter + 6.0 + } + } else { + streamingCacheStatusFrame = CGRect() + } + return (fittedLayoutSize, { [weak self] in if let strongSelf = self { strongSelf.account = account @@ -468,78 +531,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { + var previousHadCacheStatus = false + if let resourceStatus = strongSelf.resourceStatus { + switch resourceStatus.fetchStatus { + case .Fetching, .Remote: + previousHadCacheStatus = true + case .Local: + previousHadCacheStatus = false + } + } + var hasCacheStatus = false + switch status.fetchStatus { + case .Fetching, .Remote: + hasCacheStatus = true + case .Local: + hasCacheStatus = false + } strongSelf.resourceStatus = status - - if strongSelf.statusNode == nil { - let backgroundNodeColor: UIColor - if strongSelf.iconNode != nil { - backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor - } else if incoming { - backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor - } else { - backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor - } - let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor) - strongSelf.statusNode = statusNode - statusNode.frame = progressFrame - strongSelf.addSubnode(statusNode) - } else if let _ = updatedTheme { - //strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage)) - } - - let state: RadialStatusNodeState - let statusForegroundColor: UIColor - if strongSelf.iconNode != nil { - statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor - } else if incoming { - statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.incoming.withoutWallpaper.fill : bubbleTheme.incoming.withWallpaper.fill + if isAudio && !isVoice && previousHadCacheStatus != hasCacheStatus { + strongSelf.requestUpdateLayout(false) } else { - statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill - } - switch status { - case let .fetchStatus(fetchStatus): - strongSelf.waveformScrubbingNode?.enableScrubbing = false - switch fetchStatus { - case let .Fetching(isActive, progress): - var adjustedProgress = progress - if isActive { - adjustedProgress = max(adjustedProgress, 0.027) - } - state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true) - case .Local: - if isAudio { - state = .play(statusForegroundColor) - } else if let fileIconImage = fileIconImage { - state = .customIcon(fileIconImage) - } else { - state = .none - } - case .Remote: - if isAudio && !isVoice { - state = .play(statusForegroundColor) - } else { - state = .download(statusForegroundColor) - } - } - case let .playbackStatus(playbackStatus): - strongSelf.waveformScrubbingNode?.enableScrubbing = true - switch playbackStatus { - case .playing: - state = .pause(statusForegroundColor) - case .paused: - state = .play(statusForegroundColor) - } - } - - if let statusNode = strongSelf.statusNode { - if state == .none { - strongSelf.statusNode = nil - } - statusNode.transitionToState(state, completion: { [weak statusNode] in - if state == .none { - statusNode?.removeFromSupernode() - } - }) + strongSelf.updateStatus() } } } @@ -551,6 +563,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } strongSelf.statusNode?.frame = progressFrame + strongSelf.progressFrame = progressFrame + strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame + strongSelf.fileIconImage = fileIconImage + strongSelf.cloudFetchIconImage = cloudFetchIconImage if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) @@ -558,6 +574,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { updatedFetchControls.fetch() } } + + strongSelf.updateStatus() } }) }) @@ -565,6 +583,158 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } + private func updateStatus() { + guard let resourceStatus = self.resourceStatus else { + return + } + guard let message = self.message else { + return + } + guard let account = self.account else { + return + } + guard let presentationData = self.themeAndStrings?.0 else { + return + } + guard let progressFrame = self.progressFrame, let streamingCacheStatusFrame = self.streamingCacheStatusFrame else { + return + } + guard let file = self.file else { + return + } + let incoming = message.effectivelyIncoming(account.peerId) + let bubbleTheme = presentationData.theme.chat.bubble + + var isAudio = false + var isVoice = false + for attribute in file.attributes { + if case let .Audio(voice, _, _, _, _) = attribute { + isAudio = true + + if voice { + isVoice = true + } + break + } + } + + let state: RadialStatusNodeState + var streamingState: RadialStatusNodeState = .none + + if isAudio && !isVoice { + let streamingStatusForegroundColor: UIColor = incoming ? bubbleTheme.incomingAccentControlColor : bubbleTheme.outgoingAccentControlColor + let streamingStatusBackgroundColor: UIColor = incoming ? bubbleTheme.incomingMediaInactiveControlColor : bubbleTheme.outgoingMediaInactiveControlColor + switch resourceStatus.fetchStatus { + case let .Fetching(isActive, progress): + var adjustedProgress = progress + if isActive { + adjustedProgress = max(adjustedProgress, 0.027) + } + streamingState = .cloudProgress(color: streamingStatusForegroundColor, strokeBackgroundColor: streamingStatusBackgroundColor, lineWidth: 2.0, value: CGFloat(adjustedProgress)) + case .Local: + streamingState = .none + case .Remote: + if let cloudFetchIconImage = self.cloudFetchIconImage { + streamingState = .customIcon(cloudFetchIconImage) + } else { + streamingState = .none + } + } + } else { + streamingState = .none + } + + let statusForegroundColor: UIColor + if self.iconNode != nil { + statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor + } else if incoming { + statusForegroundColor = presentationData.wallpaper.isEmpty ? bubbleTheme.incoming.withoutWallpaper.fill : bubbleTheme.incoming.withWallpaper.fill + } else { + statusForegroundColor = presentationData.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill + } + switch resourceStatus.mediaStatus { + case let .fetchStatus(fetchStatus): + self.waveformScrubbingNode?.enableScrubbing = false + switch fetchStatus { + case let .Fetching(isActive, progress): + var adjustedProgress = progress + if isActive { + adjustedProgress = max(adjustedProgress, 0.027) + } + state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true) + case .Local: + if isAudio { + state = .play(statusForegroundColor) + } else if let fileIconImage = self.fileIconImage { + state = .customIcon(fileIconImage) + } else { + state = .none + } + case .Remote: + if isAudio && !isVoice { + state = .play(statusForegroundColor) + } else { + state = .download(statusForegroundColor) + } + } + case let .playbackStatus(playbackStatus): + self.waveformScrubbingNode?.enableScrubbing = true + switch playbackStatus { + case .playing: + state = .pause(statusForegroundColor) + case .paused: + state = .play(statusForegroundColor) + } + } + + if state != .none && self.statusNode == nil { + let backgroundNodeColor: UIColor + if self.iconNode != nil { + backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor + } else if incoming { + backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor + } else { + backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor + } + let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor) + self.statusNode = statusNode + statusNode.frame = progressFrame + self.addSubnode(statusNode) + } + + if streamingState != .none && self.streamingStatusNode == nil { + let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear) + self.streamingStatusNode = streamingStatusNode + streamingStatusNode.frame = streamingCacheStatusFrame + self.addSubnode(streamingStatusNode) + } + + if let statusNode = self.statusNode { + if state == .none { + self.statusNode = nil + } + statusNode.transitionToState(state, completion: { [weak statusNode] in + if state == .none { + statusNode?.removeFromSupernode() + } + }) + } + + if let streamingStatusNode = self.streamingStatusNode { + if streamingState == .none { + self.streamingStatusNode = nil + streamingStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak streamingStatusNode] _ in + if streamingState == .none { + streamingStatusNode?.removeFromSupernode() + } + }) + } else { + streamingStatusNode.transitionToState(streamingState, completion: { + }) + } + } + } + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() diff --git a/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift b/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift index 20bdf2226c..41fc64fd37 100644 --- a/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift +++ b/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift @@ -44,7 +44,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { private let infoBackgroundNode: ASImageNode private let muteIconNode: ASImageNode - private var status: FileMediaResourceStatus? + private var status: FileMediaResourceMediaStatus? private let playbackStatusDisposable = MetaDisposable() private var shouldAcquireVideoContext: Bool { @@ -178,16 +178,16 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { var updatedPlaybackStatus: Signal? if let updatedFile = updatedFile, updatedMedia { updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message, isRecentActions: item.associatedData.isRecentActions), item.account.pendingMessageManager.pendingMessageStatus(item.message.id)) - |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in - if let pendingStatus = pendingStatus { - var progress = pendingStatus.progress - if pendingStatus.isRunning { - progress = max(progress, 0.27) - } - return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress)) - } else { - return resourceStatus + |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in + if let pendingStatus = pendingStatus { + var progress = pendingStatus.progress + if pendingStatus.isRunning { + progress = max(progress, 0.27) } + return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress)), fetchStatus: resourceStatus.fetchStatus) + } else { + return resourceStatus + } } } @@ -280,7 +280,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { guard let strongSelf = self else { return } - strongSelf.status = status + strongSelf.status = status.mediaStatus strongSelf.updateStatus() })) } diff --git a/TelegramUI/ChatTextInputMediaRecordingButton.swift b/TelegramUI/ChatTextInputMediaRecordingButton.swift index 5cf2979b3e..2935b10346 100644 --- a/TelegramUI/ChatTextInputMediaRecordingButton.swift +++ b/TelegramUI/ChatTextInputMediaRecordingButton.swift @@ -238,6 +238,8 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto super.init(frame: CGRect()) + self.disablesInteractiveTransitionGestureRecognizer = true + let inputPanelTheme = theme.chat.inputPanel self.pallete = TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor) diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index c6ac3b82e8..6a09c15f31 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -7,7 +7,7 @@ import SwiftSignalKit import LegacyComponents enum ChatTitleContent { - case peer(PeerView) + case peer(peerView: PeerView, onlineMemberCount: Int32?) case group([Peer]) } @@ -207,14 +207,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.insertSubview(statusNode.view, belowSubview: self.button.view) } switch self.networkState { - case .waitingForNetwork: - statusNode.title = self.strings.State_WaitingForNetwork - case .connecting: - statusNode.title = self.strings.State_Connecting - case .updating: - statusNode.title = self.strings.State_Updating - case .online: - break + case .waitingForNetwork: + statusNode.title = self.strings.State_WaitingForNetwork + case .connecting: + statusNode.title = self.strings.State_Connecting + case .updating: + statusNode.title = self.strings.State_Updating + case .online: + break } } @@ -246,7 +246,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { var titleLeftIcon: ChatTitleIcon = .none var titleRightIcon: ChatTitleIcon = .none switch titleContent { - case let .peer(peerView): + case let .peer(peerView, _): if let peer = peerViewMainPeer(peerView) { if peerView.peerId == self.account.peerId { string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) @@ -302,7 +302,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { var shouldUpdateLayout = false if let titleContent = self.titleContent { switch titleContent { - case let .peer(peerView): + case let .peer(peerView, onlineMemberCount): if let peer = peerViewMainPeer(peerView) { if peer.id == self.account.peerId { let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) @@ -379,17 +379,27 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } else if let channel = peer as? TelegramChannel { if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - let membersString: String - if case .group = channel.info { - membersString = strings.Conversation_StatusMembers(memberCount) + if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { + let string = NSMutableAttributedString() + + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } } else { - membersString = strings.Conversation_StatusSubscribers(memberCount) - } - - let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { - self.infoNode.attributedText = string - shouldUpdateLayout = true + let membersString: String + if case .group = channel.info { + membersString = strings.Conversation_StatusMembers(memberCount) + } else { + membersString = strings.Conversation_StatusSubscribers(memberCount) + } + let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } } } else { switch channel.info { diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index b0d2d6af0d..6929f7339c 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -620,7 +620,7 @@ final class ContactListNode: ASDisplayNode { } return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get()) - |> mapToQueue { localPeers, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal in + |> mapToQueue { localPeersAndStatuses, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal in let signal = deferred { () -> Signal in var existingPeerIds = Set() var disabledPeerIds = Set() @@ -628,17 +628,17 @@ final class ContactListNode: ASDisplayNode { var existingNormalizedPhoneNumbers = Set() for filter in filters { switch filter { - case .excludeSelf: - existingPeerIds.insert(account.peerId) - case let .exclude(peerIds): - existingPeerIds = existingPeerIds.union(peerIds) - case let .disable(peerIds): - disabledPeerIds = disabledPeerIds.union(peerIds) + case .excludeSelf: + existingPeerIds.insert(account.peerId) + case let .exclude(peerIds): + existingPeerIds = existingPeerIds.union(peerIds) + case let .disable(peerIds): + disabledPeerIds = disabledPeerIds.union(peerIds) } } var peers: [ContactListPeer] = [] - for peer in localPeers { + for peer in localPeersAndStatuses.0 { if !existingPeerIds.contains(peer.id) { existingPeerIds.insert(peer.id) peers.append(.peer(peer: peer, isGlobal: false)) @@ -680,7 +680,7 @@ final class ContactListNode: ASDisplayNode { peers.append(.deviceContact(stableId, contact)) } - let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: [:], presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds) + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false)) } diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index 1323757d22..8431e1b512 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -14,6 +14,7 @@ private enum ContactListSearchGroup { private struct ContactListSearchEntry: Identifiable, Comparable { let index: Int let peer: ContactListPeer + let presence: PeerPresence? let group: ContactListSearchGroup let enabled: Bool @@ -28,6 +29,13 @@ private struct ContactListSearchEntry: Identifiable, Comparable { if lhs.peer != rhs.peer { return false } + if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence { + if !lhsPresence.isEqual(to: rhsPresence) { + return false + } + } else if (lhs.presence != nil) != (rhs.presence != nil) { + return false + } if lhs.group != rhs.group { return false } @@ -41,13 +49,17 @@ private struct ContactListSearchEntry: Identifiable, Comparable { return lhs.index < rhs.index } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, openPeer: @escaping (ContactListPeer) -> Void) -> ListViewItem { + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void) -> ListViewItem { let header: ListViewItemHeader let status: ContactsPeerItemStatus switch self.group { case .contacts: header = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil) - status = .none + if let presence = self.presence { + status = .presence(presence, timeFormat) + } else { + status = .none + } case .global: header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil) if case let .peer(peer, _) = self.peer, let _ = peer.addressName { @@ -80,12 +92,12 @@ struct ContactListSearchContainerTransition { let isSearching: Bool } -private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, openPeer: @escaping (ContactListPeer) -> Void) -> ContactListSearchContainerTransition { +private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void) -> ContactListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, openPeer: openPeer), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, openPeer: openPeer), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer), directionHint: nil) } return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -170,7 +182,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, themeAndStringsPromise.get()) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) - |> map { localPeers, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in + |> map { localPeersAndPresences, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in var entries: [ContactListSearchEntry] = [] var existingPeerIds = Set() var disabledPeerIds = Set() @@ -186,7 +198,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { } var existingNormalizedPhoneNumbers = Set() var index = 0 - for peer in localPeers { + for peer in localPeersAndPresences.0 { if existingPeerIds.contains(peer.id) { continue } @@ -195,7 +207,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { if onlyWriteable { enabled = canSendMessagesToPeer(peer) } - entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer, isGlobal: false), group: .contacts, enabled: enabled)) + entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer, isGlobal: false), presence: localPeersAndPresences.1[peer.id], group: .contacts, enabled: enabled)) if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -214,7 +226,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { enabled = canSendMessagesToPeer(peer.peer) } - entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), group: .global, enabled: enabled)) + entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), presence: nil, group: .global, enabled: enabled)) if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -233,7 +245,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { enabled = canSendMessagesToPeer(peer.peer) } - entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), group: .global, enabled: enabled)) + entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), presence: nil, group: .global, enabled: enabled)) if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -249,7 +261,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { continue outer } } - entries.append(ContactListSearchEntry(index: index, peer: .deviceContact(stableId, contact), group: .deviceContacts, enabled: true)) + entries.append(ContactListSearchEntry(index: index, peer: .deviceContact(stableId, contact), presence: nil, group: .deviceContacts, enabled: true)) index += 1 } } @@ -263,17 +275,17 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { let previousSearchItems = Atomic<[ContactListSearchEntry]>(value: []) self.searchDisposable.set((searchItems - |> deliverOnMainQueue).start(next: { [weak self] items in - if let strongSelf = self { - let previousItems = previousSearchItems.swap(items ?? []) - - let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, openPeer: { peer in self?.listNode.clearHighlightAnimated(true) - self?.openPeer(peer) - }) - - strongSelf.enqueueTransition(transition) - } - })) + |> deliverOnMainQueue).start(next: { [weak self] items in + if let strongSelf = self { + let previousItems = previousSearchItems.swap(items ?? []) + + let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, openPeer: { peer in self?.listNode.clearHighlightAnimated(true) + self?.openPeer(peer) + }) + + strongSelf.enqueueTransition(transition) + } + })) self.listNode.beganInteractiveDragging = { [weak self] in self?.dismissInput?() diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index af2759a10a..989b63e502 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -172,7 +172,7 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe context.requestedCompleteFetch = false } else { if streamable { - context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: context.readingOffset ..< resourceSize, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) + context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: context.readingOffset ..< Int(Int32.max), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) } else if !context.requestedCompleteFetch && context.fetchAutomatically { context.requestedCompleteFetch = true context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) @@ -236,7 +236,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1) if streamable { - self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: 0 ..< resourceSize, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) + self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: 0 ..< Int(Int32.max), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } else if !self.requestedCompleteFetch && self.fetchAutomatically { self.requestedCompleteFetch = true self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) @@ -308,6 +308,26 @@ final class FFMpegMediaFrameSourceContext: NSObject { } } } + } else if codecPar.pointee.codec_id == AV_CODEC_ID_MPEG4 { + if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromMpeg4CodecData(UInt32(kCMVideoCodecType_MPEG4Video), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size) { + let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000)) + + let duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale) + + var rotationAngle: Double = 0.0 + if let rotationInfo = av_dict_get(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.metadata, "rotate", nil, 0), let value = rotationInfo.pointee.value { + if strcmp(value, "0") != 0 { + if let angle = Double(String(cString: value)) { + rotationAngle = angle * Double.pi / 180.0 + } + } + } + + let aspect = Double(codecPar.pointee.width) / Double(codecPar.pointee.height) + + videoStream = StreamContext(index: streamIndex, codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) + break + } } else if codecPar.pointee.codec_id == AV_CODEC_ID_H264 { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAVCCodecData(UInt32(kCMVideoCodecType_H264), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size) { let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000)) diff --git a/TelegramUI/FFMpegMediaFrameSourceContextHelpers.swift b/TelegramUI/FFMpegMediaFrameSourceContextHelpers.swift index d5ccaf6de5..f05866ce91 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContextHelpers.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContextHelpers.swift @@ -40,6 +40,35 @@ final class FFMpegMediaFrameSourceContextHelpers { return formatDescription } + static func createFormatDescriptionFromMpeg4CodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: UnsafePointer, _ 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, _ extradata_size: Int32) -> CMFormatDescription? { let par = NSMutableDictionary() par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString) diff --git a/TelegramUI/FetchVideoMediaResource.swift b/TelegramUI/FetchVideoMediaResource.swift index 23530a8d01..fd874dd1ac 100644 --- a/TelegramUI/FetchVideoMediaResource.swift +++ b/TelegramUI/FetchVideoMediaResource.swift @@ -208,6 +208,7 @@ public func fetchVideoLibraryMediaResourceHash(resource: VideoLibraryMediaResour if fetchResult.count != 0 { let asset = fetchResult.object(at: 0) let option = PHVideoRequestOptions() + option.isNetworkAccessAllowed = true option.deliveryMode = .highQualityFormat let alreadyReceivedAsset = Atomic(value: false) diff --git a/TelegramUI/FileMediaResourceStatus.swift b/TelegramUI/FileMediaResourceStatus.swift index 8db73857a0..273f728a64 100644 --- a/TelegramUI/FileMediaResourceStatus.swift +++ b/TelegramUI/FileMediaResourceStatus.swift @@ -8,7 +8,12 @@ enum FileMediaResourcePlaybackStatus { case paused } -enum FileMediaResourceStatus { +struct FileMediaResourceStatus { + let mediaStatus: FileMediaResourceMediaStatus + let fetchStatus: MediaResourceStatus +} + +enum FileMediaResourceMediaStatus { case fetchStatus(MediaResourceStatus) case playbackStatus(FileMediaResourcePlaybackStatus) } @@ -50,45 +55,49 @@ func messageFileMediaResourceStatus(account: Account, file: TelegramMediaFile, m if message.flags.isSending { return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), account.pendingMessageManager.pendingMessageStatus(message.id), playbackStatus) - |> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in - if let playbackStatus = playbackStatus { - switch playbackStatus { - case .playing: - return .playbackStatus(.playing) - case .paused: - return .playbackStatus(.paused) - case let .buffering(_, whilePlaying): - if whilePlaying { - return .playbackStatus(.playing) - } else { - return .playbackStatus(.paused) - } - } - } else if let pendingStatus = pendingStatus { - return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress)) - } else { - return .fetchStatus(resourceStatus) + |> map { resourceStatus, pendingStatus, playbackStatus -> FileMediaResourceStatus in + let mediaStatus: FileMediaResourceMediaStatus + if let playbackStatus = playbackStatus { + switch playbackStatus { + case .playing: + mediaStatus = .playbackStatus(.playing) + case .paused: + mediaStatus = .playbackStatus(.paused) + case let .buffering(_, whilePlaying): + if whilePlaying { + mediaStatus = .playbackStatus(.playing) + } else { + mediaStatus = .playbackStatus(.paused) + } } + } else if let pendingStatus = pendingStatus { + mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress)) + } else { + mediaStatus = .fetchStatus(resourceStatus) + } + return FileMediaResourceStatus(mediaStatus: mediaStatus, fetchStatus: resourceStatus) } } else { return combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), playbackStatus) - |> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in - if let playbackStatus = playbackStatus { - switch playbackStatus { - case .playing: - return .playbackStatus(.playing) - case .paused: - return .playbackStatus(.paused) - case let .buffering(_, whilePlaying): - if whilePlaying { - return .playbackStatus(.playing) - } else { - return .playbackStatus(.paused) - } - } - } else { - return .fetchStatus(resourceStatus) + |> map { resourceStatus, playbackStatus -> FileMediaResourceStatus in + let mediaStatus: FileMediaResourceMediaStatus + if let playbackStatus = playbackStatus { + switch playbackStatus { + case .playing: + mediaStatus = .playbackStatus(.playing) + case .paused: + mediaStatus = .playbackStatus(.paused) + case let .buffering(_, whilePlaying): + if whilePlaying { + mediaStatus = .playbackStatus(.playing) + } else { + mediaStatus = .playbackStatus(.paused) + } } + } else { + mediaStatus = .fetchStatus(resourceStatus) + } + return FileMediaResourceStatus(mediaStatus: mediaStatus, fetchStatus: resourceStatus) } } } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 34ff6d6ba6..3eff758772 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -131,9 +131,13 @@ func galleryItemForEntry(account: Account, presentationData: PresentationData, e if file.isVideo || supportedVideoMimeTypes.contains(file.mimeType) { let content: UniversalVideoContent if file.isAnimated { - content = NativeVideoContent(id: .message(message.id, message.stableId + 1, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: true) + content = NativeVideoContent(id: .message(message.id, message.stableId + 1, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: true, enableSound: false) } else { - content = NativeVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos) + if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { + content = NativeVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos) + } else { + content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos) + } } return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text, hideControls: hideControls, playbackCompleted: playbackCompleted) } else { diff --git a/TelegramUI/InAppNotificationSettings.swift b/TelegramUI/InAppNotificationSettings.swift index 3e31b325de..04d89dbda9 100644 --- a/TelegramUI/InAppNotificationSettings.swift +++ b/TelegramUI/InAppNotificationSettings.swift @@ -38,7 +38,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0 self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0 self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("tds", orElse: 0)) ?? .filtered - self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 0)) ?? .chats + self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 0)) ?? .messages self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0 } diff --git a/TelegramUI/LegacyComponentsStickers.swift b/TelegramUI/LegacyComponentsStickers.swift index 2d5a479baa..f44da3e5ab 100644 --- a/TelegramUI/LegacyComponentsStickers.swift +++ b/TelegramUI/LegacyComponentsStickers.swift @@ -157,7 +157,7 @@ final class LegacyStickerImageDataSource: TGImageDataSource { attributes.append(.Sticker(displayText: "", packReference: .id(id: stickerPackId, accessHash: stickerPackAccessHash), maskData: nil)) } - return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: attributes), small: !highQuality, fitSize: fitSize, completion: { image in + return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil, fileName: fileNameFromFileAttributes(attributes)), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: attributes), small: !highQuality, fitSize: fitSize, completion: { image in if let image = image { sharedImageCache.setImage(image, forKey: uri, attributes: nil) completion?(TGDataResource(image: image, decoded: true)) diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift index 9e7ba8d6d6..b0af8e32a0 100644 --- a/TelegramUI/LegacyInstantVideoController.swift +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -92,6 +92,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, legacyController.bind(controller: baseController) legacyController.presentationCompleted = { [weak legacyController, weak baseController] in if let legacyController = legacyController, let baseController = baseController { + legacyController.view.disablesInteractiveTransitionGestureRecognizer = true let inputPanelTheme = theme.chat.inputPanel var uploadInterface: LegacyLiveUploadInterface? if peerId.namespace != Namespaces.Peer.SecretChat { diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index 8874e1034a..976a1ee9c4 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -154,7 +154,7 @@ final class ListMessageFileItemNode: ListMessageNode { private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) - private var resourceStatus: FileMediaResourceStatus? + private var resourceStatus: FileMediaResourceMediaStatus? private let fetchDisposable = MetaDisposable() private var downloadStatusIconNode: ASImageNode @@ -396,10 +396,11 @@ final class ListMessageFileItemNode: ListMessageNode { if isAudio { if let currentUpdatedStatusSignal = updatedStatusSignal { - updatedStatusSignal = currentUpdatedStatusSignal |> map { status in - switch status { + updatedStatusSignal = currentUpdatedStatusSignal + |> map { status in + switch status.mediaStatus { case .fetchStatus: - return .fetchStatus(.Local) + return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus) case .playbackStatus: return status } @@ -571,7 +572,9 @@ final class ListMessageFileItemNode: ListMessageNode { } if let updatedStatusSignal = updatedStatusSignal { - strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in + strongSelf.statusDisposable.set((updatedStatusSignal + |> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in + let status = fileStatus.mediaStatus displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.resourceStatus = status diff --git a/TelegramUI/NavigateToChatController.swift b/TelegramUI/NavigateToChatController.swift index 5ba4e726c6..64b120b68e 100644 --- a/TelegramUI/NavigateToChatController.swift +++ b/TelegramUI/NavigateToChatController.swift @@ -9,7 +9,7 @@ public enum NavigateToChatKeepStack { case never } -public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (()-> Void)? = nil, animated: Bool = true) { +public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, animated: Bool = true, completion: @escaping () -> Void = {}) { var found = false var isFirst = true for controller in navigationController.viewControllers.reversed() { @@ -24,6 +24,7 @@ public func navigateToChatController(navigationController: NavigationController, } else { let _ = navigationController.popToViewController(controller, animated: animated) } + completion() found = true break } @@ -48,9 +49,9 @@ public func navigateToChatController(navigationController: NavigationController, resolvedKeepStack = false } if resolvedKeepStack { - navigationController.pushViewController(controller) + navigationController.pushViewController(controller, completion: completion) } else { - navigationController.replaceAllButRootController(controller, animated: animated) + navigationController.replaceAllButRootController(controller, animated: animated, completion: completion) } } } diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 8a401f310d..c413e09938 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -137,6 +137,47 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic } let continueHandling: () -> Void = { + let handleInternalUrl: (String) -> Void = { url in + let _ = (resolveUrl(account: account, url: url) + |> deliverOnMainQueue).start(next: { resolved in + if case let .externalUrl(value) = resolved { + applicationContext.applicationBindings.openUrl(value) + } else { + openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in + switch navigation { + case .info: + let _ = (account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + if let infoController = peerInfoController(account: account, peer: peer) { + if let navigationController = navigationController { + navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) + } + navigationController?.pushViewController(infoController) + } + }) + case .chat: + if let navigationController = navigationController { + navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + case let .withBotStartPayload(payload): + if let navigationController = navigationController { + navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), botStart: payload) + } + } + }, present: { c, a in + if let navigationController = navigationController { + navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) + (navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a) + } + }, dismissInput: { + dismissInput() + }) + } + }) + } + if parsedUrl.scheme == "tg", let query = parsedUrl.query { var convertedUrl: String? if parsedUrl.host == "localpeer" { @@ -418,62 +459,29 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic } if let convertedUrl = convertedUrl { - let _ = (resolveUrl(account: account, url: convertedUrl) - |> deliverOnMainQueue).start(next: { resolved in - if case let .externalUrl(value) = resolved { - applicationContext.applicationBindings.openUrl(value) - } else { - openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in - switch navigation { - case .info: - let _ = (account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { peer in - if let infoController = peerInfoController(account: account, peer: peer) { - if let navigationController = navigationController { - navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) - } - navigationController?.pushViewController(infoController) - } - }) - case .chat: - if let navigationController = navigationController { - navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) - navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) - } - case let .withBotStartPayload(payload): - if let navigationController = navigationController { - navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) - navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), botStart: payload) - } - } - }, present: { c, a in - if let navigationController = navigationController { - navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) - (navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a) - } - }, dismissInput: { - dismissInput() - }) - } - }) + handleInternalUrl(convertedUrl) } return } if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { - if #available(iOSApplicationExtension 9.0, *) { - if let window = navigationController?.view.window { - let controller = SFSafariViewController(url: parsedUrl) - if #available(iOSApplicationExtension 10.0, *) { - controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor - controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor - } - window.rootViewController?.present(controller, animated: true) - } else { - applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString) - } + if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" { + handleInternalUrl(parsedUrl.absoluteString) } else { - applicationContext.applicationBindings.openUrl(url) + if #available(iOSApplicationExtension 9.0, *) { + if let window = navigationController?.view.window { + let controller = SFSafariViewController(url: parsedUrl) + if #available(iOSApplicationExtension 10.0, *) { + controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor + controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor + } + window.rootViewController?.present(controller, animated: true) + } else { + applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString) + } + } else { + applicationContext.applicationBindings.openUrl(url) + } } } else { applicationContext.applicationBindings.openUrl(url) @@ -481,11 +489,16 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic } if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { - applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in - if !success { - continueHandling() - } - })) + let nativeHosts = ["t.me", "telegram.me"] + if let host = parsedUrl.host, nativeHosts.contains(host) { + continueHandling() + } else { + applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in + if !success { + continueHandling() + } + })) + } } else { continueHandling() } diff --git a/TelegramUI/OverlayStatusController.swift b/TelegramUI/OverlayStatusController.swift index e211fca5eb..c856b28c49 100644 --- a/TelegramUI/OverlayStatusController.swift +++ b/TelegramUI/OverlayStatusController.swift @@ -4,7 +4,7 @@ import Display import LegacyComponents enum OverlayStatusControllerType { - case loading + case loading(cancelled: (() -> Void)?) case success case proxySettingSuccess } @@ -47,12 +47,14 @@ private enum OverlayStatusContentController { } } - func dismiss() { + func dismiss(completion: @escaping () -> Void) { switch self { case let .loading(controller): - controller.dismiss(true) {} + controller.dismiss(true, completion: { + completion() + }) default: - break + completion() } } } @@ -64,8 +66,12 @@ private final class OverlayStatusControllerNode: ViewControllerTracingNode { init(theme: PresentationTheme, type: OverlayStatusControllerType, dismissed: @escaping () -> Void) { self.dismissed = dismissed switch type { - case .loading: - self.contentController = .loading(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light)) + case let .loading(cancelled): + let controller = TGProgressWindowController(light: theme.actionSheet.backgroundType == .light)! + controller.cancelled = { + cancelled?() + } + self.contentController = .loading(controller) case .success: self.contentController = .progress(TGProgressWindowController(light: theme.actionSheet.backgroundType == .light)) case .proxySettingSuccess: @@ -92,8 +98,9 @@ private final class OverlayStatusControllerNode: ViewControllerTracingNode { } func dismiss() { - self.contentController.dismiss() - self.dismissed() + self.contentController.dismiss(completion: { [weak self] in + self?.dismissed() + }) } } diff --git a/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift b/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift index e57a10e3fc..11520a0a1b 100644 --- a/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift +++ b/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift @@ -102,6 +102,56 @@ final class PeerChannelMemberCategoriesContextsManager { return self.getContext(postbox: postbox, network: network, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) } + func recentOnline(postbox: Postbox, network: Network, peerId: PeerId) -> Signal { + return Signal { [weak self] subscriber in + var previousIds: Set? + let statusesDisposable = MetaDisposable() + let disposableAndControl = self?.recent(postbox: postbox, network: network, peerId: peerId, updated: { state in + var idList: [PeerId] = [] + for item in state.list { + idList.append(item.peer.id) + if idList.count >= 200 { + break + } + } + let updatedIds = Set(idList) + if previousIds != updatedIds { + previousIds = updatedIds + let key: PostboxViewKey = .peerPresences(peerIds: updatedIds) + statusesDisposable.set((postbox.combinedView(keys: [key]) + |> map { view -> Int32 in + var count: Int32 = 0 + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + if let presences = (view.views[key] as? PeerPresencesView)?.presences { + for (_, presence) in presences { + if let presence = presence as? TelegramUserPresence { + let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) + switch relativeStatus { + case .online: + count += 1 + default: + break + } + } + } + } + return count + } + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { count in + subscriber.putNext(count) + })) + } + }) + return ActionDisposable { + disposableAndControl?.0.dispose() + statusesDisposable.dispose() + } + } + |> runOn(Queue.mainQueue()) + + } + func admins(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated) } diff --git a/TelegramUI/PlatformVideoContent.swift b/TelegramUI/PlatformVideoContent.swift new file mode 100644 index 0000000000..c5f9aca1ef --- /dev/null +++ b/TelegramUI/PlatformVideoContent.swift @@ -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() + var status: Signal { + return self._status.get() + } + + private let _bufferingStatus = Promise<(IndexSet, Int)?>() + var bufferingStatus: Signal<(IndexSet, Int)?, NoError> { + return self._bufferingStatus.get() + } + + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let _preloadCompleted = ValuePromise() + var preloadCompleted: Signal { + 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()) + + 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) { + } +} diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 2359777574..f306ec92c9 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -110,6 +110,9 @@ enum PresentationResourceKey: Int32 { case chatBubbleActionButtonOutgoingBottomRightImage case chatBubbleActionButtonOutgoingBottomSingleImage + case chatBubbleFileCloudFetchIncomingIcon + case chatBubbleFileCloudFetchOutgoingIcon + case chatBubbleReplyThumbnailPlayImage case chatInfoItemBackgroundImageWithWallpaper diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index 295fffc535..6f1fc9b850 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -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) + }) + } } diff --git a/TelegramUI/RadialCloudProgressContentNode.swift b/TelegramUI/RadialCloudProgressContentNode.swift new file mode 100644 index 0000000000..3cc0109aa6 --- /dev/null +++ b/TelegramUI/RadialCloudProgressContentNode.swift @@ -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) + } +} diff --git a/TelegramUI/RadialStatusNode.swift b/TelegramUI/RadialStatusNode.swift index 69e8d9822f..f33d7159b2 100644 --- a/TelegramUI/RadialStatusNode.swift +++ b/TelegramUI/RadialStatusNode.swift @@ -7,6 +7,7 @@ public enum RadialStatusNodeState: Equatable { case play(UIColor) case pause(UIColor) case progress(color: UIColor, lineWidth: CGFloat?, value: CGFloat?, cancelEnabled: Bool) + case cloudProgress(color: UIColor, strokeBackgroundColor: UIColor, lineWidth: CGFloat, value: CGFloat?) case check(UIColor) case customIcon(UIImage) case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double) @@ -43,6 +44,12 @@ public enum RadialStatusNodeState: Equatable { } else { return false } + case let .cloudProgress(lhsColor, lhsStrokeBackgroundColor, lhsLineWidth, lhsValue): + if case let .cloudProgress(rhsColor, rhsStrokeBackgroundColor, rhsLineWidth, rhsValue) = rhs, lhsColor.isEqual(rhsColor), lhsStrokeBackgroundColor.isEqual(rhsStrokeBackgroundColor), lhsLineWidth.isEqual(to: rhsLineWidth), lhsValue == rhsValue { + return true + } else { + return false + } case let .check(lhsColor): if case let .check(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { return true @@ -99,6 +106,18 @@ public enum RadialStatusNodeState: Equatable { node.progress = value return node } + case let .cloudProgress(color, strokeLineColor, lineWidth, value): + if let current = current as? RadialCloudProgressContentNode { + if !current.color.isEqual(color) { + current.color = color + } + current.progress = value + return current + } else { + let node = RadialCloudProgressContentNode(color: color, backgroundStrokeColor: strokeLineColor, lineWidth: lineWidth) + node.progress = value + return node + } case let .secretTimeout(color, icon, beginTime, timeout): return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon) } diff --git a/TelegramUI/SaveToCameraRoll.swift b/TelegramUI/SaveToCameraRoll.swift index 4ad937e8c2..a23648ed6f 100644 --- a/TelegramUI/SaveToCameraRoll.swift +++ b/TelegramUI/SaveToCameraRoll.swift @@ -9,15 +9,20 @@ import Display func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal { var resource: MediaResource? var isImage = true + var fileExtension: String? if let image = mediaReference.media as? TelegramMediaImage { if let representation = largestImageRepresentation(image.representations) { resource = representation.resource } } else if let file = mediaReference.media as? TelegramMediaFile { resource = file.resource - if file.isVideo { + if file.isVideo || file.mimeType.hasPrefix("video/") { isImage = false } + let maybeExtension = ((file.fileName ?? "") as NSString).pathExtension + if !maybeExtension.isEmpty { + fileExtension = maybeExtension + } } else if let webpage = mediaReference.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if let file = content.file { resource = file.resource @@ -34,7 +39,7 @@ func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: P if let resource = resource { let fetchedData: Signal = Signal { subscriber in let fetched = fetchedMediaResource(postbox: postbox, reference: mediaReference.resourceReference(resource)).start() - let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: true)).start(next: { next in + let data = postbox.mediaBox.resourceData(resource, pathExtension: fileExtension, option: .complete(waitUntilFetchStatus: true)).start(next: { next in subscriber.putNext(next) }, completed: { subscriber.putCompletion() diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 5b6ea6bfb5..1558804242 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -443,7 +443,7 @@ public func settingsController(account: Account, accountManager: AccountManager) let archivedPacks = Promise<[ArchivedStickerPackItem]?>() let openFaq: (Promise) -> Void = { resolvedUrl in - let controller = OverlayStatusController(theme: account.telegramApplicationContext.currentPresentationData.with { $0 }.theme, type: .loading) + let controller = OverlayStatusController(theme: account.telegramApplicationContext.currentPresentationData.with { $0 }.theme, type: .loading(cancelled: nil)) presentControllerImpl?(controller, nil) let _ = (resolvedUrl.get() |> take(1) diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index d19aaa1b48..2302d0d2c9 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -1251,7 +1251,35 @@ public func userInfoController(account: Account, peerId: PeerId, mode: UserInfoC navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(currentPeerId)) } } else { - createSecretChatDisposable.set((createSecretChat(account: account, peerId: peerId) |> deliverOnMainQueue).start(next: { peerId in + var createSignal = createSecretChat(account: account, peerId: peerId) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { subscriber in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + presentControllerImpl?(controller, nil) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + createSignal = createSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + createSecretChatDisposable.set(nil) + } + + createSecretChatDisposable.set((createSignal |> deliverOnMainQueue).start(next: { peerId in if let navigationController = (controller?.navigationController as? NavigationController) { navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) } diff --git a/TelegramUI/ZoomableContentGalleryItemNode.swift b/TelegramUI/ZoomableContentGalleryItemNode.swift index 5c239d6441..3532b03900 100644 --- a/TelegramUI/ZoomableContentGalleryItemNode.swift +++ b/TelegramUI/ZoomableContentGalleryItemNode.swift @@ -201,6 +201,11 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { if !self.ignoreZoom { self.centerScrollViewContents(transition: self.ignoreZoomTransition ?? .immediate) } + if self.scrollNode.view.zoomScale.isEqual(to: self.scrollNode.view.minimumZoomScale) { + self.scrollNode.view.isScrollEnabled = false + } else { + self.scrollNode.view.isScrollEnabled = true + } } override func contentSize() -> CGSize? {