From fc8fa045a61703fe6d0910279871d3b7f328e51c Mon Sep 17 00:00:00 2001 From: Peter <> Date: Sat, 13 Oct 2018 03:31:39 +0300 Subject: [PATCH] Fixed Apple Pay Added ability to download music without streaming Added progress indicators for various blocking tasks Fixed image gallery swipe to dismiss after zooming Added online member count indication in supergroups Fixed contact statuses in contact search --- .../FileCloudFetch.imageset/Contents.json | 12 + .../Message/FileCloudFetch.imageset/cloud.pdf | Bin 0 -> 4598 bytes TelegramUI.xcodeproj/project.pbxproj | 8 + TelegramUI/BotCheckoutControllerNode.swift | 89 +++-- .../ChannelMembersSearchContainerNode.swift | 6 +- TelegramUI/ChatController.swift | 204 +++++++---- TelegramUI/ChatControllerNode.swift | 14 + TelegramUI/ChatImageGalleryItem.swift | 6 +- TelegramUI/ChatListController.swift | 5 +- .../ChatMessageFileBubbleContentNode.swift | 8 + .../ChatMessageInteractiveFileNode.swift | 324 +++++++++++++---- ...atMessageInteractiveInstantVideoNode.swift | 22 +- .../ChatTextInputMediaRecordingButton.swift | 2 + TelegramUI/ChatTitleView.swift | 52 +-- TelegramUI/ContactListNode.swift | 18 +- TelegramUI/ContactsSearchContainerNode.swift | 56 +-- .../FFMpegMediaFrameSourceContext.swift | 24 +- ...FFMpegMediaFrameSourceContextHelpers.swift | 29 ++ TelegramUI/FetchVideoMediaResource.swift | 1 + TelegramUI/FileMediaResourceStatus.swift | 79 +++-- TelegramUI/GalleryController.swift | 8 +- TelegramUI/InAppNotificationSettings.swift | 2 +- TelegramUI/LegacyComponentsStickers.swift | 2 +- TelegramUI/LegacyInstantVideoController.swift | 1 + TelegramUI/ListMessageFileItemNode.swift | 13 +- TelegramUI/NavigateToChatController.swift | 7 +- TelegramUI/OpenUrl.swift | 123 ++++--- TelegramUI/OverlayStatusController.swift | 23 +- ...annelMemberCategoriesContextsManager.swift | 50 +++ TelegramUI/PlatformVideoContent.swift | 330 ++++++++++++++++++ TelegramUI/PresentationResourceKey.swift | 3 + TelegramUI/PresentationResourcesChat.swift | 12 + .../RadialCloudProgressContentNode.swift | 301 ++++++++++++++++ TelegramUI/RadialStatusNode.swift | 19 + TelegramUI/SaveToCameraRoll.swift | 9 +- TelegramUI/SettingsController.swift | 2 +- TelegramUI/UserInfoController.swift | 30 +- .../ZoomableContentGalleryItemNode.swift | 5 + 38 files changed, 1534 insertions(+), 365 deletions(-) create mode 100644 Images.xcassets/Chat/Message/FileCloudFetch.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Message/FileCloudFetch.imageset/cloud.pdf create mode 100644 TelegramUI/PlatformVideoContent.swift create mode 100644 TelegramUI/RadialCloudProgressContentNode.swift 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 0000000000000000000000000000000000000000..ae1fc2aa1fa6eb951326548d978f43e36c5cde1a GIT binary patch literal 4598 zcmai&2UHW=*2gJPAR?kt6j4T`Dv$(1iAoJc1f(M!(tyyVx6q_W6KPTukRnYf3JQn; z=^{!*ny7#Xp?8oLkQcoAUGKZ!T5r~@nKS>hv**k?d!O~&;nz`C6@`gOfcaa%{!Iv)s5KWx3t%n^TNtN^fh$`OE1LsD)JEA;r$~cS*76-`5f!#ga za409R53NOtmNUOJbJzv}TuQHeR#;Dd_P(_dA{cfP67=l4cn7EgDRg1KBpJs8?pBq$`RhE%QkekQ#J9dLYXe;l!{GXtX)%lc^hihYU)Io zdRf!tU&Oicj?CS!ds9SK8}G>!!jZmuVsEJKxjDWd-=^9x8}c>OvsaQ3fG)l}upqbI z%KNUi&7QNfmh#WVg)VSth7m48-AD5FN(2UnDr@Yqc?(&?-P0Q^XCUu$ zDaKrrpH9Zx3ejs-JKb)$7UrrdEVLONdBdgc+~x3F(Z;XSiJglEpd0?rJGxhw4sT7;MhF0v|d6_7prqANiCy>a!i-q2f zt#KcwL70N{)1*)$ zPJ8=$-(f4+kIOUWnRNN!f{ifKOiW9`9A^ASuUS&1;&(rU%sFFB_%t|^Xznwnb%M9U z4Yl~qGY*mT<<<7_q2XhBY3dMRjTNo>o4yK}_7{oz#=7UtJsvv7GDNUoXAJa~+gg@K zPY*ZQrr7RIYvv*AKh{vZD1;!vWt?_UL(S?k&+XlrYXxIZjXS`Y4LKi~#5>@dIBn#9n#Tl-{{DT8ZCc-qHVUaywa?K_%GU)h^-d8?*+MM&U zMKw7#(VjBNGQ4Va{L%!%bQ*|Ljl1Di9C1c8;4{o#B;vdt9GMbu@I)pLJD)+DGctN_ zkC|u{bXPu5cu#?t+VIAuA;`{`BJ^}mZum_(Gctm4&tS9m7Mh-=)mn>0+@TP6ghqbQ ztBe`@f^p8+UwK4r?Wf;<&h6i2#1Z?Q`%UpbGSk4<6$e0cP_{p>Za8NTK;o|~)x){F zc)DS5?ts)^0@B6VgWB&7>}R&#e#ZXtx8MIe({MltSrxDVAV?QS7dL%Z z6b1+EZv{wq7$E)2!5=nZ{3jm^w^TK0rdTL7ldn3ub z>usPv$Pq~*N)z87;|2KFxcr!a50|d6gLI?RgLUX4!UVjO52&&o6*j^eyrNZ$3DR*2ioVZZek(Mej zp2KC*b@C-VE4EH&ID>yQFBX(zp2q*xHZOnyM>J5+pdoJv_4I7s*Q9~6rCYt76v%Om z5?5h6xe&ENN5h!hf_Ul160?P{%cAiqupN&EMX93;jt->Z$y2XwwwEjQx6rT)$<11$ zw|q27YgNp*p`5|xE_YRE`AwYC?Golox##Ci`X}tR8U{zs1nTd~zV@RSE(^?&vJ_*p zOhz2%9z%#6H0yk(_fA*Vrx?;yIUQ6Wk3QC31ZY+c&{PfIUp*7yXqnjZjal$e%-o!# zJ8iau*o7%+N_MErn}!I)MtbK0#pqH2r+mmcb#@7y3b~-LKtuWMTV|G25_Wfkb#Lqy z0{^a~S>%59E6(NcDMQ_^!Y#2;m5w z8g+)(F0wSpbM$=9@_uxLSu<7`_;vkDyV8Vm_jqx?ACucWhs>j$M>Q3B^(WSu5koD@OS`*hloMxM63^U2TQop4+At|!B<%6@6wB1p~EYHo+`?Hqya0! zKGJ&Kw0EWH4e@&qx)v(sde9tn{ncUDL(IWy3gC!F4Jhq!2%a%?iIqK9vE&X%{*iR0 ze%4c_jJ`-k9T_(IR%Fy2)B~XonMuG*zYrNzWO|RwT45ErMii8^wbiKNVru~5~IRvM*415 zW5!d<%1U>P`Dl-xbx7dNrz=*;xWkfvsy6OQ!i{nIgONF?v=ba{C&wa9Vn@+NsDAWS zXkQv8%}A4TLdu3!B4jJ{A-eBLfwACYc`qiZqxa%OTa2vZt<5S@pU{6q&0XSVX@7O{ z8@Fp<%?WdHXUQ^9^^t+ty5QO7!=nPrJ}=J?vn_@mX>?yg2b`@32XP0T8HwjoQl#y{8^P+^VvZJma{Hs=OLcC6Ye{VC5qz9_ ze8TBfAqML9ycPno0>raP9T%}`ZYN9>EVaw{vH>gpBZ=}l$wC5b+E=+%lm@RMuGT8} zD)=HF>K!g}?lmq+dy#3XO;jdaB|y}YBvMyX$2v-r9wj)W%%=MDYbOt+dZ${6WeZxJ z4Z8ZHV5f#ow^5-{KP0chC`Wa)s!29nT_qzo1$_&hfsR7o{3h9|%l|SlB(eIKcCo^A z#RvX}XStMm9{PT$snV*t_zCw3dRk37pVE^w-MYpzMO`@%4kmQ|3%Cv&-jqgOxRJ@4ttJ${y-aI2alf;$5RnK*j%aAJ- z-X?i6!#SfPgPbu5Z?Cd5J@gD@N;aK*W(iG6UwCU^!(SPL$SEzGFBI*T%O83qTZ$>F zG`7vVR@rsjB2mmtTCv)^THjkqTLWSYu`1VH%#qX0F3rwwP-`#?dh_Z%@X0zx@m^z5 zPIOKKDhri2iaA%3@TmKIo@w5tHW|$G8_~;hV`c&sQJLzA>Tw-f`X76T4Xj^~sz}TZ z6LtddN0N2zxfNebOdhNDKR`5JejSkKdvEK)A=wz?m`si#4l9v)5q;5F5grp^lLN+1 zRWlXa&7WE_94s(|_6YkiOl4QW(8ROSV$l(=hIzuJ42y~(j=F1gq;-~cUWxz(#vmK% z6G>#*_~_v!R2N3*J32cxTGO>0JH{aBFZiTKy>KMcq)WK#a97?TX3izfUeOb7-8V*S zxZM)Ouwt=RKCU^vhM!Z$OlP;>Y7N9qdbK#U41JUx+^wNpIHGYRK`o{|xiD#xi*(5H zR%-e6gzM$ii5uue`^7@Ji*iwNadOA&z3LwkAe*RNx?QjBq1Az<$?XA(Hb@5am}v@} z4^jmgF~}VfXF7Jk6V%*D+gKOM1NmUZXWq*}r?i9g3ct)G$!x*C?S4W`1pZiH&bP+U zC(y?mB#Ks1Mz+*liGTg!wOULZhX#kYYKK~@N}rmRioZ&#ibkrjh)LzRYOHB2P$0{} zbBf)c&&#cN0S6G`zpe{B!L!Z;Qqkqu=XibSvtu?J5^1+LUUQ zbVrGzU1ltft&Z%D^Fub3rS_jwc`duYqdUJvf79mwk~lkJLb$#)^maXLp7?P6A;tTv z7o+!=q3IW^#_4Di%fa_$UoA?>Z&z|xs_U-L)f=JAZdf0|$7AEG#EYI1ua?`Z^%c(K z&xCNrXG{=h{Hy0PKec}zXc%z{>Y!^ZtG==Fw3{suw2nkx?IuX( zEMoav`zFs~7W3sn-)KU~wBT;``t$du(WW{5U;3k-=S$)JHT_4XgL^VhcbyiMzqc2# z7Q3FFQTfJJgOt$4e*YdK+dFVYsV#gtCWkkknw=F1;;q?~; z^6kCMQ}i(hw-?0f{r4=t$W9Z6ce@#ik66mT^^@7P+r`X{89&h|&L1Fapqk8%Eklh>#X01J8 zdQN*&J0&|#-aCM4XT?Xr$!20?BW#uOZ+_mV(4WvO1rz@by!-sRuTdda9f?#zx#O_F zKEUb$7QapQA^JBH|IOI$07MIi#iNv5d;l{j)kGXlJ%2*7CzZTm07L_ib*IAfKB7~- z{Q=0Zee-{MM4~)UjxM%;;k)}UZvPw0VX$An5OP9cwDkcaoSQq|#TkG>#b9v2Tu8|i z?}!CpGKvxin7J^Z>xpvn@B^sS{uA_lJcNIL3+#8nAf$Xz914?xL1E(JP;scFv?&xS zNIn0y{9}yNN&xGLq1O37$3ZRok<{|ugE|1^eT@I!%6y6c%_VdpaeY_ua z{-{Nfo{I}LX7=NPI+5zmHZIfv`m3!>4WY}DP>hX~4NOKFhQ-3r(g>7{l#Glt94;k= iK#L==7&-9&Hu), 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? {