Merge branch 'master' of github.com:peter-iakovlev/TelegramUI

# Conflicts:
#	TelegramUI/ChatTitleView.swift
#	TelegramUI/OpenUrl.swift
This commit is contained in:
Ilya Laktyushin 2018-10-13 05:30:22 +01:00
commit 454146cb80
38 changed files with 1532 additions and 363 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 {
@ -4305,7 +4339,35 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UIV
disposable = MetaDisposable()
self.resolvePeerByNameDisposable = disposable
}
disposable.set((resolvePeerByName(account: self.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in
var resolveSignal = resolvePeerByName(account: self.account, name: name, ageLimit: 10)
var cancelImpl: (() -> Void)?
let presentationData = self.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
resolveSignal = resolveSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = { [weak self] in
self?.resolvePeerByNameDisposable?.set(nil)
}
disposable.set((resolveSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in
if let strongSelf = self {
strongSelf.openResolved(.peer(peerId, .default))
}

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

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

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])
}
@ -184,13 +184,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
}
}
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout) {
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) {
var isOnline = false
if case .online = networkState {
isOnline = true
}
if isOnline || layout.metrics.widthClass == .regular {
if isOnline || layout?.metrics.widthClass == .regular {
self.contentContainer.isHidden = false
if let networkStatusNode = self.networkStatusNode {
self.networkStatusNode = nil
@ -210,7 +210,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
case .waitingForNetwork:
statusNode.title = self.strings.State_WaitingForNetwork
case let .connecting(proxy):
if proxy != nil && layout.size.width > 320.0 {
if let layout = layout, proxy != nil && layout.size.width > 320.0 {
statusNode.title = self.strings.State_ConnectingToProxy
} else {
statusNode.title = self.strings.State_Connecting
@ -233,7 +233,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
}
}
var layout: ContainerViewLayout = ContainerViewLayout()() {
var layout: ContainerViewLayout? {
didSet {
if self.layout != oldValue {
updateNetworkStatusNode(networkState: self.networkState, layout: self.layout)
@ -250,7 +250,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
var titleLeftIcon: ChatTitleIcon = .none
var titleRightIcon: ChatTitleIcon = .none
switch titleContent {
case let .peer(peerView):
case let .peer(peerView, _):
if let peer = peerViewMainPeer(peerView) {
if peerView.peerId == self.account.peerId {
string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
@ -306,7 +306,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
var shouldUpdateLayout = false
if let titleContent = self.titleContent {
switch titleContent {
case let .peer(peerView):
case let .peer(peerView, onlineMemberCount):
if let peer = peerViewMainPeer(peerView) {
if peer.id == self.account.peerId {
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
@ -383,17 +383,27 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
}
} else if let channel = peer as? TelegramChannel {
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
let membersString: String
if case .group = channel.info {
membersString = strings.Conversation_StatusMembers(memberCount)
if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 {
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
self.infoNode.attributedText = string
shouldUpdateLayout = true
}
} else {
membersString = strings.Conversation_StatusSubscribers(memberCount)
}
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
self.infoNode.attributedText = string
shouldUpdateLayout = true
let membersString: String
if case .group = channel.info {
membersString = strings.Conversation_StatusMembers(memberCount)
} else {
membersString = strings.Conversation_StatusSubscribers(memberCount)
}
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
self.infoNode.attributedText = string
shouldUpdateLayout = true
}
}
} else {
switch channel.info {

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

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