Fixed Apple Pay

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,330 @@
import Foundation
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AVFoundation
enum PlatformVideoContentId: Hashable {
case message(MessageId, UInt32, MediaId)
case instantPage(MediaId, MediaId)
static func ==(lhs: PlatformVideoContentId, rhs: PlatformVideoContentId) -> Bool {
switch lhs {
case let .message(messageId, stableId, mediaId):
if case .message(messageId, stableId, mediaId) = rhs {
return true
} else {
return false
}
case let .instantPage(pageId, mediaId):
if case .instantPage(pageId, mediaId) = rhs {
return true
} else {
return false
}
}
}
var hashValue: Int {
switch self {
case let .message(messageId, _, mediaId):
return messageId.hashValue &* 31 &+ mediaId.hashValue
case let .instantPage(pageId, mediaId):
return pageId.hashValue &* 31 &+ mediaId.hashValue
}
}
}
final class PlatformVideoContent: UniversalVideoContent {
let id: AnyHashable
let nativeId: PlatformVideoContentId
let fileReference: FileMediaReference
let dimensions: CGSize
let duration: Int32
let streamVideo: Bool
let loopVideo: Bool
let enableSound: Bool
let baseRate: Double
let fetchAutomatically: Bool
init(id: PlatformVideoContentId, fileReference: FileMediaReference, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) {
self.id = id
self.nativeId = id
self.fileReference = fileReference
self.dimensions = fileReference.media.dimensions ?? CGSize(width: 128.0, height: 128.0)
self.duration = fileReference.media.duration ?? 0
self.streamVideo = streamVideo
self.loopVideo = loopVideo
self.enableSound = enableSound
self.baseRate = baseRate
self.fetchAutomatically = fetchAutomatically
}
func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically)
}
func isEqual(to other: UniversalVideoContent) -> Bool {
if let other = other as? PlatformVideoContent {
if case let .message(_, stableId, _) = self.nativeId {
if case .message(_, stableId, _) = other.nativeId {
if self.fileReference.media.isInstantVideo {
return true
}
}
}
}
return false
}
}
private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
private let postbox: Postbox
private let fileReference: FileMediaReference
private let approximateDuration: Double
private let intrinsicDimensions: CGSize
private let audioSessionManager: ManagedAudioSession
private let audioSessionDisposable = MetaDisposable()
private var hasAudioSession = false
private let playbackCompletedListeners = Bag<() -> Void>()
private var initializedStatus = false
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused)
private var isBuffering = false
private let _status = ValuePromise<MediaPlayerStatus>()
var status: Signal<MediaPlayerStatus, NoError> {
return self._status.get()
}
private let _bufferingStatus = Promise<(IndexSet, Int)?>()
var bufferingStatus: Signal<(IndexSet, Int)?, NoError> {
return self._bufferingStatus.get()
}
private let _ready = Promise<Void>()
var ready: Signal<Void, NoError> {
return self._ready.get()
}
private let _preloadCompleted = ValuePromise<Bool>()
var preloadCompleted: Signal<Bool, NoError> {
return self._preloadCompleted.get()
}
private let imageNode: TransformImageNode
private let playerItem: AVPlayerItem
private let player: AVPlayer
private let playerNode: ASDisplayNode
private var loadProgressDisposable: Disposable?
private var statusDisposable: Disposable?
private var didPlayToEndTimeObserver: NSObjectProtocol?
private let fetchDisposable = MetaDisposable()
private var dimensions: CGSize?
private let dimensionsPromise = ValuePromise<CGSize>(CGSize())
private var validLayout: CGSize?
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) {
self.postbox = postbox
self.fileReference = fileReference
self.approximateDuration = Double(fileReference.media.duration ?? 1)
self.audioSessionManager = audioSessionManager
self.imageNode = TransformImageNode()
self.playerItem = AVPlayerItem(url: URL(string: postbox.mediaBox.completedResourcePath(fileReference.media.resource, pathExtension: "mov") ?? "")!)
let player = AVPlayer(playerItem: self.playerItem)
self.player = player
self.playerNode = ASDisplayNode()
self.playerNode.setLayerBlock({
return AVPlayerLayer(player: player)
})
self.intrinsicDimensions = fileReference.media.dimensions ?? CGSize()
self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions)
super.init()
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, videoReference: fileReference) |> map { [weak self] getSize, getData in
Queue.mainQueue().async {
if let strongSelf = self, strongSelf.dimensions == nil {
if let dimensions = getSize() {
strongSelf.dimensions = dimensions
strongSelf.dimensionsPromise.set(dimensions)
if let size = strongSelf.validLayout {
strongSelf.updateLayout(size: size, transition: .immediate)
}
}
}
}
return getData
})
self.addSubnode(self.imageNode)
self.addSubnode(self.playerNode)
self.player.actionAtItemEnd = .pause
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { [weak self] notification in
self?.performActionAtEnd()
})
self.imageNode.imageUpdated = { [weak self] in
self?._ready.set(.single(Void()))
}
self.player.addObserver(self, forKeyPath: "rate", options: [], context: nil)
playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil)
self._bufferingStatus.set(.single(nil))
}
deinit {
self.player.removeObserver(self, forKeyPath: "rate")
self.playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty")
self.playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
self.playerItem.removeObserver(self, forKeyPath: "playbackBufferFull")
self.audioSessionDisposable.dispose()
self.loadProgressDisposable?.dispose()
self.statusDisposable?.dispose()
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate" {
let isPlaying = !self.player.rate.isZero
let status: MediaPlayerPlaybackStatus
if self.isBuffering {
status = .buffering(initial: false, whilePlaying: isPlaying)
} else {
status = isPlaying ? .playing : .paused
}
self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: status)
self._status.set(self.statusValue)
} else if keyPath == "playbackBufferEmpty" {
let isPlaying = !self.player.rate.isZero
let status: MediaPlayerPlaybackStatus
self.isBuffering = true
if self.isBuffering {
status = .buffering(initial: false, whilePlaying: isPlaying)
} else {
status = isPlaying ? .playing : .paused
}
self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: status)
self._status.set(self.statusValue)
} else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" {
let isPlaying = !self.player.rate.isZero
let status: MediaPlayerPlaybackStatus
self.isBuffering = false
if self.isBuffering {
status = .buffering(initial: false, whilePlaying: isPlaying)
} else {
status = isPlaying ? .playing : .paused
}
self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: status)
self._status.set(self.statusValue)
}
}
private func performActionAtEnd() {
for listener in self.playbackCompletedListeners.copyItems() {
listener()
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width)
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
let makeImageLayout = self.imageNode.asyncLayout()
let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets()))
applyImageLayout()
}
func play() {
assert(Queue.mainQueue().isCurrent())
if !self.initializedStatus {
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true)))
}
if !self.hasAudioSession {
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] _ in
self?.hasAudioSession = true
self?.player.play()
}, deactivate: { [weak self] in
self?.hasAudioSession = false
self?.player.pause()
return .complete()
}))
} else {
self.player.play()
}
}
func pause() {
assert(Queue.mainQueue().isCurrent())
if !self.initializedStatus {
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused))
}
self.player.pause()
}
func togglePlayPause() {
assert(Queue.mainQueue().isCurrent())
if self.player.rate.isZero {
self.play()
} else {
self.pause()
}
}
func setSoundEnabled(_ value: Bool) {
assert(Queue.mainQueue().isCurrent())
}
func seek(_ timestamp: Double) {
assert(Queue.mainQueue().isCurrent())
self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30))
}
func playOnceWithSound(playAndRecord: Bool) {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
}
func continuePlayingWithoutSound() {
}
func setBaseRate(_ baseRate: Double) {
}
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int {
return self.playbackCompletedListeners.add(f)
}
func removePlaybackCompleted(_ index: Int) {
self.playbackCompletedListeners.remove(index)
}
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
}
}

