mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-31 15:37:01 +00:00
Merge commit '34ec6b26174789e5f12bc6a32b2e0faaf140db75'
This commit is contained in:
commit
ffc9deba9f
@ -6463,5 +6463,6 @@ Sorry for the inconvenience.";
|
||||
"Calls.StartNewCall" = "Start New Call";
|
||||
|
||||
"VoiceChat.VideoPreviewTitle" = "Video Preview";
|
||||
"VoiceChat.VideoPreviewDescription" = "Place your face in the area above for better experience.";
|
||||
"VoiceChat.VideoPreviewShare" = "Share Video";
|
||||
"VoiceChat.VideoPreviewDescription" = "Are you sure you want to share your video?";
|
||||
"VoiceChat.VideoPreviewShareCamera" = "Share Camera Video";
|
||||
"VoiceChat.VideoPreviewShareScreen" = "Share Screen";
|
||||
|
@ -219,17 +219,14 @@ public final class ChatPeerNearbyData: Equatable {
|
||||
|
||||
public final class ChatGreetingData: Equatable {
|
||||
public static func == (lhs: ChatGreetingData, rhs: ChatGreetingData) -> Bool {
|
||||
if let lhsSticker = lhs.sticker, let rhsSticker = rhs.sticker, !lhsSticker.isEqual(to: rhsSticker) {
|
||||
return false
|
||||
} else if (lhs.sticker == nil) != (rhs.sticker == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return lhs.uuid == rhs.uuid
|
||||
}
|
||||
|
||||
public let sticker: TelegramMediaFile?
|
||||
public let uuid: UUID
|
||||
public let sticker: Signal<TelegramMediaFile?, NoError>
|
||||
|
||||
public init(sticker: TelegramMediaFile?) {
|
||||
public init(uuid: UUID, sticker: Signal<TelegramMediaFile?, NoError>) {
|
||||
self.uuid = uuid
|
||||
self.sticker = sticker
|
||||
}
|
||||
}
|
||||
@ -285,14 +282,13 @@ public final class NavigateToChatControllerParams {
|
||||
public let activateMessageSearch: (ChatSearchDomain, String)?
|
||||
public let peekData: ChatPeekTimeout?
|
||||
public let peerNearbyData: ChatPeerNearbyData?
|
||||
public let greetingData: ChatGreetingData?
|
||||
public let reportReason: ReportReason?
|
||||
public let animated: Bool
|
||||
public let options: NavigationAnimationOptions
|
||||
public let parentGroupId: PeerGroupId?
|
||||
public let completion: (ChatController) -> Void
|
||||
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, greetingData: ChatGreetingData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
|
||||
self.navigationController = navigationController
|
||||
self.chatController = chatController
|
||||
self.chatLocationContextHolder = chatLocationContextHolder
|
||||
@ -309,7 +305,6 @@ public final class NavigateToChatControllerParams {
|
||||
self.activateMessageSearch = activateMessageSearch
|
||||
self.peekData = peekData
|
||||
self.peerNearbyData = peerNearbyData
|
||||
self.greetingData = greetingData
|
||||
self.reportReason = reportReason
|
||||
self.animated = animated
|
||||
self.options = options
|
||||
@ -719,6 +714,7 @@ public protocol AccountContext: class {
|
||||
var liveLocationManager: LiveLocationManager? { get }
|
||||
var peersNearbyManager: PeersNearbyManager? { get }
|
||||
var fetchManager: FetchManager { get }
|
||||
var prefetchManager: PrefetchManager? { get }
|
||||
var downloadedMediaStoreManager: DownloadedMediaStoreManager { get }
|
||||
var peerChannelMemberCategoriesContextsManager: PeerChannelMemberCategoriesContextsManager { get }
|
||||
var wallpaperUploadManager: WallpaperUploadManager? { get }
|
||||
|
@ -158,3 +158,8 @@ public protocol FetchManager {
|
||||
func cancelInteractiveFetches(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource)
|
||||
func fetchStatus(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource) -> Signal<MediaResourceStatus, NoError>
|
||||
}
|
||||
|
||||
public protocol PrefetchManager {
|
||||
var preloadedGreetingSticker: ChatGreetingData { get }
|
||||
func prepareNextGreetingSticker()
|
||||
}
|
||||
|
@ -372,6 +372,7 @@ public protocol PresentationGroupCall: class {
|
||||
var incomingVideoSources: Signal<Set<String>, NoError> { get }
|
||||
|
||||
func makeIncomingVideoView(endpointId: String, completion: @escaping (PresentationCallVideoView?) -> Void)
|
||||
func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
|
||||
|
||||
func loadMoreMembers(token: String)
|
||||
}
|
||||
|
@ -144,9 +144,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private let featuredFiltersDisposable = MetaDisposable()
|
||||
private var processedFeaturedFilters = false
|
||||
|
||||
private let preloadedSticker = Promise<TelegramMediaFile?>(nil)
|
||||
private let preloadStickerDisposable = MetaDisposable()
|
||||
|
||||
private let isReorderingTabsValue = ValuePromise<Bool>(false)
|
||||
|
||||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
@ -613,53 +610,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
scrollToEndIfExists = true
|
||||
}
|
||||
|
||||
let _ = (strongSelf.preloadedSticker.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] greetingSticker in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, greetingData: greetingSticker.flatMap({ ChatGreetingData(sticker: $0) }), animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in
|
||||
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
|
||||
if let promoInfo = promoInfo {
|
||||
switch promoInfo {
|
||||
case .proxy:
|
||||
let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager)
|
||||
|> deliverOnMainQueue).start(next: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
controller.displayPromoAnnouncement(text: strongSelf.presentationData.strings.DialogList_AdNoticeAlert)
|
||||
let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
||||
}
|
||||
})
|
||||
case let .psa(type, _):
|
||||
let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id)
|
||||
|> deliverOnMainQueue).start(next: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
var text = strongSelf.presentationData.strings.ChatList_GenericPsaAlert
|
||||
let key = "ChatList.PsaAlert.\(type)"
|
||||
if let string = strongSelf.presentationData.strings.primaryComponent.dict[key] {
|
||||
text = string
|
||||
} else if let string = strongSelf.presentationData.strings.secondaryComponent?.dict[key] {
|
||||
text = string
|
||||
}
|
||||
|
||||
controller.displayPromoAnnouncement(text: text)
|
||||
let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start()
|
||||
}
|
||||
})
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in
|
||||
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
|
||||
if let promoInfo = promoInfo {
|
||||
switch promoInfo {
|
||||
case .proxy:
|
||||
let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager)
|
||||
|> deliverOnMainQueue).start(next: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if activateInput {
|
||||
strongSelf.prepareRandomGreetingSticker()
|
||||
if !value {
|
||||
controller.displayPromoAnnouncement(text: strongSelf.presentationData.strings.DialogList_AdNoticeAlert)
|
||||
let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
||||
}
|
||||
})
|
||||
case let .psa(type, _):
|
||||
let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id)
|
||||
|> deliverOnMainQueue).start(next: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
var text = strongSelf.presentationData.strings.ChatList_GenericPsaAlert
|
||||
let key = "ChatList.PsaAlert.\(type)"
|
||||
if let string = strongSelf.presentationData.strings.primaryComponent.dict[key] {
|
||||
text = string
|
||||
} else if let string = strongSelf.presentationData.strings.secondaryComponent?.dict[key] {
|
||||
text = string
|
||||
}
|
||||
|
||||
controller.displayPromoAnnouncement(text: text)
|
||||
let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1126,7 +1113,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
super.displayNodeDidLoad()
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
self.prepareRandomGreetingSticker()
|
||||
self.context.prefetchManager?.prepareNextGreetingSticker()
|
||||
}
|
||||
}
|
||||
|
||||
@ -2763,28 +2750,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareRandomGreetingSticker() {
|
||||
let context = self.context
|
||||
self.preloadedSticker.set(.single(nil)
|
||||
|> then(context.engine.stickers.randomGreetingSticker()
|
||||
|> map { item in
|
||||
return item?.file
|
||||
}))
|
||||
|
||||
self.preloadStickerDisposable.set((self.preloadedSticker.get()
|
||||
|> mapToSignal { sticker -> Signal<Void, NoError> in
|
||||
if let sticker = sticker {
|
||||
let _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: sticker)).start()
|
||||
return chatMessageAnimationData(postbox: context.account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false)
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}).start())
|
||||
}
|
||||
|
||||
override public func tabBarDisabledAction() {
|
||||
self.donePressed()
|
||||
}
|
||||
|
@ -89,9 +89,6 @@ public class ContactsController: ViewController {
|
||||
|
||||
public var switchToChatsController: (() -> Void)?
|
||||
|
||||
private let preloadedSticker = Promise<TelegramMediaFile?>(nil)
|
||||
private let preloadStickerDisposable = MetaDisposable()
|
||||
|
||||
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if self.isNodeLoaded {
|
||||
self.contactsNode.contactListNode.updateSelectedChatLocation(data as? ChatLocation, progress: progress, transition: transition)
|
||||
@ -235,24 +232,16 @@ public class ContactsController: ViewController {
|
||||
scrollToEndIfExists = true
|
||||
}
|
||||
|
||||
let _ = (strongSelf.preloadedSticker.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] greetingSticker in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), purposefulAction: { [weak self] in
|
||||
if fromSearch {
|
||||
self?.deactivateSearch(animated: false)
|
||||
self?.switchToChatsController?()
|
||||
}
|
||||
}, scrollToEndIfExists: scrollToEndIfExists, greetingData: greetingSticker.flatMap({ ChatGreetingData(sticker: $0) }), options: [.removeOnMasterDetails], completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
|
||||
}
|
||||
}))
|
||||
|
||||
strongSelf.prepareRandomGreetingSticker()
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), purposefulAction: { [weak self] in
|
||||
if fromSearch {
|
||||
self?.deactivateSearch(animated: false)
|
||||
self?.switchToChatsController?()
|
||||
}
|
||||
})
|
||||
}, scrollToEndIfExists: scrollToEndIfExists, options: [.removeOnMasterDetails], completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
|
||||
}
|
||||
}))
|
||||
}
|
||||
case let .deviceContact(id, _):
|
||||
let _ = ((strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil))
|
||||
@ -423,14 +412,6 @@ public class ContactsController: ViewController {
|
||||
self.contactsNode.contactListNode.enableUpdates = false
|
||||
}
|
||||
|
||||
public override func displayNodeDidLoad() {
|
||||
super.displayNodeDidLoad()
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
self.prepareRandomGreetingSticker()
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
@ -525,26 +506,4 @@ public class ContactsController: ViewController {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func prepareRandomGreetingSticker() {
|
||||
let context = self.context
|
||||
self.preloadedSticker.set(.single(nil)
|
||||
|> then(context.engine.stickers.randomGreetingSticker()
|
||||
|> map { item in
|
||||
return item?.file
|
||||
}))
|
||||
|
||||
self.preloadStickerDisposable.set((self.preloadedSticker.get()
|
||||
|> mapToSignal { sticker -> Signal<Void, NoError> in
|
||||
if let sticker = sticker {
|
||||
let _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: sticker)).start()
|
||||
return chatMessageAnimationData(postbox: context.account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false)
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}).start())
|
||||
}
|
||||
}
|
||||
|
@ -165,8 +165,11 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
||||
gesture.isEnabled = self.panSelectionGestureEnabled
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var minActionsWidth: CGFloat = 250.0
|
||||
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
|
||||
minActionsWidth = minimalWidth
|
||||
}
|
||||
|
||||
switch widthClass {
|
||||
case .compact:
|
||||
@ -517,10 +520,10 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
var contentSize = CGSize()
|
||||
let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, transition: transition)
|
||||
let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, minimalWidth: nil, transition: transition)
|
||||
|
||||
if let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode {
|
||||
let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, transition: transition)
|
||||
let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, minimalWidth: actionsSize.width, transition: transition)
|
||||
contentSize = additionalActionsSize
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: additionalActionsSize)
|
||||
|
@ -5,6 +5,7 @@ import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import AppBundle
|
||||
import SemanticStatusNode
|
||||
import AnimationUI
|
||||
|
||||
private let labelFont = Font.regular(13.0)
|
||||
|
||||
@ -30,6 +31,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
}
|
||||
|
||||
enum Image {
|
||||
case cameraOff
|
||||
case cameraOn
|
||||
case camera
|
||||
case mute
|
||||
case flipCamera
|
||||
@ -63,6 +66,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
private let effectView: UIVisualEffectView
|
||||
private let contentBackgroundNode: ASImageNode
|
||||
private let contentNode: ASImageNode
|
||||
private var animationNode: AnimationNode?
|
||||
private let overlayHighlightNode: ASImageNode
|
||||
private var statusNode: SemanticStatusNode?
|
||||
let textNode: ImmediateTextNode
|
||||
@ -179,6 +183,33 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
|
||||
let contentBackgroundImage: UIImage? = nil
|
||||
|
||||
var animationName: String?
|
||||
switch content.image {
|
||||
case .cameraOff:
|
||||
animationName = "anim_cameraoff"
|
||||
case .cameraOn:
|
||||
animationName = "anim_cameraon"
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let animationName = animationName {
|
||||
let animationFrame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
if self.animationNode == nil {
|
||||
let animationNode = AnimationNode(animation: animationName, colors: nil, scale: 1.0)
|
||||
self.animationNode = animationNode
|
||||
self.contentContainer.insertSubnode(animationNode, aboveSubnode: self.contentNode)
|
||||
}
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = animationFrame
|
||||
if previousContent == nil {
|
||||
animationNode.seekToEnd()
|
||||
} else if previousContent?.image != content.image {
|
||||
animationNode.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let contentImage = generateImage(CGSize(width: self.largeButtonSize, height: self.largeButtonSize), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
@ -219,6 +250,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
var image: UIImage?
|
||||
|
||||
switch content.image {
|
||||
case .cameraOff, .cameraOn:
|
||||
image = nil
|
||||
case .camera:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallCameraButton"), color: imageColor)
|
||||
case .mute:
|
||||
|
@ -2384,6 +2384,76 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.participantsContext?.lowerHand()
|
||||
}
|
||||
|
||||
public func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) {
|
||||
if self.videoCapturer == nil {
|
||||
let videoCapturer = OngoingCallVideoCapturer()
|
||||
self.videoCapturer = videoCapturer
|
||||
}
|
||||
|
||||
self.videoCapturer?.makeOutgoingVideoView(completion: { view in
|
||||
if let view = view {
|
||||
let setOnFirstFrameReceived = view.setOnFirstFrameReceived
|
||||
let setOnOrientationUpdated = view.setOnOrientationUpdated
|
||||
let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated
|
||||
completion(PresentationCallVideoView(
|
||||
holder: view,
|
||||
view: view.view,
|
||||
setOnFirstFrameReceived: { f in
|
||||
setOnFirstFrameReceived(f)
|
||||
},
|
||||
getOrientation: { [weak view] in
|
||||
if let view = view {
|
||||
let mappedValue: PresentationCallVideoView.Orientation
|
||||
switch view.getOrientation() {
|
||||
case .rotation0:
|
||||
mappedValue = .rotation0
|
||||
case .rotation90:
|
||||
mappedValue = .rotation90
|
||||
case .rotation180:
|
||||
mappedValue = .rotation180
|
||||
case .rotation270:
|
||||
mappedValue = .rotation270
|
||||
}
|
||||
return mappedValue
|
||||
} else {
|
||||
return .rotation0
|
||||
}
|
||||
},
|
||||
getAspect: { [weak view] in
|
||||
if let view = view {
|
||||
return view.getAspect()
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
},
|
||||
setOnOrientationUpdated: { f in
|
||||
setOnOrientationUpdated { value, aspect in
|
||||
let mappedValue: PresentationCallVideoView.Orientation
|
||||
switch value {
|
||||
case .rotation0:
|
||||
mappedValue = .rotation0
|
||||
case .rotation90:
|
||||
mappedValue = .rotation90
|
||||
case .rotation180:
|
||||
mappedValue = .rotation180
|
||||
case .rotation270:
|
||||
mappedValue = .rotation270
|
||||
}
|
||||
f?(mappedValue, aspect)
|
||||
}
|
||||
},
|
||||
setOnIsMirroredUpdated: { f in
|
||||
setOnIsMirroredUpdated { value in
|
||||
f?(value)
|
||||
}
|
||||
}
|
||||
))
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func requestVideo() {
|
||||
if self.videoCapturer == nil {
|
||||
let videoCapturer = OngoingCallVideoCapturer()
|
||||
|
@ -1,283 +1,399 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import UrlEscaping
|
||||
import TelegramPresentationData
|
||||
import SolidRoundedButtonNode
|
||||
import PresentationDataUtils
|
||||
import UIKitRuntimeUtils
|
||||
|
||||
private final class VoiceChatCameraPreviewAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let title: String
|
||||
private let text: String
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
final class VoiceChatCameraPreviewController: ViewController {
|
||||
private var controllerNode: VoiceChatCameraPreviewControllerNode {
|
||||
return self.displayNode as! VoiceChatCameraPreviewControllerNode
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
private let context: AccountContext
|
||||
|
||||
private var animatedIn = false
|
||||
|
||||
private let cameraNode: GroupVideoNode
|
||||
private let shareCamera: (ASDisplayNode) -> Void
|
||||
private let switchCamera: () -> Void
|
||||
private let shareScreen: () -> Void
|
||||
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, cameraNode: GroupVideoNode, shareCamera: @escaping (ASDisplayNode) -> Void, switchCamera: @escaping () -> Void, shareScreen: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.cameraNode = cameraNode
|
||||
self.shareCamera = shareCamera
|
||||
self.switchCamera = switchCamera
|
||||
self.shareScreen = shareScreen
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.clipsToBounds = true
|
||||
self.backgroundNode.backgroundColor = UIColor(rgb: 0x161619)
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
self.maskNode.displaysAsynchronously = false
|
||||
self.maskNode.contentMode = .scaleToFill
|
||||
self.maskNode.image = generateImage(CGSize(width: 300.0, height: 400.0), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
|
||||
context.fill(bounds)
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
context.fillEllipse(in: CGRect(x: 27.0, y: 37.0, width: 246.0, height: 300.0))
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerNode.updatePresentationData(presentationData)
|
||||
}
|
||||
})
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 8
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.backgroundNode.addSubnode(self.maskNode)
|
||||
|
||||
self.updateTheme(theme)
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = VoiceChatCameraPreviewControllerNode(context: self.context, cameraNode: self.cameraNode)
|
||||
self.controllerNode.shareCamera = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.shareCamera(strongSelf.cameraNode)
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
}
|
||||
self.controllerNode.switchCamera = { [weak self] in
|
||||
self?.switchCamera()
|
||||
}
|
||||
self.controllerNode.shareScreen = { [weak self] in
|
||||
self?.shareScreen()
|
||||
self?.dismiss()
|
||||
}
|
||||
self.controllerNode.dismiss = { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
self.controllerNode.cancel = { [weak self] in
|
||||
self?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override public func loadView() {
|
||||
super.loadView()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.animatedIn {
|
||||
self.animatedIn = true
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.controllerNode.animateOut(completion: completion)
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 300.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
private let cameraNode: GroupVideoNode
|
||||
private let dimNode: ASDisplayNode
|
||||
private let wrappingScrollNode: ASScrollNode
|
||||
private let contentContainerNode: ASDisplayNode
|
||||
private let effectNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let contentBackgroundNode: ASDisplayNode
|
||||
private let titleNode: ASTextNode
|
||||
private let previewContainerNode: ASDisplayNode
|
||||
private let cameraButton: SolidRoundedButtonNode
|
||||
private let screenButton: SolidRoundedButtonNode
|
||||
private let cancelButton: SolidRoundedButtonNode
|
||||
|
||||
private let switchCameraButton: HighlightTrackingButtonNode
|
||||
private let switchCameraEffectView: UIVisualEffectView
|
||||
private let switchCameraIconNode: ASImageNode
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
var shareCamera: (() -> Void)?
|
||||
var switchCamera: (() -> Void)?
|
||||
var shareScreen: (() -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
var cancel: (() -> Void)?
|
||||
|
||||
init(context: AccountContext, cameraNode: GroupVideoNode) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
self.validLayout = size
|
||||
self.cameraNode = cameraNode
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 18.0)
|
||||
let spacing: CGFloat = 3.0
|
||||
self.wrappingScrollNode = ASScrollNode()
|
||||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
|
||||
let videoHeight: CGFloat = 400.0
|
||||
origin.y += videoHeight
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
self.contentContainerNode.isOpaque = false
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.clipsToBounds = true
|
||||
self.backgroundNode.cornerRadius = 16.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0
|
||||
let backgroundColor = UIColor(rgb: 0x1c1c1e)
|
||||
let textColor: UIColor = .white
|
||||
let buttonColor: UIColor = UIColor(rgb: 0x2b2b2f)
|
||||
let buttonTextColor: UIColor = .white
|
||||
let blurStyle: UIBlurEffect.Style = .dark
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
self.effectNode = ASDisplayNode(viewBlock: {
|
||||
return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
|
||||
})
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
self.contentBackgroundNode = ASDisplayNode()
|
||||
self.contentBackgroundNode.backgroundColor = backgroundColor
|
||||
|
||||
let title = self.presentationData.strings.VoiceChat_VideoPreviewTitle
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor)
|
||||
|
||||
self.cameraButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), font: .bold, height: 52.0, cornerRadius: 11.0, gloss: false)
|
||||
self.cameraButton.title = self.presentationData.strings.VoiceChat_VideoPreviewShareCamera
|
||||
|
||||
self.screenButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .bold, height: 52.0, cornerRadius: 11.0, gloss: false)
|
||||
self.screenButton.title = self.presentationData.strings.VoiceChat_VideoPreviewShareScreen
|
||||
|
||||
self.cancelButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .regular, height: 52.0, cornerRadius: 11.0, gloss: false)
|
||||
self.cancelButton.title = self.presentationData.strings.Common_Cancel
|
||||
|
||||
self.previewContainerNode = ASDisplayNode()
|
||||
self.previewContainerNode.cornerRadius = 11.0
|
||||
self.previewContainerNode.backgroundColor = .black
|
||||
|
||||
self.switchCameraButton = HighlightTrackingButtonNode()
|
||||
self.switchCameraEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
||||
self.switchCameraEffectView.clipsToBounds = true
|
||||
self.switchCameraEffectView.layer.cornerRadius = 24.0
|
||||
self.switchCameraEffectView.isUserInteractionEnabled = false
|
||||
|
||||
self.switchCameraIconNode = ASImageNode()
|
||||
self.switchCameraIconNode.displaysAsynchronously = false
|
||||
self.switchCameraIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: .white)
|
||||
self.switchCameraIconNode.contentMode = .center
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = nil
|
||||
self.isOpaque = false
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
self.addSubnode(self.dimNode)
|
||||
|
||||
self.wrappingScrollNode.view.delegate = self
|
||||
self.addSubnode(self.wrappingScrollNode)
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.backgroundNode)
|
||||
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
|
||||
|
||||
self.backgroundNode.addSubnode(self.effectNode)
|
||||
self.backgroundNode.addSubnode(self.contentBackgroundNode)
|
||||
self.contentContainerNode.addSubnode(self.titleNode)
|
||||
self.contentContainerNode.addSubnode(self.cameraButton)
|
||||
self.contentContainerNode.addSubnode(self.screenButton)
|
||||
self.contentContainerNode.addSubnode(self.cancelButton)
|
||||
|
||||
self.contentContainerNode.addSubnode(self.previewContainerNode)
|
||||
|
||||
self.previewContainerNode.addSubnode(self.cameraNode)
|
||||
self.previewContainerNode.addSubnode(self.switchCameraButton)
|
||||
self.switchCameraButton.view.addSubview(self.switchCameraEffectView)
|
||||
self.switchCameraButton.addSubnode(self.switchCameraIconNode)
|
||||
|
||||
self.cameraButton.pressed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.shareCamera?()
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
self.screenButton.pressed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.shareScreen?()
|
||||
}
|
||||
}
|
||||
self.cancelButton.pressed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.cancel?()
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 300.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + videoHeight + spacing + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: resultWidth, height: videoHeight)))
|
||||
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: resultWidth, height: videoHeight)))
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
self.switchCameraButton.addTarget(self, action: #selector(self.switchCameraPressed), forControlEvents: .touchUpInside)
|
||||
self.switchCameraButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
|
||||
transition.updateSublayerTransformScale(node: strongSelf.switchCameraButton, scale: 0.9)
|
||||
} else {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
|
||||
transition.updateSublayerTransformScale(node: strongSelf.switchCameraButton, scale: 1.0)
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.maskNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.maskNode.layer.animateSpring(from: 1.4 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 104.0)
|
||||
}
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func switchCameraPressed() {
|
||||
self.switchCamera?()
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.cancel?()
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
|
||||
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
|
||||
|
||||
let dimPosition = self.dimNode.layer.position
|
||||
self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
let targetBounds = self.bounds
|
||||
self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset)
|
||||
transition.animateView({
|
||||
self.bounds = targetBounds
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)? = nil) {
|
||||
var dimCompleted = false
|
||||
var offsetCompleted = false
|
||||
|
||||
let internalCompletion: () -> Void = { [weak self] in
|
||||
if let strongSelf = self, dimCompleted && offsetCompleted {
|
||||
strongSelf.dismiss?()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||
dimCompleted = true
|
||||
internalCompletion()
|
||||
})
|
||||
|
||||
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
|
||||
let dimPosition = self.dimNode.layer.position
|
||||
self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
offsetCompleted = true
|
||||
internalCompletion()
|
||||
})
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.bounds.contains(point) {
|
||||
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) {
|
||||
return self.dimNode.view
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
let contentOffset = scrollView.contentOffset
|
||||
let additionalTopHeight = max(0.0, -contentOffset.y)
|
||||
|
||||
if additionalTopHeight >= 30.0 {
|
||||
self.cancel?()
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var insets = layout.insets(options: [.statusBar, .input])
|
||||
let cleanInsets = layout.insets(options: [.statusBar])
|
||||
insets.top = max(10.0, insets.top)
|
||||
|
||||
let buttonOffset: CGFloat = 120.0
|
||||
|
||||
func voiceChatCameraPreviewController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, apply: @escaping () -> Void) -> AlertController {
|
||||
var presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
if let forceTheme = forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_VideoPreviewShare, action: {
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = VoiceChatCameraPreviewAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text)
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
dismissImpl?(true)
|
||||
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
|
||||
let titleHeight: CGFloat = 54.0
|
||||
var contentHeight = titleHeight + bottomInset + 52.0 + 17.0
|
||||
let innerContentHeight: CGFloat = layout.size.height - contentHeight - 160.0
|
||||
contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + innerContentHeight + buttonOffset
|
||||
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in
|
||||
var presentationData = presentationData
|
||||
if let forceTheme = forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
let previewInset: CGFloat = 16.0
|
||||
let sideInset = floor((layout.size.width - width) / 2.0)
|
||||
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
|
||||
let contentFrame = contentContainerFrame
|
||||
|
||||
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0))
|
||||
if backgroundFrame.minY < contentFrame.minY {
|
||||
backgroundFrame.origin.y = contentFrame.minY
|
||||
}
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
})
|
||||
controller.dismissed = {
|
||||
presentationDataDisposable.dispose()
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
let previewSize = CGSize(width: contentFrame.width - previewInset * 2.0, height: contentHeight - 243.0 - bottomInset)
|
||||
transition.updateFrame(node: self.previewContainerNode, frame: CGRect(origin: CGPoint(x: previewInset, y: 56.0), size: previewSize))
|
||||
|
||||
self.cameraNode.frame = CGRect(origin: CGPoint(), size: previewSize)
|
||||
self.cameraNode.updateLayout(size: previewSize, isLandscape: false, transition: .immediate)
|
||||
|
||||
let switchCameraFrame = CGRect(x: previewSize.width - 48.0 - 16.0, y: previewSize.height - 48.0 - 16.0, width: 48.0, height: 48.0)
|
||||
transition.updateFrame(node: self.switchCameraButton, frame: switchCameraFrame)
|
||||
transition.updateFrame(view: self.switchCameraEffectView, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size))
|
||||
transition.updateFrame(node: self.switchCameraIconNode, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size))
|
||||
|
||||
let buttonInset: CGFloat = 16.0
|
||||
let cameraButtonHeight = self.cameraButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.cameraButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: cameraButtonHeight))
|
||||
|
||||
let screenButtonHeight = self.screenButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.screenButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: screenButtonHeight))
|
||||
|
||||
let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.cancelButton, frame: CGRect(x: buttonInset, y: contentHeight - cancelButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: cancelButtonHeight))
|
||||
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
||||
}
|
||||
dismissImpl = { [weak controller, weak contentNode] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
@ -177,8 +177,6 @@ final class GroupVideoNode: ASDisplayNode {
|
||||
rotatedVideoFrame.size.height = ceil(rotatedVideoFrame.size.height)
|
||||
|
||||
var videoSize = rotatedVideoFrame.size
|
||||
// CGSize(width: 1203, height: 677)
|
||||
|
||||
transition.updatePosition(layer: self.videoView.view.layer, position: rotatedVideoFrame.center)
|
||||
transition.updateBounds(layer: self.videoView.view.layer, bounds: CGRect(origin: CGPoint(), size: videoSize))
|
||||
|
||||
@ -194,6 +192,8 @@ private final class MainVideoContainerNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let call: PresentationGroupCall
|
||||
|
||||
private var backdropVideoNode: GroupVideoNode?
|
||||
|
||||
private var currentVideoNode: GroupVideoNode?
|
||||
private var candidateVideoNode: GroupVideoNode?
|
||||
private let topCornersNode: ASImageNode
|
||||
@ -291,7 +291,6 @@ private final class MainVideoContainerNode: ASDisplayNode {
|
||||
strongSelf.candidateVideoNode = nil
|
||||
|
||||
let videoNode = GroupVideoNode(videoView: videoView)
|
||||
|
||||
if let currentVideoNode = strongSelf.currentVideoNode {
|
||||
currentVideoNode.removeFromSupernode()
|
||||
strongSelf.currentVideoNode = nil
|
||||
@ -429,7 +428,8 @@ public final class VoiceChatController: ViewController {
|
||||
var isMyPeer: Bool
|
||||
var ssrc: UInt32?
|
||||
var effectiveVideoEndpointId: String?
|
||||
var presence: TelegramUserPresence?
|
||||
var hasVideo: Bool
|
||||
var hasScreencast: Bool
|
||||
var activityTimestamp: Int32
|
||||
var state: State
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
@ -447,7 +447,8 @@ public final class VoiceChatController: ViewController {
|
||||
isMyPeer: Bool,
|
||||
ssrc: UInt32?,
|
||||
effectiveVideoEndpointId: String?,
|
||||
presence: TelegramUserPresence?,
|
||||
hasVideo: Bool,
|
||||
hasScreencast: Bool,
|
||||
activityTimestamp: Int32,
|
||||
state: State,
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState?,
|
||||
@ -464,7 +465,8 @@ public final class VoiceChatController: ViewController {
|
||||
self.isMyPeer = isMyPeer
|
||||
self.ssrc = ssrc
|
||||
self.effectiveVideoEndpointId = effectiveVideoEndpointId
|
||||
self.presence = presence
|
||||
self.hasVideo = hasVideo
|
||||
self.hasScreencast = hasScreencast
|
||||
self.activityTimestamp = activityTimestamp
|
||||
self.state = state
|
||||
self.muteState = muteState
|
||||
@ -497,7 +499,10 @@ public final class VoiceChatController: ViewController {
|
||||
if lhs.effectiveVideoEndpointId != rhs.effectiveVideoEndpointId {
|
||||
return false
|
||||
}
|
||||
if lhs.presence != rhs.presence {
|
||||
if lhs.hasVideo != rhs.hasVideo {
|
||||
return false
|
||||
}
|
||||
if lhs.hasScreencast != rhs.hasScreencast {
|
||||
return false
|
||||
}
|
||||
if lhs.activityTimestamp != rhs.activityTimestamp {
|
||||
@ -622,7 +627,7 @@ public final class VoiceChatController: ViewController {
|
||||
})
|
||||
case let .peer(peerEntry):
|
||||
let peer = peerEntry.peer
|
||||
|
||||
|
||||
var text: VoiceChatParticipantItem.ParticipantText
|
||||
var expandedText: VoiceChatParticipantItem.ParticipantText?
|
||||
let icon: VoiceChatParticipantItem.Icon
|
||||
@ -632,11 +637,12 @@ public final class VoiceChatController: ViewController {
|
||||
state = .listening
|
||||
}
|
||||
|
||||
let textIcon: VoiceChatParticipantItem.ParticipantText.Icon?
|
||||
if peerEntry.volume != nil {
|
||||
textIcon = .volume
|
||||
} else {
|
||||
textIcon = nil
|
||||
var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon()
|
||||
if peerEntry.hasVideo {
|
||||
textIcon.insert(.video)
|
||||
}
|
||||
if peerEntry.hasScreencast {
|
||||
textIcon.insert(.screen)
|
||||
}
|
||||
let yourText: String
|
||||
if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil {
|
||||
@ -671,6 +677,9 @@ public final class VoiceChatController: ViewController {
|
||||
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
|
||||
icon = .microphone(true, UIColor(rgb: 0xff3b30))
|
||||
} else {
|
||||
if peerEntry.volume != nil {
|
||||
textIcon.insert(.volume)
|
||||
}
|
||||
let volumeValue = peerEntry.volume.flatMap { $0 / 100 }
|
||||
if let volume = volumeValue, volume != 100 {
|
||||
text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").0, textIcon, .constructive)
|
||||
@ -699,7 +708,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
let revealOptions: [VoiceChatParticipantItem.RevealOption] = []
|
||||
|
||||
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, ssrc: peerEntry.ssrc, presence: peerEntry.presence, text: text, expandedText: expandedText, icon: icon, style: peerEntry.style, enabled: true, transparent: transparent, pinned: peerEntry.pinned, selectable: true, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, getVideo: {
|
||||
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, ssrc: peerEntry.ssrc, presence: nil, text: text, expandedText: expandedText, icon: icon, style: peerEntry.style, enabled: true, transparent: transparent, pinned: peerEntry.pinned, selectable: true, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, getVideo: {
|
||||
if let endpointId = peerEntry.effectiveVideoEndpointId {
|
||||
return interaction.getPeerVideo(endpointId, peerEntry.style != .list)
|
||||
} else {
|
||||
@ -789,7 +798,6 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
private var enqueuedTransitions: [ListTransition] = []
|
||||
private var enqueuedTileTransitions: [ListTransition] = []
|
||||
private var floatingHeaderOffset: CGFloat?
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
private var didSetContentsReady: Bool = false
|
||||
@ -871,8 +879,8 @@ public final class VoiceChatController: ViewController {
|
||||
private var requestedVideoSources = Set<String>()
|
||||
private var videoNodes: [(String, GroupVideoNode)] = []
|
||||
|
||||
private var currentDominantSpeakerWithVideo: (PeerId, String)?
|
||||
private var currentForcedSpeakerWithVideo: (PeerId, String)?
|
||||
private var currentDominantSpeakerWithVideo: PeerId?
|
||||
private var currentForcedSpeakerWithVideo: PeerId?
|
||||
private var effectiveSpeakerWithVideo: (PeerId, String)?
|
||||
|
||||
private var updateAvatarDisposable = MetaDisposable()
|
||||
@ -978,11 +986,13 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
self.bottomPanelBackgroundNode = ASDisplayNode()
|
||||
self.bottomPanelBackgroundNode.backgroundColor = panelBackgroundColor
|
||||
self.bottomPanelBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.bottomCornersNode = ASImageNode()
|
||||
self.bottomCornersNode.displaysAsynchronously = false
|
||||
self.bottomCornersNode.displayWithoutProcessing = true
|
||||
self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: false)
|
||||
self.bottomCornersNode.isUserInteractionEnabled = false
|
||||
|
||||
self.audioButton = CallControllerButtonItemNode()
|
||||
self.cameraButton = CallControllerButtonItemNode()
|
||||
@ -1072,12 +1082,12 @@ public final class VoiceChatController: ViewController {
|
||||
let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
|
||||
}, pinPeer: { [weak self] peerId, endpointId in
|
||||
if let strongSelf = self {
|
||||
if peerId != strongSelf.currentForcedSpeakerWithVideo?.0, let endpointId = endpointId {
|
||||
strongSelf.currentForcedSpeakerWithVideo = (peerId, endpointId)
|
||||
if peerId != strongSelf.currentForcedSpeakerWithVideo {
|
||||
strongSelf.currentForcedSpeakerWithVideo = peerId
|
||||
} else {
|
||||
strongSelf.currentForcedSpeakerWithVideo = nil
|
||||
}
|
||||
strongSelf.updatePinnedParticipant()
|
||||
strongSelf.updatePinnedParticipant(waitForFullSize: false)
|
||||
|
||||
var updateLayout = false
|
||||
if strongSelf.effectiveSpeakerWithVideo != nil && !strongSelf.isExpanded {
|
||||
@ -1094,7 +1104,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
strongSelf.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
strongSelf.animatingExpansion = false
|
||||
})
|
||||
}
|
||||
@ -1411,7 +1421,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
for (endpointId, _) in strongSelf.videoNodes {
|
||||
if entry.effectiveVideoEndpointId == endpointId {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.currentForcedSpeakerWithVideo?.0 == peer.id ? strongSelf.presentationData.strings.VoiceChat_UnpinVideo : strongSelf.presentationData.strings.VoiceChat_PinVideo, icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.currentForcedSpeakerWithVideo == peer.id ? strongSelf.presentationData.strings.VoiceChat_UnpinVideo : strongSelf.presentationData.strings.VoiceChat_PinVideo, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pin"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self else {
|
||||
@ -1705,8 +1715,6 @@ public final class VoiceChatController: ViewController {
|
||||
self.topPanelNode.addSubnode(self.closeButton)
|
||||
self.topPanelNode.addSubnode(self.topCornersNode)
|
||||
|
||||
self.bottomPanelNode.addSubnode(self.bottomCornersNode)
|
||||
self.bottomPanelNode.addSubnode(self.bottomPanelBackgroundNode)
|
||||
self.bottomPanelNode.addSubnode(self.audioButton)
|
||||
if let _ = self.mainVideoContainerNode {
|
||||
self.bottomPanelNode.addSubnode(self.cameraButton)
|
||||
@ -1731,6 +1739,8 @@ public final class VoiceChatController: ViewController {
|
||||
self.contentContainer.addSubnode(self.leftBorderNode)
|
||||
self.contentContainer.addSubnode(self.rightBorderNode)
|
||||
self.contentContainer.addSubnode(self.bottomPanelCoverNode)
|
||||
self.contentContainer.addSubnode(self.bottomCornersNode)
|
||||
self.contentContainer.addSubnode(self.bottomPanelBackgroundNode)
|
||||
self.contentContainer.addSubnode(self.bottomPanelNode)
|
||||
self.contentContainer.addSubnode(self.timerNode)
|
||||
self.contentContainer.addSubnode(self.scheduleTextNode)
|
||||
@ -1887,12 +1897,12 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
if let (peerId, endpointId, _) = maxLevelWithVideo {
|
||||
/*if strongSelf.currentDominantSpeakerWithVideo?.0 != peerId || strongSelf.currentDominantSpeakerWithVideo?.1 != endpointId {
|
||||
strongSelf.currentDominantSpeakerWithVideo = (peerId, endpointId)
|
||||
strongSelf.call.setFullSizeVideo(endpointId: endpointId)
|
||||
strongSelf.mainVideoContainerNode?.updatePeer(peer: (peerId: peerId, source: endpointId), waitForFullSize: true)
|
||||
}*/
|
||||
if let (peerId, _, _) = maxLevelWithVideo {
|
||||
if strongSelf.currentDominantSpeakerWithVideo != peerId {
|
||||
strongSelf.currentDominantSpeakerWithVideo = peerId
|
||||
|
||||
strongSelf.updatePinnedParticipant(waitForFullSize: true)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.itemInteraction?.updateAudioLevels(levels)
|
||||
@ -1937,7 +1947,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let strongSelf = self {
|
||||
strongSelf.currentContentOffset = offset
|
||||
if !strongSelf.animatingExpansion && !strongSelf.animatingInsertion && strongSelf.panGestureArguments == nil && !strongSelf.animatingAppearance {
|
||||
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition)
|
||||
strongSelf.updateDecorationsLayout(transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2052,13 +2062,13 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
if let (peerId, endpointId) = strongSelf.effectiveSpeakerWithVideo {
|
||||
if !validSources.contains(endpointId) {
|
||||
if peerId == strongSelf.currentForcedSpeakerWithVideo?.0 {
|
||||
if peerId == strongSelf.currentForcedSpeakerWithVideo {
|
||||
strongSelf.currentForcedSpeakerWithVideo = nil
|
||||
}
|
||||
if peerId == strongSelf.currentDominantSpeakerWithVideo?.0 {
|
||||
if peerId == strongSelf.currentDominantSpeakerWithVideo {
|
||||
strongSelf.currentDominantSpeakerWithVideo = nil
|
||||
}
|
||||
strongSelf.updatePinnedParticipant()
|
||||
strongSelf.updatePinnedParticipant(waitForFullSize: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2070,7 +2080,7 @@ public final class VoiceChatController: ViewController {
|
||||
}))
|
||||
|
||||
self.titleNode.tapped = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let strongSelf = self, !strongSelf.isScheduling {
|
||||
if strongSelf.callState?.canManageCall ?? false {
|
||||
strongSelf.openTitleEditing()
|
||||
} else if !strongSelf.titleNode.recordingIconNode.isHidden {
|
||||
@ -2152,7 +2162,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
strongSelf.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
if let (peerId, _) = minimalVisiblePeerid {
|
||||
@ -2200,7 +2210,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
strongSelf.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
if let (peerId, _) = minimalVisiblePeerid {
|
||||
@ -2224,7 +2234,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
strongSelf.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3236,16 +3246,22 @@ public final class VoiceChatController: ViewController {
|
||||
self.call.disableVideo()
|
||||
self.call.disableScreencast()
|
||||
} else {
|
||||
#if DEBUG
|
||||
//self.call.requestScreencast()
|
||||
self.call.requestVideo()
|
||||
return;
|
||||
#endif
|
||||
|
||||
let controller = voiceChatCameraPreviewController(sharedContext: self.context.sharedContext, account: self.context.account, forceTheme: self.darkTheme, title: self.presentationData.strings.VoiceChat_VideoPreviewTitle, text: self.presentationData.strings.VoiceChat_VideoPreviewDescription, apply: { [weak self] in
|
||||
self?.call.requestVideo()
|
||||
})
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
self.call.makeOutgoingVideoView { [weak self] view in
|
||||
guard let strongSelf = self, let view = view else {
|
||||
return
|
||||
}
|
||||
let cameraNode = GroupVideoNode(videoView: view)
|
||||
let controller = VoiceChatCameraPreviewController(context: strongSelf.context, cameraNode: cameraNode, shareCamera: { [weak self] videoNode in
|
||||
if let strongSelf = self {
|
||||
strongSelf.call.requestVideo()
|
||||
}
|
||||
}, switchCamera: { [weak self] in
|
||||
self?.call.switchVideoCamera()
|
||||
}, shareScreen: { [weak self] in
|
||||
self?.call.requestScreencast()
|
||||
})
|
||||
strongSelf.controller?.present(controller, in: .window(.root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3271,20 +3287,11 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
private var bringVideoToBackOnCompletion = false
|
||||
private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) {
|
||||
private func updateDecorationsLayout(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) {
|
||||
guard let (layout, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
|
||||
let listTopInset = layoutTopInset + topPanelHeight
|
||||
let bottomPanelHeight = self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom
|
||||
|
||||
var size = layout.size
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
size.width = floor(min(size.width, size.height) * 0.5)
|
||||
}
|
||||
|
||||
|
||||
var isLandscape = false
|
||||
var effectiveDisplayMode = self.displayMode
|
||||
if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height {
|
||||
@ -3295,7 +3302,24 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
|
||||
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
|
||||
let listTopInset = isLandscape ? topPanelHeight : layoutTopInset + topPanelHeight
|
||||
let bottomPanelHeight = isLandscape ? layout.intrinsicInsets.bottom : self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom
|
||||
|
||||
var size = layout.size
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
size.width = floor(min(size.width, size.height) * 0.5)
|
||||
}
|
||||
|
||||
let contentWidth: CGFloat
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
size.width = floor(min(size.width, size.height) * 0.5)
|
||||
contentWidth = size.width
|
||||
} else {
|
||||
contentWidth = isLandscape ? min(530.0, size.width - 210.0) : size.width
|
||||
}
|
||||
|
||||
let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - bottomPanelHeight)
|
||||
let topInset: CGFloat
|
||||
if let (panInitialTopInset, panOffset) = self.panGestureArguments {
|
||||
if self.isExpanded {
|
||||
@ -3319,9 +3343,8 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let offset = (bottomEdge.isZero ? 0.0 : offset) + topInset
|
||||
self.floatingHeaderOffset = offset
|
||||
let currentContentOffset = self.currentContentOffset ?? 0.0
|
||||
let offset = (bottomEdge.isZero ? 0.0 : currentContentOffset) + topInset
|
||||
|
||||
if bottomEdge.isZero {
|
||||
bottomEdge = self.listNode.frame.minY + 46.0 + 56.0
|
||||
@ -3331,7 +3354,7 @@ public final class VoiceChatController: ViewController {
|
||||
let panelOffset = max(layoutTopInset, rawPanelOffset)
|
||||
let topPanelFrame: CGRect
|
||||
if isLandscape {
|
||||
topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 0.0))
|
||||
topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: topPanelHeight))
|
||||
} else {
|
||||
topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: size.width, height: topPanelHeight))
|
||||
}
|
||||
@ -3423,8 +3446,8 @@ public final class VoiceChatController: ViewController {
|
||||
let leftBorderFrame: CGRect
|
||||
let rightBorderFrame: CGRect
|
||||
if isLandscape {
|
||||
leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.safeInsets.left, height: layout.size.height))
|
||||
rightBorderFrame = CGRect(origin: CGPoint(x: size.width - layout.safeInsets.right, y: 0.0), size: CGSize(width: layout.safeInsets.right, height: layout.size.height))
|
||||
leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY - 16.0), size: CGSize(width: (size.width - contentWidth) / 2.0 + sideInset, height: layout.size.height))
|
||||
rightBorderFrame = CGRect(origin: CGPoint(x: size.width - (size.width - contentWidth) / 2.0 - sideInset, y: topPanelFrame.maxY - 16.0), size: CGSize(width: layout.safeInsets.right + (size.width - contentWidth) / 2.0 + sideInset, height: layout.size.height))
|
||||
} else {
|
||||
leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY - 16.0), size: CGSize(width: sideInset, height: layout.size.height))
|
||||
rightBorderFrame = CGRect(origin: CGPoint(x: size.width - sideInset, y: topPanelFrame.maxY - 16.0), size: CGSize(width: sideInset, height: layout.size.height))
|
||||
@ -3458,10 +3481,10 @@ public final class VoiceChatController: ViewController {
|
||||
self.topPanelBackgroundNode.frame = CGRect(x: 0.0, y: topPanelHeight - 24.0, width: size.width, height: min(topPanelFrame.height, 24.0))
|
||||
|
||||
let listMaxY = listTopInset + listSize.height
|
||||
let bottomOffset: CGFloat = min(0.0, bottomEdge - listMaxY)
|
||||
let bottomOffset: CGFloat = min(0.0, bottomEdge - listMaxY) + layout.size.height - bottomPanelHeight
|
||||
let bottomDelta = self.effectiveBottomAreaHeight - bottomAreaHeight
|
||||
|
||||
let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset, y: -50.0 + bottomOffset + bottomDelta), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0))
|
||||
let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset + floorToScreenPixels((size.width - contentWidth) / 2.0), y: -50.0 + bottomOffset + bottomDelta), size: CGSize(width: contentWidth - sideInset * 2.0, height: 50.0))
|
||||
let previousBottomCornersFrame = self.bottomCornersNode.frame
|
||||
if !bottomCornersFrame.equalTo(previousBottomCornersFrame) {
|
||||
self.bottomCornersNode.frame = bottomCornersFrame
|
||||
@ -3500,8 +3523,10 @@ public final class VoiceChatController: ViewController {
|
||||
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
|
||||
}
|
||||
|
||||
var isLandscape = false
|
||||
var effectiveDisplayMode = self.displayMode
|
||||
if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height {
|
||||
isLandscape = true
|
||||
if case .fullscreen = effectiveDisplayMode {
|
||||
} else {
|
||||
effectiveDisplayMode = .fullscreen(controlsHidden: false)
|
||||
@ -3510,7 +3535,11 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
let backgroundColor: UIColor
|
||||
if case .fullscreen = effectiveDisplayMode {
|
||||
backgroundColor = fullscreenBackgroundColor
|
||||
if isLandscape {
|
||||
backgroundColor = isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor
|
||||
} else {
|
||||
backgroundColor = fullscreenBackgroundColor
|
||||
}
|
||||
} else if self.isScheduling || self.callState?.scheduleTimestamp != nil {
|
||||
backgroundColor = panelBackgroundColor
|
||||
} else {
|
||||
@ -3556,7 +3585,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
private func updateTitle(slide: Bool = false, transition: ContainedViewLayoutTransition) {
|
||||
guard let (layout, _) = self.validLayout else {
|
||||
guard let _ = self.validLayout else {
|
||||
return
|
||||
}
|
||||
var title = self.currentTitle
|
||||
@ -3583,12 +3612,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
var size = layout.size
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
size.width = floor(min(size.width, size.height) * 0.5)
|
||||
}
|
||||
|
||||
self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: subtitle, slide: slide, transition: transition)
|
||||
self.titleNode.update(size: CGSize(width: self.titleNode.bounds.width, height: 44.0), title: title, subtitle: subtitle, slide: slide, transition: transition)
|
||||
}
|
||||
|
||||
private func updateButtons(animated: Bool) {
|
||||
@ -3688,7 +3712,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate
|
||||
self.cameraButton.update(size: videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .camera), text: self.presentationData.strings.VoiceChat_Video, transition: transition)
|
||||
self.cameraButton.update(size: videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition)
|
||||
|
||||
self.switchCameraButton.update(size: videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .flipCamera), text: "", transition: transition)
|
||||
|
||||
@ -3709,8 +3733,12 @@ public final class VoiceChatController: ViewController {
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
|
||||
var size = layout.size
|
||||
let contentWidth: CGFloat
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
size.width = floor(min(size.width, size.height) * 0.5)
|
||||
contentWidth = size.width
|
||||
} else {
|
||||
contentWidth = isLandscape ? min(530.0, size.width - 210.0) : size.width
|
||||
}
|
||||
|
||||
let isScheduled = self.isScheduling || self.callState?.scheduleTimestamp != nil
|
||||
@ -3728,7 +3756,7 @@ public final class VoiceChatController: ViewController {
|
||||
if !self.isFullscreen {
|
||||
self.isExpanded = true
|
||||
self.updateIsFullscreen(true)
|
||||
self.tileListNode.isHidden = false
|
||||
// self.tileListNode.isHidden = false
|
||||
}
|
||||
if case .fullscreen = effectiveDisplayMode {
|
||||
} else {
|
||||
@ -3752,34 +3780,37 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentWidth) / 2.0), y: 10.0), size: CGSize(width: contentWidth, height: 44.0)))
|
||||
self.updateTitle(transition: transition)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: CGSize(width: size.width, height: 44.0)))
|
||||
transition.updateFrame(node: self.optionsButton, frame: CGRect(origin: CGPoint(x: 20.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - 20.0 - 28.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
|
||||
transition.updateFrame(node: self.optionsButton, frame: CGRect(origin: CGPoint(x: 20.0 + floorToScreenPixels((size.width - contentWidth) / 2.0), y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - floorToScreenPixels((size.width - contentWidth) / 2.0) - 20.0 - 28.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: 0.0), size: size))
|
||||
|
||||
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.left = layout.safeInsets.left + sideInset
|
||||
insets.right = layout.safeInsets.right + sideInset
|
||||
insets.left = sideInset + (isLandscape ? 0.0 : layout.safeInsets.left)
|
||||
insets.right = sideInset + (isLandscape ? 0.0 : layout.safeInsets.right)
|
||||
|
||||
let topEdgeOffset: CGFloat
|
||||
if let statusBarHeight = layout.statusBarHeight {
|
||||
topEdgeOffset = statusBarHeight
|
||||
} else {
|
||||
topEdgeOffset = 44.0
|
||||
}
|
||||
|
||||
if isLandscape {
|
||||
transition.updateFrame(node: self.topPanelEdgeNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: 0.0))
|
||||
transition.updateFrame(node: self.topPanelEdgeNode, frame: CGRect(x: 0.0, y: -topEdgeOffset, width: size.width, height: topPanelHeight + topEdgeOffset))
|
||||
} else if let _ = self.panGestureArguments {
|
||||
} else {
|
||||
let topEdgeFrame: CGRect
|
||||
if self.isFullscreen {
|
||||
let offset: CGFloat
|
||||
if let statusBarHeight = layout.statusBarHeight {
|
||||
offset = statusBarHeight
|
||||
} else {
|
||||
offset = 44.0
|
||||
}
|
||||
topEdgeFrame = CGRect(x: 0.0, y: -offset, width: size.width, height: topPanelHeight + offset)
|
||||
topEdgeFrame = CGRect(x: 0.0, y: -topEdgeOffset, width: size.width, height: topPanelHeight + topEdgeOffset)
|
||||
} else {
|
||||
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
|
||||
}
|
||||
@ -3790,15 +3821,15 @@ public final class VoiceChatController: ViewController {
|
||||
var listTopInset = layoutTopInset + topPanelHeight
|
||||
var topCornersY = topPanelHeight
|
||||
if isLandscape {
|
||||
listTopInset = 0.0
|
||||
topCornersY = -50.0
|
||||
listTopInset = topPanelHeight
|
||||
// topCornersY = -50.0
|
||||
} else if self.mainVideoContainerNode != nil && self.isFullscreen {
|
||||
let videoContainerHeight = min(mainVideoHeight, layout.size.width)
|
||||
listTopInset += videoContainerHeight
|
||||
topCornersY += videoContainerHeight
|
||||
}
|
||||
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
|
||||
|
||||
|
||||
let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - (isLandscape ? layout.intrinsicInsets.bottom : bottomPanelHeight))
|
||||
let topInset: CGFloat
|
||||
if let (panInitialTopInset, panOffset) = self.panGestureArguments {
|
||||
if self.isExpanded {
|
||||
@ -3812,7 +3843,7 @@ public final class VoiceChatController: ViewController {
|
||||
topInset = listSize.height - 46.0 - floor(56.0 * 3.5)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset + topInset), size: listSize))
|
||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentWidth) / 2.0), y: listTopInset + topInset), size: listSize))
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: listSize, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
@ -3845,28 +3876,29 @@ public final class VoiceChatController: ViewController {
|
||||
self.tileListNode.transform = tileListTransform
|
||||
self.tileListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: tileListUpdateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
transition.updateFrame(node: self.topCornersNode, frame: CGRect(origin: CGPoint(x: sideInset, y: topCornersY), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0)))
|
||||
transition.updateFrame(node: self.topCornersNode, frame: CGRect(origin: CGPoint(x: sideInset + floorToScreenPixels((size.width - contentWidth) / 2.0), y: topCornersY), size: CGSize(width: contentWidth - sideInset * 2.0, height: 50.0)))
|
||||
|
||||
var bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight))
|
||||
let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom
|
||||
let bottomPanelCoverFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: size.width, height: bottomPanelCoverHeight))
|
||||
if isLandscape {
|
||||
transition.updateAlpha(node: self.closeButton, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.optionsButton, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.titleNode, alpha: 0.0)
|
||||
// transition.updateAlpha(node: self.closeButton, alpha: 0.0)
|
||||
// transition.updateAlpha(node: self.optionsButton, alpha: 0.0)
|
||||
// transition.updateAlpha(node: self.titleNode, alpha: 0.0)
|
||||
bottomPanelFrame = CGRect(origin: CGPoint(x: layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right, y: 0.0), size: CGSize(width: fullscreenBottomAreaHeight + layout.safeInsets.right, height: layout.size.height))
|
||||
} else {
|
||||
transition.updateAlpha(node: self.closeButton, alpha: 1.0)
|
||||
transition.updateAlpha(node: self.optionsButton, alpha: self.optionsButton.isUserInteractionEnabled ? 1.0 : 0.0)
|
||||
transition.updateAlpha(node: self.titleNode, alpha: 1.0)
|
||||
// transition.updateAlpha(node: self.closeButton, alpha: 1.0)
|
||||
// transition.updateAlpha(node: self.optionsButton, alpha: self.optionsButton.isUserInteractionEnabled ? 1.0 : 0.0)
|
||||
// transition.updateAlpha(node: self.titleNode, alpha: 1.0)
|
||||
}
|
||||
transition.updateAlpha(node: self.optionsButton, alpha: self.optionsButton.isUserInteractionEnabled ? 1.0 : 0.0)
|
||||
transition.updateFrame(node: self.bottomPanelCoverNode, frame: bottomPanelCoverFrame)
|
||||
transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame)
|
||||
|
||||
if let pickerView = self.pickerView {
|
||||
transition.updateFrame(view: pickerView, frame: CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0))
|
||||
}
|
||||
|
||||
|
||||
let timerFrame = CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0)
|
||||
transition.updateFrame(node: self.timerNode, frame: timerFrame)
|
||||
self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate)
|
||||
@ -3905,16 +3937,26 @@ public final class VoiceChatController: ViewController {
|
||||
forthButtonFrame = rightButtonFrame
|
||||
case let .fullscreen(controlsHidden):
|
||||
smallButtons = true
|
||||
let sideInset: CGFloat = 26.0
|
||||
|
||||
if isLandscape {
|
||||
let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * 4.0) / 3.0)
|
||||
let sideInset: CGFloat
|
||||
let buttonsCount: Int
|
||||
if self.mainVideoContainerNode == nil {
|
||||
sideInset = 42.0
|
||||
buttonsCount = 3
|
||||
} else {
|
||||
sideInset = 26.0
|
||||
buttonsCount = 4
|
||||
}
|
||||
let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1)))
|
||||
let x = controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0: floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0)
|
||||
forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize)
|
||||
let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize)
|
||||
thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize)
|
||||
secondButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height - spacing - sideButtonSize.height), size: sideButtonSize)
|
||||
secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize)
|
||||
firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize)
|
||||
} else {
|
||||
let sideInset: CGFloat = 26.0
|
||||
let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * 4.0) / 3.0)
|
||||
let y = controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)
|
||||
firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize)
|
||||
@ -4047,7 +4089,7 @@ public final class VoiceChatController: ViewController {
|
||||
guard let (layout, navigationHeight) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.updateFloatingHeaderOffset(offset: 0.0, transition: .immediate)
|
||||
self.updateDecorationsLayout(transition: .immediate)
|
||||
|
||||
self.animatingAppearance = true
|
||||
|
||||
@ -4172,9 +4214,7 @@ public final class VoiceChatController: ViewController {
|
||||
itemsCount -= 1
|
||||
}
|
||||
itemsHeight += CGFloat(itemsCount) * 56.0
|
||||
|
||||
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
|
||||
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
var insets = UIEdgeInsets()
|
||||
insets.left = layout.safeInsets.left + sideInset
|
||||
@ -4185,7 +4225,8 @@ public final class VoiceChatController: ViewController {
|
||||
size.width = floor(min(size.width, size.height) * 0.5)
|
||||
}
|
||||
|
||||
let bottomPanelHeight = self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom
|
||||
let bottomPanelHeight = self.isLandscape ? layout.intrinsicInsets.bottom : self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom
|
||||
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
|
||||
let listTopInset = layoutTopInset + topPanelHeight
|
||||
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
|
||||
|
||||
@ -4207,9 +4248,9 @@ public final class VoiceChatController: ViewController {
|
||||
return
|
||||
}
|
||||
if isFirstTime {
|
||||
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .immediate)
|
||||
strongSelf.updateDecorationsLayout(transition: .immediate)
|
||||
} else if strongSelf.animatingInsertion {
|
||||
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
strongSelf.updateDecorationsLayout(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
strongSelf.animatingInsertion = false
|
||||
if !strongSelf.didSetContentsReady {
|
||||
@ -4275,12 +4316,7 @@ public final class VoiceChatController: ViewController {
|
||||
entries.append(.invite(self.presentationData.theme, self.presentationData.strings, inviteIsLink ? self.presentationData.strings.VoiceChat_Share : self.presentationData.strings.VoiceChat_InviteMember, inviteIsLink))
|
||||
}
|
||||
|
||||
if let _ = self.effectiveSpeakerWithVideo {
|
||||
index += 1
|
||||
}
|
||||
|
||||
var pinnedEntry: ListEntry?
|
||||
|
||||
for member in callMembers.0 {
|
||||
if processedPeerIds.contains(member.peer.id) {
|
||||
continue
|
||||
@ -4339,7 +4375,8 @@ public final class VoiceChatController: ViewController {
|
||||
isMyPeer: self.callState?.myPeerId == member.peer.id,
|
||||
ssrc: member.ssrc,
|
||||
effectiveVideoEndpointId: member.presentationEndpointId ?? member.videoEndpointId,
|
||||
presence: nil,
|
||||
hasVideo: member.videoEndpointId != nil,
|
||||
hasScreencast: member.presentationEndpointId != nil,
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: memberState,
|
||||
muteState: memberMuteState,
|
||||
@ -4359,7 +4396,8 @@ public final class VoiceChatController: ViewController {
|
||||
isMyPeer: self.callState?.myPeerId == member.peer.id,
|
||||
ssrc: member.ssrc,
|
||||
effectiveVideoEndpointId: member.presentationEndpointId ?? member.videoEndpointId,
|
||||
presence: nil,
|
||||
hasVideo: member.videoEndpointId != nil,
|
||||
hasScreencast: member.presentationEndpointId != nil,
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: memberState,
|
||||
muteState: memberMuteState,
|
||||
@ -4377,11 +4415,12 @@ public final class VoiceChatController: ViewController {
|
||||
if memberPeer.id == self.effectiveSpeakerWithVideo?.0 {
|
||||
pinnedEntry = .peer(PeerEntry(
|
||||
peer: memberPeer,
|
||||
about: member.about,
|
||||
about: nil,
|
||||
isMyPeer: self.callState?.myPeerId == member.peer.id,
|
||||
ssrc: member.ssrc,
|
||||
effectiveVideoEndpointId: member.presentationEndpointId ?? member.videoEndpointId,
|
||||
presence: nil,
|
||||
hasVideo: false,
|
||||
hasScreencast: false,
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: memberState,
|
||||
muteState: memberMuteState,
|
||||
@ -4408,7 +4447,8 @@ public final class VoiceChatController: ViewController {
|
||||
isMyPeer: false,
|
||||
ssrc: nil,
|
||||
effectiveVideoEndpointId: nil,
|
||||
presence: nil,
|
||||
hasVideo: false,
|
||||
hasScreencast: false,
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: .invited,
|
||||
muteState: nil,
|
||||
@ -4467,20 +4507,20 @@ public final class VoiceChatController: ViewController {
|
||||
self.enqueueTileTransition(tileTransition)
|
||||
}
|
||||
|
||||
private func updatePinnedParticipant() {
|
||||
private func updatePinnedParticipant(waitForFullSize: Bool) {
|
||||
let effectivePinnedParticipant = self.currentForcedSpeakerWithVideo ?? self.currentDominantSpeakerWithVideo
|
||||
guard effectivePinnedParticipant?.0 != self.effectiveSpeakerWithVideo?.0 || effectivePinnedParticipant?.1 != self.effectiveSpeakerWithVideo?.1 else {
|
||||
guard effectivePinnedParticipant != self.effectiveSpeakerWithVideo?.0 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let (peerId, _) = effectivePinnedParticipant {
|
||||
if let peerId = effectivePinnedParticipant {
|
||||
for entry in self.currentEntries {
|
||||
switch entry {
|
||||
case let .peer(peer):
|
||||
if peer.peer.id == peerId, let endpointId = peer.effectiveVideoEndpointId {
|
||||
self.effectiveSpeakerWithVideo = (peerId, endpointId)
|
||||
self.call.setFullSizeVideo(endpointId: endpointId)
|
||||
self.mainVideoContainerNode?.updatePeer(peer: (peerId: peerId, endpointId: endpointId), waitForFullSize: false)
|
||||
self.mainVideoContainerNode?.updatePeer(peer: (peerId: peerId, endpointId: endpointId), waitForFullSize: waitForFullSize)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -4577,7 +4617,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .immediate)
|
||||
self.updateDecorationsLayout(transition: .immediate)
|
||||
}
|
||||
|
||||
if !self.isExpanded {
|
||||
@ -4625,7 +4665,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.animatingExpansion = false
|
||||
})
|
||||
} else {
|
||||
@ -4635,7 +4675,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.animatingExpansion = false
|
||||
})
|
||||
}
|
||||
@ -4663,7 +4703,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.animatingExpansion = false
|
||||
})
|
||||
} else if !isScheduling {
|
||||
@ -4674,7 +4714,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.animatingExpansion = false
|
||||
})
|
||||
}
|
||||
@ -4698,7 +4738,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.animatingExpansion = false
|
||||
})
|
||||
default:
|
||||
|
@ -25,10 +25,21 @@ final class VoiceChatParticipantItem: ListViewItem {
|
||||
case tile(isLandscape: Bool)
|
||||
}
|
||||
|
||||
enum ParticipantText {
|
||||
public enum Icon {
|
||||
case volume
|
||||
case video
|
||||
enum ParticipantText: Equatable {
|
||||
public struct TextIcon: OptionSet {
|
||||
public var rawValue: Int32
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
public static let volume = TextIcon(rawValue: 1 << 0)
|
||||
public static let video = TextIcon(rawValue: 1 << 1)
|
||||
public static let screen = TextIcon(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
public enum TextColor {
|
||||
@ -39,7 +50,7 @@ final class VoiceChatParticipantItem: ListViewItem {
|
||||
}
|
||||
|
||||
case presence
|
||||
case text(String, Icon?, TextColor)
|
||||
case text(String, TextIcon, TextColor)
|
||||
case none
|
||||
}
|
||||
|
||||
@ -179,7 +190,7 @@ private let borderImage = generateImage(CGSize(width: tileSize.width, height: ti
|
||||
context.clear(bounds)
|
||||
|
||||
context.setLineWidth(borderLineWidth)
|
||||
context.setStrokeColor(accentColor.cgColor)
|
||||
context.setStrokeColor(constructiveColor.cgColor)
|
||||
|
||||
context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: (borderLineWidth - UIScreenPixel) / 2.0, dy: (borderLineWidth - UIScreenPixel) / 2.0), cornerRadius: backgroundCornerRadius - UIScreenPixel).cgPath)
|
||||
context.strokePath()
|
||||
@ -196,23 +207,109 @@ private let fadeImage = generateImage(CGSize(width: 1.0, height: 30.0), rotatedC
|
||||
})
|
||||
|
||||
private class VoiceChatParticipantStatusNode: ASDisplayNode {
|
||||
private let iconNode: ASImageNode
|
||||
private var iconNodes: [ASImageNode]
|
||||
private let textNode: TextNode
|
||||
|
||||
private var currentParams: (CGSize, VoiceChatParticipantItem.ParticipantText)?
|
||||
|
||||
override init() {
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.iconNodes = []
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.contentMode = .left
|
||||
self.textNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
func update() {
|
||||
func asyncLayout() -> (_ size: CGSize, _ text: VoiceChatParticipantItem.ParticipantText, _ transparent: Bool) -> (CGSize, () -> Void) {
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
return { size, text, transparent in
|
||||
let statusFont = Font.regular(14.0)
|
||||
|
||||
var attributedString: NSAttributedString?
|
||||
var color: UIColor = .white
|
||||
var hasVolume = false
|
||||
var hasVideo = false
|
||||
var hasScreen = false
|
||||
switch text {
|
||||
case let .text(text, textIcon, textColor):
|
||||
hasVolume = textIcon.contains(.volume)
|
||||
hasVideo = textIcon.contains(.video)
|
||||
hasScreen = textIcon.contains(.screen)
|
||||
|
||||
var textColorValue: UIColor
|
||||
switch textColor {
|
||||
case .generic:
|
||||
textColorValue = UIColor(rgb: 0x98989e)
|
||||
case .accent:
|
||||
textColorValue = accentColor
|
||||
case .constructive:
|
||||
textColorValue = constructiveColor
|
||||
case .destructive:
|
||||
textColorValue = destructiveColor
|
||||
}
|
||||
if transparent {
|
||||
textColorValue = UIColor(rgb: 0xffffff, alpha: 0.65)
|
||||
}
|
||||
color = textColorValue
|
||||
attributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 16.0, height: 16.0)
|
||||
let spacing: CGFloat = 3.0
|
||||
|
||||
var icons: [UIImage] = []
|
||||
if hasVolume, let image = generateTintedImage(image: UIImage(bundleImageName: "Call/StatusVolume"), color: color) {
|
||||
icons.append(image)
|
||||
}
|
||||
if hasVideo, let image = generateTintedImage(image: UIImage(bundleImageName: "Call/StatusVideo"), color: color) {
|
||||
icons.append(image)
|
||||
}
|
||||
if hasScreen, let image = generateTintedImage(image: UIImage(bundleImageName: "Call/StatusScreen"), color: color) {
|
||||
icons.append(image)
|
||||
}
|
||||
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: size.width - (iconSize.width + spacing) * CGFloat(icons.count), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
var contentSize = textLayout.size
|
||||
contentSize.width += (iconSize.width + spacing) * CGFloat(icons.count)
|
||||
|
||||
return (contentSize, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.currentParams = (size, text)
|
||||
|
||||
for i in 0 ..< icons.count {
|
||||
let iconNode: ASImageNode
|
||||
if strongSelf.iconNodes.count >= i + 1 {
|
||||
iconNode = strongSelf.iconNodes[i]
|
||||
} else {
|
||||
iconNode = ASImageNode()
|
||||
strongSelf.addSubnode(iconNode)
|
||||
strongSelf.iconNodes.append(iconNode)
|
||||
}
|
||||
iconNode.frame = CGRect(origin: CGPoint(x: (iconSize.width + spacing) * CGFloat(i), y: 0.0), size: iconSize)
|
||||
|
||||
iconNode.image = icons[i]
|
||||
}
|
||||
if strongSelf.iconNodes.count > icons.count {
|
||||
for i in icons.count ..< strongSelf.iconNodes.count {
|
||||
strongSelf.iconNodes[i].image = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let _ = textApply()
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: (iconSize.width + spacing) * CGFloat(icons.count), y: 0.0), size: textLayout.size)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,9 +334,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
private let pinIconNode: ASImageNode
|
||||
private let contentWrapperNode: ASDisplayNode
|
||||
private let titleNode: TextNode
|
||||
private let statusIconNode: ASImageNode
|
||||
private let statusNode: TextNode
|
||||
private let expandedStatusNode: TextNode
|
||||
private let statusNode: VoiceChatParticipantStatusNode
|
||||
private let expandedStatusNode: VoiceChatParticipantStatusNode
|
||||
private var credibilityIconNode: ASImageNode?
|
||||
|
||||
private var avatarTransitionNode: ASImageNode?
|
||||
@ -332,18 +428,11 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.statusIconNode = ASImageNode()
|
||||
self.statusIconNode.displaysAsynchronously = false
|
||||
|
||||
self.statusNode = TextNode()
|
||||
self.statusNode = VoiceChatParticipantStatusNode()
|
||||
self.statusNode.isUserInteractionEnabled = false
|
||||
self.statusNode.contentMode = .left
|
||||
self.statusNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.expandedStatusNode = TextNode()
|
||||
self.expandedStatusNode = VoiceChatParticipantStatusNode()
|
||||
self.expandedStatusNode.isUserInteractionEnabled = false
|
||||
self.expandedStatusNode.contentMode = .left
|
||||
self.expandedStatusNode.contentsScale = UIScreen.main.scale
|
||||
self.expandedStatusNode.alpha = 0.0
|
||||
|
||||
self.actionContainerNode = ASDisplayNode()
|
||||
@ -366,7 +455,6 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
self.offsetContainerNode.addSubnode(self.videoContainerNode)
|
||||
self.offsetContainerNode.addSubnode(self.contentWrapperNode)
|
||||
self.contentWrapperNode.addSubnode(self.titleNode)
|
||||
self.contentWrapperNode.addSubnode(self.statusIconNode)
|
||||
self.contentWrapperNode.addSubnode(self.statusNode)
|
||||
self.contentWrapperNode.addSubnode(self.expandedStatusNode)
|
||||
self.contentWrapperNode.addSubnode(self.actionContainerNode)
|
||||
@ -877,8 +965,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
func asyncLayout() -> (_ item: VoiceChatParticipantItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
|
||||
let makeExpandedStatusLayout = TextNode.asyncLayout(self.expandedStatusNode)
|
||||
let makeStatusLayout = self.statusNode.asyncLayout()
|
||||
let makeExpandedStatusLayout = self.expandedStatusNode.asyncLayout()
|
||||
var currentDisabledOverlayNode = self.disabledOverlayNode
|
||||
|
||||
let currentItem = self.layoutParams?.0
|
||||
@ -891,11 +979,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
var titleFont = item.style == .list ? Font.regular(17.0) : Font.regular(12.0)
|
||||
let statusFont = Font.regular(14.0)
|
||||
|
||||
var titleAttributedString: NSAttributedString?
|
||||
var statusAttributedString: NSAttributedString?
|
||||
var expandedStatusAttributedString: NSAttributedString?
|
||||
|
||||
let rightInset: CGFloat = params.rightInset
|
||||
|
||||
@ -960,60 +1045,15 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
var wavesColor = UIColor(rgb: 0x34c759)
|
||||
switch item.text {
|
||||
case .presence:
|
||||
if let user = item.peer as? TelegramUser, let botInfo = user.botInfo {
|
||||
let botStatus: String
|
||||
if botInfo.flags.contains(.hasAccessToChatHistory) {
|
||||
botStatus = item.presentationData.strings.Bot_GroupStatusReadsHistory
|
||||
} else {
|
||||
botStatus = item.presentationData.strings.Bot_GroupStatusDoesNotReadHistory
|
||||
}
|
||||
statusAttributedString = NSAttributedString(string: botStatus, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
} else if let presence = item.presence as? TelegramUserPresence {
|
||||
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
let (string, _) = stringAndActivityForUserPresence(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp))
|
||||
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
} else {
|
||||
statusAttributedString = NSAttributedString(string: item.presentationData.strings.LastSeen_Offline, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
case let .text(text, textIcon, textColor):
|
||||
var textColorValue: UIColor
|
||||
switch textColor {
|
||||
case .generic:
|
||||
textColorValue = item.presentationData.theme.list.itemSecondaryTextColor
|
||||
case .accent:
|
||||
textColorValue = item.presentationData.theme.list.itemAccentColor
|
||||
wavesColor = textColorValue
|
||||
case .constructive:
|
||||
textColorValue = constructiveColor
|
||||
case .destructive:
|
||||
textColorValue = destructiveColor
|
||||
wavesColor = textColorValue
|
||||
}
|
||||
if item.transparent && item.style == .list {
|
||||
textColorValue = UIColor(rgb: 0xffffff, alpha: 0.65)
|
||||
}
|
||||
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
if let expandedText = item.expandedText, case let .text(text, _, textColor) = expandedText {
|
||||
let textColorValue: UIColor
|
||||
if case let .text(_, _, textColor) = item.text {
|
||||
switch textColor {
|
||||
case .generic:
|
||||
textColorValue = item.presentationData.theme.list.itemSecondaryTextColor
|
||||
case .accent:
|
||||
textColorValue = item.presentationData.theme.list.itemAccentColor
|
||||
case .constructive:
|
||||
textColorValue = constructiveColor
|
||||
case .destructive:
|
||||
textColorValue = destructiveColor
|
||||
case .accent:
|
||||
wavesColor = accentColor
|
||||
case .destructive:
|
||||
wavesColor = destructiveColor
|
||||
default:
|
||||
break
|
||||
}
|
||||
expandedStatusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
|
||||
} else {
|
||||
expandedStatusAttributedString = statusAttributedString
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 58.0 + params.leftInset
|
||||
@ -1051,13 +1091,14 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
constrainedWidth = params.width - 24.0 - 10.0
|
||||
}
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(TextNodeLayoutArguments(attributedString: expandedStatusAttributedString, backgroundColor: nil, maximumNumberOfLines: 6, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - expandedRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let titleSpacing: CGFloat = statusLayout.size.height == 0.0 ? 0.0 : 1.0
|
||||
let (statusLayout, statusApply) = makeStatusLayout(CGSize(width: params.width - leftInset - 8.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), item.text, item.transparent && item.style == .list)
|
||||
let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(CGSize(width: params.width - leftInset - 8.0 - rightInset - expandedRightInset, height: CGFloat.greatestFiniteMagnitude), item.expandedText ?? item.text, false)
|
||||
|
||||
let titleSpacing: CGFloat = statusLayout.height == 0.0 ? 0.0 : 1.0
|
||||
|
||||
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
|
||||
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height
|
||||
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.height
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
@ -1122,6 +1163,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
return (layout, { [weak self] synchronousLoad, animated in
|
||||
if let strongSelf = self {
|
||||
var hadItem = strongSelf.layoutParams?.0 != nil
|
||||
strongSelf.layoutParams = (item, params, first, last)
|
||||
strongSelf.currentTitle = titleAttributedString?.string
|
||||
strongSelf.wavesColor = wavesColor
|
||||
@ -1167,7 +1209,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
var extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
|
||||
var extractedHeight = extractedRect.height + expandedStatusLayout.size.height - statusLayout.size.height
|
||||
var extractedHeight = extractedRect.height + expandedStatusLayout.height - statusLayout.height
|
||||
var extractedVerticalOffset: CGFloat = 0.0
|
||||
if item.peer.smallProfileImage != nil || strongSelf.videoNode != nil {
|
||||
extractedVerticalOffset = extractedRect.width
|
||||
@ -1209,9 +1251,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
strongSelf.accessibilityLabel = titleAttributedString?.string
|
||||
var combinedValueString = ""
|
||||
if let statusString = statusAttributedString?.string, !statusString.isEmpty {
|
||||
combinedValueString.append(statusString)
|
||||
}
|
||||
// if let statusString = statusAttributedString?.string, !statusString.isEmpty {
|
||||
// combinedValueString.append(statusString)
|
||||
// }
|
||||
|
||||
strongSelf.accessibilityValue = combinedValueString
|
||||
|
||||
@ -1222,8 +1264,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
if animated && hadItem {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
@ -1293,8 +1335,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: contentSize.height + -separatorHeight), size: CGSize(width: layoutSize.width - leftInset, height: separatorHeight)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
|
||||
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
|
||||
transition.updateFrame(node: strongSelf.expandedStatusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: expandedStatusLayout.size))
|
||||
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout))
|
||||
transition.updateFrame(node: strongSelf.expandedStatusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: expandedStatusLayout))
|
||||
|
||||
if let currentCredibilityIconImage = currentCredibilityIconImage {
|
||||
let iconNode: ASImageNode
|
||||
|
@ -66,6 +66,9 @@ final class VoiceChatTitleNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func update(size: CGSize, title: String, subtitle: String, slide: Bool, transition: ContainedViewLayoutTransition) {
|
||||
guard !size.width.isZero else {
|
||||
return
|
||||
}
|
||||
var titleUpdated = false
|
||||
if let previousTitle = self.titleNode.attributedText?.string {
|
||||
titleUpdated = previousTitle != title
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,8 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods-b515@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_call_airpodsmax.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.5 KiB |
BIN
submodules/TelegramUI/Images.xcassets/Call/CallAirpodsMaxButton.imageset/ic_call_airpodsmax.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/CallAirpodsMaxButton.imageset/ic_call_airpodsmax.pdf
vendored
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_call_flip.pdf",
|
||||
"filename" : "ic_cam_flip.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_vc_camera.pdf",
|
||||
"filename" : "ic_voicesharing.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "volsmall.pdf",
|
||||
"filename" : "ic_voicecamera.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_voicevolumeon.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/ic_voicevolumeon.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/ic_voicevolumeon.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_voicevolumeoff.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
@ -112,7 +112,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
public let engine: TelegramEngine
|
||||
|
||||
public let fetchManager: FetchManager
|
||||
private let prefetchManager: PrefetchManager?
|
||||
public let prefetchManager: PrefetchManager?
|
||||
|
||||
public var keyShortcutsController: KeyShortcutsController?
|
||||
|
||||
@ -172,7 +172,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
}
|
||||
self.fetchManager = FetchManagerImpl(postbox: account.postbox, storeManager: self.downloadedMediaStoreManager)
|
||||
if sharedContext.applicationBindings.isMainApp && !temp {
|
||||
self.prefetchManager = PrefetchManager(sharedContext: sharedContext, account: account, fetchManager: self.fetchManager)
|
||||
self.prefetchManager = PrefetchManagerImpl(sharedContext: sharedContext, account: account, engine: self.engine, fetchManager: self.fetchManager)
|
||||
self.wallpaperUploadManager = WallpaperUploadManagerImpl(sharedContext: sharedContext, account: account, presentationData: sharedContext.presentationData)
|
||||
self.themeUpdateManager = ThemeUpdateManagerImpl(sharedContext: sharedContext, account: account)
|
||||
} else {
|
||||
|
@ -418,7 +418,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
private var importStateDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, greetingData: ChatGreetingData? = nil) {
|
||||
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) {
|
||||
let _ = ChatControllerCount.modify { value in
|
||||
return value + 1
|
||||
}
|
||||
@ -463,7 +463,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
self.stickerSettings = ChatInterfaceStickerSettings(loopAnimatedStickers: false)
|
||||
|
||||
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, peerNearbyData: peerNearbyData, greetingData: greetingData, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
|
||||
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, peerNearbyData: peerNearbyData, greetingData: context.prefetchManager?.preloadedGreetingSticker, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
|
||||
self.presentationInterfaceStatePromise = ValuePromise(self.presentationInterfaceState)
|
||||
|
||||
var mediaAccessoryPanelVisibility = MediaAccessoryPanelVisibility.none
|
||||
|
@ -161,7 +161,7 @@ private final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNo
|
||||
} else if !self.didSetupSticker {
|
||||
let sticker: Signal<TelegramMediaFile?, NoError>
|
||||
if let preloadedSticker = interfaceState.greetingData?.sticker {
|
||||
sticker = .single(preloadedSticker)
|
||||
sticker = preloadedSticker
|
||||
} else {
|
||||
sticker = self.context.engine.stickers.randomGreetingSticker()
|
||||
|> map { item -> TelegramMediaFile? in
|
||||
@ -338,7 +338,7 @@ private final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNode
|
||||
} else if !self.didSetupSticker {
|
||||
let sticker: Signal<TelegramMediaFile?, NoError>
|
||||
if let preloadedSticker = interfaceState.greetingData?.sticker {
|
||||
sticker = .single(preloadedSticker)
|
||||
sticker = preloadedSticker
|
||||
} else {
|
||||
sticker = self.context.engine.stickers.randomGreetingSticker()
|
||||
|> map { item -> TelegramMediaFile? in
|
||||
@ -876,6 +876,7 @@ final class ChatEmptyNode: ASDisplayNode {
|
||||
node = ChatEmptyNodeNearbyChatContent(context: self.context, interaction: self.interaction)
|
||||
case .greeting:
|
||||
node = ChatEmptyNodeGreetingChatContent(context: self.context, interaction: self.interaction)
|
||||
self.context.prefetchManager?.prepareNextGreetingSticker()
|
||||
}
|
||||
self.content = (contentType, node)
|
||||
self.addSubnode(node)
|
||||
|
@ -71,7 +71,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
||||
})
|
||||
}
|
||||
} else {
|
||||
controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData, greetingData: params.greetingData)
|
||||
controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData)
|
||||
}
|
||||
controller.purposefulAction = params.purposefulAction
|
||||
if let search = params.activateMessageSearch {
|
||||
|
@ -1532,9 +1532,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
|
||||
private var groupMembersSearchContext: GroupMembersSearchContext?
|
||||
|
||||
private let preloadedSticker = Promise<TelegramMediaFile?>(nil)
|
||||
private let preloadStickerDisposable = MetaDisposable()
|
||||
|
||||
private let displayAsPeersPromise = Promise<[FoundPeer]>([])
|
||||
|
||||
fileprivate let accountsAndPeers = Promise<[(Account, Peer, Int32)]>()
|
||||
@ -2838,24 +2835,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
if let _ = nearbyPeerDistance {
|
||||
self.preloadHistoryDisposable.set(self.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerId))
|
||||
|
||||
self.preloadedSticker.set(.single(nil)
|
||||
|> then(context.engine.stickers.randomGreetingSticker()
|
||||
|> map { item in
|
||||
return item?.file
|
||||
}))
|
||||
|
||||
self.preloadStickerDisposable.set((self.preloadedSticker.get()
|
||||
|> mapToSignal { sticker -> Signal<Void, NoError> in
|
||||
if let sticker = sticker {
|
||||
let _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: sticker)).start()
|
||||
return chatMessageAnimationData(postbox: context.account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false)
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}).start())
|
||||
self.context.prefetchManager?.prepareNextGreetingSticker()
|
||||
}
|
||||
}
|
||||
|
||||
@ -2870,7 +2850,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
self.selectAddMemberDisposable.dispose()
|
||||
self.addMemberDisposable.dispose()
|
||||
self.preloadHistoryDisposable.dispose()
|
||||
self.preloadStickerDisposable.dispose()
|
||||
self.resolvePeerByNameDisposable?.dispose()
|
||||
self.navigationActionDisposable.dispose()
|
||||
self.enqueueMediaMessageDisposable.dispose()
|
||||
@ -3356,24 +3335,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
switch key {
|
||||
case .message:
|
||||
if let navigationController = controller.navigationController as? NavigationController {
|
||||
let _ = (self.preloadedSticker.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] sticker in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), keepStack: strongSelf.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: strongSelf.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), greetingData: strongSelf.nearbyPeerDistance != nil ? sticker.flatMap({ ChatGreetingData(sticker: $0) }) : nil, completion: { _ in
|
||||
if strongSelf.nearbyPeerDistance != nil {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers = viewControllers.filter { controller in
|
||||
if controller is PeerInfoScreen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in
|
||||
if let strongSelf = self, strongSelf.nearbyPeerDistance != nil {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers = viewControllers.filter { controller in
|
||||
if controller is PeerInfoScreen {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
return true
|
||||
}
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
case .discussion:
|
||||
if let cachedData = self.data?.cachedData as? CachedChannelData, case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId {
|
||||
@ -3773,24 +3746,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
|
||||
private func openChatWithMessageSearch() {
|
||||
if let navigationController = (self.controller?.navigationController as? NavigationController) {
|
||||
let _ = (self.preloadedSticker.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] sticker in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), keepStack: strongSelf.nearbyPeerDistance != nil ? .always : .default, activateMessageSearch: (.everything, ""), peerNearbyData: strongSelf.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), greetingData: strongSelf.nearbyPeerDistance != nil ? sticker.flatMap({ ChatGreetingData(sticker: $0) }) : nil, completion: { _ in
|
||||
if strongSelf.nearbyPeerDistance != nil {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers = viewControllers.filter { controller in
|
||||
if controller is PeerInfoScreen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), keepStack: self.nearbyPeerDistance != nil ? .always : .default, activateMessageSearch: (.everything, ""), peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in
|
||||
if let strongSelf = self, strongSelf.nearbyPeerDistance != nil {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers = viewControllers.filter { controller in
|
||||
if controller is PeerInfoScreen {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
return true
|
||||
}
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -4233,24 +4200,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
|
||||
private func openChat() {
|
||||
if let navigationController = self.controller?.navigationController as? NavigationController {
|
||||
let _ = (self.preloadedSticker.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] sticker in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), keepStack: strongSelf.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: strongSelf.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), greetingData: strongSelf.nearbyPeerDistance != nil ? sticker.flatMap({ ChatGreetingData(sticker: $0) }) : nil, completion: { _ in
|
||||
if strongSelf.nearbyPeerDistance != nil {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers = viewControllers.filter { controller in
|
||||
if controller is PeerInfoScreen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in
|
||||
if let strongSelf = self, strongSelf.nearbyPeerDistance != nil {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers = viewControllers.filter { controller in
|
||||
if controller is PeerInfoScreen {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
return true
|
||||
}
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import SyncCore
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import PhotoResources
|
||||
import StickerResources
|
||||
import Emoji
|
||||
import UniversalMediaPlayer
|
||||
|
||||
@ -21,18 +22,23 @@ public enum PrefetchMediaItem {
|
||||
case animatedEmojiSticker(TelegramMediaFile)
|
||||
}
|
||||
|
||||
private final class PrefetchManagerImpl {
|
||||
private final class PrefetchManagerInnerImpl {
|
||||
private let queue: Queue
|
||||
private let account: Account
|
||||
private let engine: TelegramEngine
|
||||
private let fetchManager: FetchManager
|
||||
|
||||
private var listDisposable: Disposable?
|
||||
|
||||
private var contexts: [MediaId: PrefetchMediaContext] = [:]
|
||||
|
||||
init(queue: Queue, sharedContext: SharedAccountContext, account: Account, fetchManager: FetchManager) {
|
||||
|
||||
private let preloadGreetingStickerDisposable = MetaDisposable()
|
||||
fileprivate let preloadedGreetingStickerPromise = Promise<TelegramMediaFile?>(nil)
|
||||
|
||||
init(queue: Queue, sharedContext: SharedAccountContext, account: Account, engine: TelegramEngine, fetchManager: FetchManager) {
|
||||
self.queue = queue
|
||||
self.account = account
|
||||
self.engine = engine
|
||||
self.fetchManager = fetchManager
|
||||
|
||||
let networkType = account.networkType
|
||||
@ -226,18 +232,63 @@ private final class PrefetchManagerImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func prepareNextGreetingSticker() {
|
||||
let account = self.account
|
||||
let engine = self.engine
|
||||
self.preloadedGreetingStickerPromise.set(.single(nil)
|
||||
|> then(engine.stickers.randomGreetingSticker()
|
||||
|> map { item in
|
||||
return item?.file
|
||||
}))
|
||||
|
||||
self.preloadGreetingStickerDisposable.set((self.preloadedGreetingStickerPromise.get()
|
||||
|> mapToSignal { sticker -> Signal<Void, NoError> in
|
||||
if let sticker = sticker {
|
||||
let _ = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: sticker)).start()
|
||||
return chatMessageAnimationData(postbox: account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false)
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}).start())
|
||||
}
|
||||
}
|
||||
|
||||
final class PrefetchManager {
|
||||
final class PrefetchManagerImpl: PrefetchManager {
|
||||
private let queue: Queue
|
||||
|
||||
private let impl: QueueLocalObject<PrefetchManagerImpl>
|
||||
private let impl: QueueLocalObject<PrefetchManagerInnerImpl>
|
||||
private let uuid = Atomic<UUID>(value: UUID())
|
||||
|
||||
init(sharedContext: SharedAccountContext, account: Account, fetchManager: FetchManager) {
|
||||
init(sharedContext: SharedAccountContext, account: Account, engine: TelegramEngine, fetchManager: FetchManager) {
|
||||
let queue = Queue.mainQueue()
|
||||
self.queue = queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return PrefetchManagerImpl(queue: queue, sharedContext: sharedContext, account: account, fetchManager: fetchManager)
|
||||
return PrefetchManagerInnerImpl(queue: queue, sharedContext: sharedContext, account: account, engine: engine, fetchManager: fetchManager)
|
||||
})
|
||||
}
|
||||
|
||||
var preloadedGreetingSticker: ChatGreetingData {
|
||||
let signal: Signal<TelegramMediaFile?, NoError> = Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set((impl.preloadedGreetingStickerPromise.get() |> take(1)).start(next: { file in
|
||||
subscriber.putNext(file)
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
return ChatGreetingData(uuid: uuid.with { $0 }, sticker: signal)
|
||||
}
|
||||
|
||||
func prepareNextGreetingSticker() {
|
||||
let _ = uuid.swap(UUID())
|
||||
self.impl.with { impl in
|
||||
impl.prepareNextGreetingSticker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 4c6ae0a13d9360d9ed51e78e4b79513bbedf5525
|
||||
Subproject commit 94a9c7b4e49c943d1ca108e35779739ad99d695a
|
Loading…
x
Reference in New Issue
Block a user