View File

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

View File

@ -1011,4 +1011,16 @@ struct PresentationResourcesChat {
})
})
}
static func chatBubbleFileCloudFetchIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubbleFileCloudFetchIncomingIcon.rawValue, { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: theme.chat.bubble.incomingAccentControlColor)
})
}
static func chatBubbleFileCloudFetchOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubbleFileCloudFetchOutgoingIcon.rawValue, { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: theme.chat.bubble.outgoingAccentControlColor)
})
}
}

View File

@ -0,0 +1,301 @@
import Foundation
import Display
import AsyncDisplayKit
import LegacyComponents
private final class RadialCloudProgressContentCancelNodeParameters: NSObject {
let color: UIColor
init(color: UIColor) {
self.color = color
}
}
private final class RadialCloudProgressContentSpinnerNodeParameters: NSObject {
let color: UIColor
let backgroundStrokeColor: UIColor
let progress: CGFloat
let lineWidth: CGFloat?
init(color: UIColor, backgroundStrokeColor: UIColor, progress: CGFloat, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.progress = progress
self.lineWidth = lineWidth
}
}
private final class RadialCloudProgressContentSpinnerNode: ASDisplayNode {
var progressAnimationCompleted: (() -> Void)?
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
var backgroundStrokeColor: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
var progress: CGFloat? {
didSet {
self.pop_removeAnimation(forKey: "progress")
if let progress = self.progress {
self.pop_removeAnimation(forKey: "indefiniteProgress")
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty
animation.fromValue = CGFloat(self.effectiveProgress) as NSNumber
animation.toValue = CGFloat(progress) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 0.2
animation.completionBlock = { [weak self] _, _ in
self?.progressAnimationCompleted?()
}
self.pop_add(animation, forKey: "progress")
} else if self.pop_animation(forKey: "indefiniteProgress") == nil {
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty
animation.fromValue = CGFloat(0.0) as NSNumber
animation.toValue = CGFloat(2.0) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 2.5
animation.repeatForever = true
self.pop_add(animation, forKey: "indefiniteProgress")
}
}
}
var isAnimatingProgress: Bool {
return self.pop_animation(forKey: "progress") != nil
}
let lineWidth: CGFloat?
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.lineWidth = lineWidth
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCloudProgressContentSpinnerNodeParameters(color: self.color, backgroundStrokeColor: self.backgroundStrokeColor, progress: self.effectiveProgress, lineWidth: self.lineWidth)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCloudProgressContentSpinnerNodeParameters {
let factor = bounds.size.width / 50.0
var progress = parameters.progress
var startAngle = -CGFloat.pi / 2.0
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
let lineWidth: CGFloat = parameters.lineWidth ?? max(1.6, 2.25 * factor)
let pathDiameter: CGFloat
if parameters.lineWidth != nil {
pathDiameter = bounds.size.width - lineWidth
} else {
pathDiameter = bounds.size.width - lineWidth - 2.5 * 2.0
}
context.setStrokeColor(parameters.backgroundStrokeColor.cgColor)
let backgroundPath = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: 0.0, endAngle: 2.0 * CGFloat.pi, clockwise:true)
backgroundPath.lineWidth = lineWidth
backgroundPath.stroke()
context.setStrokeColor(parameters.color.cgColor)
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
}
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
basicAnimation.duration = 2.0
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
basicAnimation.beginTime = 1.0
self.layer.add(basicAnimation, forKey: "progressRotation")
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.layer.removeAnimation(forKey: "progressRotation")
}
}
private final class RadialCloudProgressContentCancelNode: ASDisplayNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
init(color: UIColor) {
self.color = color
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCloudProgressContentCancelNodeParameters(color: self.color)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCloudProgressContentCancelNodeParameters {
let size: CGFloat = 8.0
context.setFillColor(parameters.color.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: floor((bounds.size.width - size) / 2.0), y: floor((bounds.size.height - size) / 2.0)), size: CGSize(width: size, height: size)), cornerRadius: 1.0)
path.fill()
}
}
}
final class RadialCloudProgressContentNode: RadialStatusContentNode {
private let spinnerNode: RadialCloudProgressContentSpinnerNode
private let cancelNode: RadialCloudProgressContentCancelNode
var color: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.color = self.color
}
}
var backgroundStrokeColor: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.backgroundStrokeColor = self.backgroundStrokeColor
}
}
var progress: CGFloat? = 0.0 {
didSet {
self.spinnerNode.progress = self.progress
}
}
private var enqueuedReadyForTransition: (() -> Void)?
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.spinnerNode = RadialCloudProgressContentSpinnerNode(color: color, backgroundStrokeColor: backgroundStrokeColor, lineWidth: lineWidth)
self.cancelNode = RadialCloudProgressContentCancelNode(color: color)
super.init()
self.isLayerBacked = true
self.addSubnode(self.spinnerNode)
self.addSubnode(self.cancelNode)
self.spinnerNode.progressAnimationCompleted = { [weak self] in
if let strongSelf = self {
if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition {
strongSelf.enqueuedReadyForTransition = nil
enqueuedReadyForTransition()
}
}
}
}
override func enqueueReadyForTransition(_ f: @escaping () -> Void) {
if self.spinnerNode.isAnimatingProgress {
self.enqueuedReadyForTransition = f
} else {
f()
}
}
override func layout() {
super.layout()
let bounds = self.bounds
self.spinnerNode.bounds = bounds
self.spinnerNode.position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
self.cancelNode.frame = bounds
}
override func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion()
})
self.cancelNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
}
override func animateIn() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.cancelNode.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15)
}
}

View File

@ -7,6 +7,7 @@ public enum RadialStatusNodeState: Equatable {
case play(UIColor)
case 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)
}

View File

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

View File

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

View File

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

View File

@ -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? {