mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Add voice chat invite links support
This commit is contained in:
parent
b9f5db6a5a
commit
e2a6a70ea2
@ -6258,3 +6258,4 @@ Sorry for the inconvenience.";
|
||||
|
||||
"VoiceChat.MutedByAdmin" = "Muted by Admin";
|
||||
"VoiceChat.MutedByAdminHelp" = "Tap if you want to speak";
|
||||
"Invitation.JoinVoiceChat" = "Join Voice Chat";
|
||||
|
@ -183,6 +183,7 @@ public enum ResolvedUrl {
|
||||
case wallet(address: String, amount: Int64?, comment: String?)
|
||||
#endif
|
||||
case settings(ResolvedUrlSettingsSection)
|
||||
case joinVoiceChat(PeerId, String?)
|
||||
}
|
||||
|
||||
public enum NavigateToChatKeepStack {
|
||||
@ -604,7 +605,7 @@ public protocol SharedAccountContext: class {
|
||||
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set<MessageId>) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set<MessageId>, messages: [MessageId: Message], peers: [PeerId: Peer]) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
func resolveUrl(account: Account, url: String, skipUrlAuth: Bool) -> Signal<ResolvedUrl, NoError>
|
||||
func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?)
|
||||
func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?)
|
||||
func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void)
|
||||
func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void)
|
||||
func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void)
|
||||
@ -730,6 +731,6 @@ public protocol AccountContext: class {
|
||||
func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<MessageId?, NoError>
|
||||
func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex)
|
||||
|
||||
func joinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?, activeCall: CachedChannelData.ActiveCall)
|
||||
func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall)
|
||||
func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void)
|
||||
}
|
||||
|
@ -342,5 +342,5 @@ public protocol PresentationCallManager: class {
|
||||
var currentGroupCallSignal: Signal<PresentationGroupCall?, NoError> { get }
|
||||
|
||||
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
func joinGroupCall(context: AccountContext, peerId: PeerId, joinAsPeerId: PeerId?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
|
||||
func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
|
||||
}
|
||||
|
@ -375,7 +375,7 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let activeCall = activeCall {
|
||||
strongSelf.context.joinGroupCall(peerId: peerId, joinAsPeerId: nil, activeCall: activeCall)
|
||||
strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: nil, activeCall: activeCall)
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
@ -168,6 +168,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
requestMessageActionUrlAuth: nil,
|
||||
joinVoiceChat: nil,
|
||||
present: { c, a in
|
||||
present(c, a)
|
||||
}, dismissInput: {
|
||||
|
@ -1199,6 +1199,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
requestMessageActionUrlAuth: nil,
|
||||
joinVoiceChat: nil,
|
||||
present: { c, a in
|
||||
self?.present(c, a)
|
||||
}, dismissInput: {
|
||||
|
@ -1446,6 +1446,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
requestMessageActionUrlAuth: nil,
|
||||
joinVoiceChat: nil,
|
||||
present: { c, a in
|
||||
presentControllerImpl?(c, a)
|
||||
}, dismissInput: {
|
||||
|
@ -180,7 +180,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle
|
||||
dismissImpl?()
|
||||
|
||||
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peer, navigation in
|
||||
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, present: { controller, arguments in
|
||||
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in
|
||||
pushControllerImpl?(controller)
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
})
|
||||
|
@ -897,7 +897,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
|
||||
let _ = (cachedFaqInstantPage(context: context)
|
||||
|> deliverOnMainQueue).start(next: { resolvedUrl in
|
||||
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peer, navigation in
|
||||
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, present: { controller, arguments in
|
||||
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in
|
||||
present(.push, controller)
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
})
|
||||
|
@ -405,7 +405,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
}
|
||||
strongSelf.joinGroupCall(
|
||||
peerId: groupCallPanelData.peerId,
|
||||
joinAsPeerId: nil,
|
||||
invite: nil,
|
||||
activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title)
|
||||
)
|
||||
})
|
||||
@ -852,7 +852,73 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
})]
|
||||
}
|
||||
|
||||
open func joinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?, activeCall: CachedChannelData.ActiveCall) {
|
||||
self.context.joinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId, activeCall: activeCall)
|
||||
open func joinGroupCall(peerId: PeerId, invite: String?, activeCall: CachedChannelData.ActiveCall) {
|
||||
let context = self.context
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.context.joinGroupCall(peerId: peerId, invite: invite, requestJoinAsPeerId: { completion in
|
||||
let currentAccountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||
|> map { peer in
|
||||
return [FoundPeer(peer: peer, subscribers: nil)]
|
||||
}
|
||||
let cachedData = context.account.postbox.transaction { transaction -> CachedPeerData? in
|
||||
return transaction.getPeerCachedData(peerId: peerId)
|
||||
}
|
||||
|
||||
let _ = (combineLatest(currentAccountPeer, cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: peerId), cachedData)
|
||||
|> map { currentAccountPeer, availablePeers, cachedData -> ([FoundPeer], CachedPeerData?) in
|
||||
var result = currentAccountPeer
|
||||
result.append(contentsOf: availablePeers)
|
||||
return (result, cachedData)
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers, cachedData in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var defaultJoinAsPeerId: PeerId?
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
defaultJoinAsPeerId = cachedData.callJoinPeerId
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
defaultJoinAsPeerId = cachedData.callJoinPeerId
|
||||
}
|
||||
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
completion(peer.peer.id)
|
||||
} else {
|
||||
if let defaultJoinAsPeerId = defaultJoinAsPeerId {
|
||||
completion(defaultJoinAsPeerId)
|
||||
} else {
|
||||
let controller = ActionSheetController(presentationData: presentationData)
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(VoiceChatAccountHeaderActionSheetItem(title: presentationData.strings.VoiceChat_SelectAccount, text: presentationData.strings.VoiceChat_DisplayAsInfo))
|
||||
for peer in peers {
|
||||
var subtitle: String?
|
||||
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
|
||||
subtitle = presentationData.strings.VoiceChat_PersonalAccount
|
||||
} else if let subscribers = peer.subscribers {
|
||||
subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers)
|
||||
}
|
||||
|
||||
items.append(VoiceChatPeerActionSheetItem(context: context, peer: peer.peer, title: peer.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: {
|
||||
dismissAction()
|
||||
completion(peer.peer.id)
|
||||
}))
|
||||
}
|
||||
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
}
|
||||
}
|
||||
})
|
||||
}, activeCall: activeCall)
|
||||
}
|
||||
}
|
||||
|
@ -624,9 +624,15 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
}
|
||||
}
|
||||
|
||||
public func joinGroupCall(context: AccountContext, peerId: PeerId, joinAsPeerId: PeerId?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult {
|
||||
public func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult {
|
||||
let begin: () -> Void = { [weak self] in
|
||||
let _ = self?.startGroupCall(accountContext: context, peerId: peerId, joinAsPeerId: joinAsPeerId, initialCall: initialCall).start()
|
||||
if let requestJoinAsPeerId = requestJoinAsPeerId {
|
||||
requestJoinAsPeerId({ joinAsPeerId in
|
||||
let _ = self?.startGroupCall(accountContext: context, peerId: peerId, invite: invite, joinAsPeerId: joinAsPeerId, initialCall: initialCall).start()
|
||||
})
|
||||
} else {
|
||||
let _ = self?.startGroupCall(accountContext: context, peerId: peerId, invite: invite, joinAsPeerId: nil, initialCall: initialCall).start()
|
||||
}
|
||||
}
|
||||
|
||||
if let currentGroupCall = self.currentGroupCallValue {
|
||||
@ -660,6 +666,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
private func startGroupCall(
|
||||
accountContext: AccountContext,
|
||||
peerId: PeerId,
|
||||
invite: String?,
|
||||
joinAsPeerId: PeerId?,
|
||||
initialCall: CachedChannelData.ActiveCall,
|
||||
internalId: CallSessionInternalId = CallSessionInternalId()
|
||||
@ -711,6 +718,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
initialCall: initialCall,
|
||||
internalId: internalId,
|
||||
peerId: peerId,
|
||||
invite: invite,
|
||||
joinAsPeerId: joinAsPeerId
|
||||
)
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
|
@ -344,6 +344,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private var initialCall: CachedChannelData.ActiveCall?
|
||||
public let internalId: CallSessionInternalId
|
||||
public let peerId: PeerId
|
||||
private let invite: String?
|
||||
private var joinAsPeerId: PeerId
|
||||
|
||||
public private(set) var isVideo: Bool
|
||||
@ -526,6 +527,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
initialCall: CachedChannelData.ActiveCall?,
|
||||
internalId: CallSessionInternalId,
|
||||
peerId: PeerId,
|
||||
invite: String?,
|
||||
joinAsPeerId: PeerId?
|
||||
) {
|
||||
self.account = accountContext.account
|
||||
@ -537,6 +539,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.initialCall = initialCall
|
||||
self.internalId = internalId
|
||||
self.peerId = peerId
|
||||
self.invite = invite
|
||||
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
|
||||
|
||||
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title)
|
||||
@ -1000,7 +1003,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
callId: callInfo.id,
|
||||
accessHash: callInfo.accessHash,
|
||||
preferMuted: true,
|
||||
joinPayload: joinPayload
|
||||
joinPayload: joinPayload,
|
||||
inviteHash: strongSelf.invite
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { joinCallResult in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -197,7 +197,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
||||
let totalHeight = titleSize.height + subtitleSize.height + 1.0
|
||||
|
||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height - totalHeight / 2.0) - 71.0), size: titleSize)
|
||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height - totalHeight / 2.0) - 62.0), size: titleSize)
|
||||
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
|
||||
|
||||
self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
@ -23,13 +23,14 @@ import PresentationDataUtils
|
||||
import DirectionalPanGesture
|
||||
import PeerInfoUI
|
||||
import AvatarNode
|
||||
import TooltipUI
|
||||
|
||||
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
|
||||
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
|
||||
private let fullscreenBackgroundColor = UIColor(rgb: 0x000000)
|
||||
private let dimColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
private let sideButtonSize = CGSize(width: 56.0, height: 56.0)
|
||||
private let bottomAreaHeight: CGFloat = 175.0
|
||||
private let bottomAreaHeight: CGFloat = 200.0
|
||||
|
||||
private func cornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
|
||||
if !top && !bottom {
|
||||
@ -63,6 +64,15 @@ private final class VoiceChatControllerTitleNode: ASDisplayNode {
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let infoNode: ASTextNode
|
||||
fileprivate let recordingIconNode: VoiceChatRecordingIconNode
|
||||
|
||||
public var isRecording: Bool = false {
|
||||
didSet {
|
||||
self.recordingIconNode.isHidden = !self.isRecording
|
||||
}
|
||||
}
|
||||
|
||||
var tapped: (() -> Void)?
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
@ -79,16 +89,29 @@ private final class VoiceChatControllerTitleNode: ASDisplayNode {
|
||||
self.infoNode.truncationMode = .byTruncatingTail
|
||||
self.infoNode.isOpaque = false
|
||||
|
||||
self.recordingIconNode = VoiceChatRecordingIconNode(hasBackground: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.infoNode)
|
||||
self.addSubnode(self.recordingIconNode)
|
||||
}
|
||||
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
|
||||
}
|
||||
|
||||
@objc private func tap() {
|
||||
self.tapped?()
|
||||
}
|
||||
|
||||
func update(size: CGSize, title: String, subtitle: String, transition: ContainedViewLayoutTransition) {
|
||||
var titleUpdated = false
|
||||
if let previousTitle = self.titleNode.attributedText?.string {
|
||||
@ -116,8 +139,13 @@ private final class VoiceChatControllerTitleNode: ASDisplayNode {
|
||||
|
||||
let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing
|
||||
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
self.titleNode.frame = titleFrame
|
||||
self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize)
|
||||
|
||||
let iconSide = 16.0 + (1.0 + UIScreenPixel) * 2.0
|
||||
let iconSize: CGSize = CGSize(width: iconSide, height: iconSide)
|
||||
self.recordingIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 1.0, y: titleFrame.minY + 1.0), size: iconSize)
|
||||
}
|
||||
}
|
||||
|
||||
@ -733,7 +761,6 @@ public final class VoiceChatController: ViewController {
|
||||
self.closeButton.setContent(.image(closeButtonImage(dark: false)))
|
||||
|
||||
self.titleNode = VoiceChatControllerTitleNode(theme: self.presentationData.theme)
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.topCornersNode = ASImageNode()
|
||||
self.topCornersNode.displaysAsynchronously = false
|
||||
@ -792,6 +819,10 @@ public final class VoiceChatController: ViewController {
|
||||
let displayAsPeersPromise = Promise<[FoundPeer]>([])
|
||||
displayAsPeersPromise.set(displayAsPeers)
|
||||
|
||||
let inviteLinksPromise = Promise<GroupCallInviteLinks?>(nil)
|
||||
inviteLinksPromise.set(.single(nil)
|
||||
|> then(call.inviteLinks))
|
||||
|
||||
self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in
|
||||
let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
@ -829,10 +860,12 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
let groupPeerId = strongSelf.call.peerId
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
|
||||
let groupPeer = strongSelf.context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(groupPeerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { groupPeer in
|
||||
|
||||
|
||||
let _ = combineLatest(queue: Queue.mainQueue(), groupPeer, inviteLinksPromise.get()).start(next: { groupPeer, inviteLinks in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -840,6 +873,18 @@ public final class VoiceChatController: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
if let groupPeer = groupPeer as? TelegramChannel, let inviteLinks = inviteLinks {
|
||||
var canInvite = true
|
||||
if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) {
|
||||
canInvite = false
|
||||
}
|
||||
|
||||
if !canInvite {
|
||||
strongSelf.presentShare(inviteLinks)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var filters: [ChannelMembersSearchFilter] = []
|
||||
if let (currentCallMembers, _) = strongSelf.currentCallMembers {
|
||||
filters.append(.disable(Array(currentCallMembers.map { $0.peer.id })))
|
||||
@ -1350,23 +1395,23 @@ public final class VoiceChatController: ViewController {
|
||||
})
|
||||
|
||||
|
||||
let title: Signal<String?, NoError> = self.call.state
|
||||
|> map { state -> String? in
|
||||
return state.title
|
||||
let titleAndRecording: Signal<(String?, Bool), NoError> = self.call.state
|
||||
|> map { state -> (String?, Bool) in
|
||||
return (state.title, state.recordingStartTimestamp != nil)
|
||||
}
|
||||
self.peerViewDisposable = combineLatest(queue: Queue.mainQueue(), self.context.account.viewTracker.peerView(self.call.peerId), title).start(next: { [weak self] view, title in
|
||||
self.peerViewDisposable = combineLatest(queue: Queue.mainQueue(), self.context.account.viewTracker.peerView(self.call.peerId), titleAndRecording).start(next: { [weak self] view, titleAndRecording in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let (title, isRecording) = titleAndRecording
|
||||
if let peer = peerViewMainPeer(view) {
|
||||
strongSelf.peer = peer
|
||||
strongSelf.currentTitleIsCustom = title != nil
|
||||
strongSelf.currentTitle = title ?? peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)
|
||||
|
||||
if strongSelf.didSetDataReady {
|
||||
strongSelf.updateTitle(transition: .immediate)
|
||||
}
|
||||
strongSelf.updateTitle(transition: .immediate)
|
||||
strongSelf.titleNode.isRecording = isRecording
|
||||
}
|
||||
if !strongSelf.didSetDataReady {
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? ([], nil), invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set())
|
||||
@ -1445,10 +1490,6 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
self.cameraButtonNode.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
let inviteLinksPromise = Promise<GroupCallInviteLinks?>(nil)
|
||||
inviteLinksPromise.set(.single(nil)
|
||||
|> then(call.inviteLinks))
|
||||
|
||||
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||
self.optionsButton.contextAction = { [weak self] sourceNode, gesture in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
@ -1629,38 +1670,7 @@ public final class VoiceChatController: ViewController {
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let formatSendTitle: (String) -> String = { string in
|
||||
var string = string
|
||||
if string.contains("[") && string.contains("]") {
|
||||
if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") {
|
||||
string.removeSubrange(startIndex ... endIndex)
|
||||
}
|
||||
} else {
|
||||
string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,."))
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
var segmentedValues: [ShareControllerSegmentedValue]?
|
||||
if let speakerLink = inviteLinks.speakerLink {
|
||||
segmentedValues = [ShareControllerSegmentedValue(title: strongSelf.presentationData.strings.VoiceChat_InviteLink_Speaker, subject: .url(speakerLink), actionTitle: strongSelf.presentationData.strings.VoiceChat_InviteLink_CopySpeakerLink, formatSendTitle: { count in
|
||||
return formatSendTitle(strongSelf.presentationData.strings.VoiceChat_InviteLink_InviteSpeakers(Int32(count)))
|
||||
}), ShareControllerSegmentedValue(title: strongSelf.presentationData.strings.VoiceChat_InviteLink_Listener, subject: .url(inviteLinks.listenerLink), actionTitle: strongSelf.presentationData.strings.VoiceChat_InviteLink_CopyListenerLink, formatSendTitle: { count in
|
||||
return formatSendTitle(strongSelf.presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count)))
|
||||
})]
|
||||
}
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forcedTheme: strongSelf.darkTheme, forcedActionTitle: strongSelf.presentationData.strings.VoiceChat_InviteLink_CopyListenerLink)
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
}
|
||||
strongSelf.controller?.present(shareController, in: .window(.root))
|
||||
self?.presentShare(inviteLinks)
|
||||
})))
|
||||
}
|
||||
|
||||
@ -1858,6 +1868,15 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}))
|
||||
|
||||
self.titleNode.tapped = { [weak self] in
|
||||
if let strongSelf = self, !strongSelf.titleNode.recordingIconNode.isHidden {
|
||||
let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil)
|
||||
strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
|
||||
return .dismiss(consume: false)
|
||||
}), in: .window(.root))
|
||||
}
|
||||
}
|
||||
|
||||
//self.isFullscreen = true
|
||||
//self.isExpanded = true
|
||||
}
|
||||
@ -1937,6 +1956,37 @@ public final class VoiceChatController: ViewController {
|
||||
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: action), in: .current)
|
||||
}
|
||||
|
||||
private func presentShare(_ inviteLinks: GroupCallInviteLinks) {
|
||||
let formatSendTitle: (String) -> String = { string in
|
||||
var string = string
|
||||
if string.contains("[") && string.contains("]") {
|
||||
if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") {
|
||||
string.removeSubrange(startIndex ... endIndex)
|
||||
}
|
||||
} else {
|
||||
string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,."))
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
var segmentedValues: [ShareControllerSegmentedValue]?
|
||||
if let speakerLink = inviteLinks.speakerLink {
|
||||
segmentedValues = [ShareControllerSegmentedValue(title: self.presentationData.strings.VoiceChat_InviteLink_Speaker, subject: .url(speakerLink), actionTitle: self.presentationData.strings.VoiceChat_InviteLink_CopySpeakerLink, formatSendTitle: { count in
|
||||
return formatSendTitle(self.presentationData.strings.VoiceChat_InviteLink_InviteSpeakers(Int32(count)))
|
||||
}), ShareControllerSegmentedValue(title: self.presentationData.strings.VoiceChat_InviteLink_Listener, subject: .url(inviteLinks.listenerLink), actionTitle: self.presentationData.strings.VoiceChat_InviteLink_CopyListenerLink, formatSendTitle: { count in
|
||||
return formatSendTitle(self.presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count)))
|
||||
})]
|
||||
}
|
||||
let shareController = ShareController(context: self.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forcedTheme: self.darkTheme, forcedActionTitle: self.presentationData.strings.VoiceChat_InviteLink_CopyListenerLink)
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
}
|
||||
self.controller?.present(shareController, in: .window(.root))
|
||||
}
|
||||
|
||||
private var pressTimer: SwiftSignalKit.Timer?
|
||||
private func startPressTimer() {
|
||||
self.pressTimer?.invalidate()
|
||||
|
642
submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift
Normal file
642
submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift
Normal file
@ -0,0 +1,642 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import PeerInfoUI
|
||||
import ShareController
|
||||
import AvatarNode
|
||||
|
||||
public final class VoiceChatJoinScreen: ViewController {
|
||||
private var controllerNode: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
private var animatedIn = false
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let invite: String?
|
||||
private var join: (CachedChannelData.ActiveCall) -> Void
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId, invite: String?, join: @escaping (CachedChannelData.ActiveCall) -> Void) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.invite = invite
|
||||
self.join = join
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, requestLayout: { [weak self] transition in
|
||||
self?.requestLayout(transition: transition)
|
||||
})
|
||||
self.controllerNode.dismiss = { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
self.controllerNode.cancel = { [weak self] in
|
||||
self?.dismiss()
|
||||
}
|
||||
self.controllerNode.join = { [weak self] call in
|
||||
self?.dismiss()
|
||||
self?.join(call)
|
||||
}
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
let context = self.context
|
||||
let peerId = self.peerId
|
||||
let signal = updatedCurrentPeerGroupCall(account: context.account, peerId: peerId)
|
||||
|> castError(GetCurrentGroupCallError.self)
|
||||
|> mapToSignal { call -> Signal<(Peer, GroupCallSummary)?, GetCurrentGroupCallError> in
|
||||
if let call = call {
|
||||
let peer = context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> castError(GetCurrentGroupCallError.self)
|
||||
return combineLatest(peer, getCurrentGroupCall(account: context.account, callId: call.id, accessHash: call.accessHash))
|
||||
|> map { peer, call -> (Peer, GroupCallSummary)? in
|
||||
if let peer = peer, let call = call {
|
||||
return (peer, call)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|
||||
self.disposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerAndCall in
|
||||
if let strongSelf = self {
|
||||
if let (peer, call) = peerAndCall {
|
||||
strongSelf.controllerNode.setPeer(call: CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title), peer: peer, title: call.info.title, memberCount: call.info.participantCount)
|
||||
} else {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.InviteLinks_InviteLinkExpired, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.ready.set(self.controllerNode.ready.get())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
class Node: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private var call: CachedChannelData.ActiveCall?
|
||||
|
||||
private let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)?
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
|
||||
private let wrappingScrollNode: ASScrollNode
|
||||
private let cancelButtonNode: ASButtonNode
|
||||
|
||||
private let contentContainerNode: ASDisplayNode
|
||||
private let contentBackgroundNode: ASImageNode
|
||||
|
||||
private var contentNode: (ASDisplayNode & ShareContentContainerNode)?
|
||||
private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)?
|
||||
private var animateContentNodeOffsetFromBackgroundOffset: CGFloat?
|
||||
|
||||
private let actionsBackgroundNode: ASImageNode
|
||||
private let actionButtonNode: ShareActionButtonNode
|
||||
private let actionSeparatorNode: ASDisplayNode
|
||||
|
||||
var dismiss: (() -> Void)?
|
||||
var cancel: (() -> Void)?
|
||||
var join: ((CachedChannelData.ActiveCall) -> Void)?
|
||||
|
||||
let ready = Promise<Bool>()
|
||||
private var didSetReady = false
|
||||
|
||||
private var scheduledLayoutTransitionRequestId: Int = 0
|
||||
private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)?
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.requestLayout = requestLayout
|
||||
|
||||
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
|
||||
let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)
|
||||
|
||||
let theme = self.presentationData.theme
|
||||
let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0)))
|
||||
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1)
|
||||
|
||||
let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0)))
|
||||
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1)
|
||||
|
||||
self.wrappingScrollNode = ASScrollNode()
|
||||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
self.cancelButtonNode = ASButtonNode()
|
||||
self.cancelButtonNode.displaysAsynchronously = false
|
||||
self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal)
|
||||
self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted)
|
||||
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
self.contentContainerNode.isOpaque = false
|
||||
self.contentContainerNode.clipsToBounds = true
|
||||
|
||||
self.contentBackgroundNode = ASImageNode()
|
||||
self.contentBackgroundNode.displaysAsynchronously = false
|
||||
self.contentBackgroundNode.displayWithoutProcessing = true
|
||||
self.contentBackgroundNode.image = roundedBackground
|
||||
|
||||
self.actionsBackgroundNode = ASImageNode()
|
||||
self.actionsBackgroundNode.isLayerBacked = true
|
||||
self.actionsBackgroundNode.displayWithoutProcessing = true
|
||||
self.actionsBackgroundNode.displaysAsynchronously = false
|
||||
self.actionsBackgroundNode.image = halfRoundedBackground
|
||||
|
||||
self.actionButtonNode = ShareActionButtonNode(badgeBackgroundColor: self.presentationData.theme.actionSheet.controlAccentColor, badgeTextColor: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
|
||||
self.actionButtonNode.displaysAsynchronously = false
|
||||
self.actionButtonNode.titleNode.displaysAsynchronously = false
|
||||
self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted)
|
||||
|
||||
self.actionSeparatorNode = ASDisplayNode()
|
||||
self.actionSeparatorNode.isLayerBacked = true
|
||||
self.actionSeparatorNode.displaysAsynchronously = false
|
||||
self.actionSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
|
||||
|
||||
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.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.cancelButtonNode)
|
||||
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.actionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.contentBackgroundNode)
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
|
||||
self.contentContainerNode.addSubnode(self.actionSeparatorNode)
|
||||
self.contentContainerNode.addSubnode(self.actionsBackgroundNode)
|
||||
self.contentContainerNode.addSubnode(self.actionButtonNode)
|
||||
|
||||
self.transitionToContentNode(ShareLoadingContainerNode(theme: theme, forceNativeAppearance: false))
|
||||
|
||||
self.actionButtonNode.alpha = 0.0
|
||||
self.actionSeparatorNode.alpha = 0.0
|
||||
self.actionsBackgroundNode.alpha = 0.0
|
||||
|
||||
self.ready.set(.single(true))
|
||||
self.didSetReady = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
func transitionToContentNode(_ contentNode: (ASDisplayNode & ShareContentContainerNode)?, fastOut: Bool = false) {
|
||||
if self.contentNode !== contentNode {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
|
||||
let previous = self.contentNode
|
||||
if let previous = previous {
|
||||
previous.setContentOffsetUpdated(nil)
|
||||
transition = .animated(duration: 0.4, curve: .spring)
|
||||
|
||||
self.previousContentNode = previous
|
||||
previous.alpha = 0.0
|
||||
previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: fastOut ? 0.1 : 0.2, removeOnCompletion: true, completion: { [weak self, weak previous] _ in
|
||||
if let strongSelf = self, let previous = previous {
|
||||
if strongSelf.previousContentNode === previous {
|
||||
strongSelf.previousContentNode = nil
|
||||
}
|
||||
previous.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
self.contentNode = contentNode
|
||||
|
||||
if let (layout, navigationBarHeight, bottomGridInset) = self.containerLayout {
|
||||
if let contentNode = contentNode, let previous = previous {
|
||||
contentNode.frame = previous.frame
|
||||
contentNode.updateLayout(size: previous.bounds.size, bottomInset: bottomGridInset, transition: .immediate)
|
||||
|
||||
contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in
|
||||
self?.contentNodeOffsetUpdated(contentOffset, transition: transition)
|
||||
})
|
||||
self.contentContainerNode.insertSubnode(contentNode, at: 0)
|
||||
|
||||
contentNode.alpha = 1.0
|
||||
let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.35)
|
||||
animation.fillMode = .both
|
||||
if !fastOut {
|
||||
animation.beginTime = CACurrentMediaTime() + 0.1
|
||||
}
|
||||
contentNode.layer.add(animation, forKey: "opacity")
|
||||
|
||||
self.animateContentNodeOffsetFromBackgroundOffset = self.contentBackgroundNode.frame.minY
|
||||
self.scheduleInteractiveTransition(transition)
|
||||
|
||||
contentNode.activate()
|
||||
previous.deactivate()
|
||||
} else {
|
||||
if let contentNode = self.contentNode {
|
||||
contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in
|
||||
self?.contentNodeOffsetUpdated(contentOffset, transition: transition)
|
||||
})
|
||||
self.contentContainerNode.insertSubnode(contentNode, at: 0)
|
||||
}
|
||||
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
}
|
||||
} else if let contentNode = contentNode {
|
||||
contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in
|
||||
self?.contentNodeOffsetUpdated(contentOffset, transition: transition)
|
||||
})
|
||||
self.contentContainerNode.insertSubnode(contentNode, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
var insets = layout.insets(options: [.statusBar, .input])
|
||||
let cleanInsets = layout.insets(options: [.statusBar])
|
||||
insets.top = max(10.0, insets.top)
|
||||
|
||||
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
|
||||
if insets.bottom > 0 {
|
||||
bottomInset -= 12.0
|
||||
}
|
||||
let buttonHeight: CGFloat = 57.0
|
||||
let sectionSpacing: CGFloat = 8.0
|
||||
let titleAreaHeight: CGFloat = 64.0
|
||||
|
||||
let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing
|
||||
|
||||
let width = min(layout.size.width, layout.size.height) - 20.0
|
||||
let sideInset = floor((layout.size.width - width) / 2.0)
|
||||
|
||||
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
|
||||
let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0)
|
||||
|
||||
let bottomGridInset = buttonHeight
|
||||
|
||||
self.containerLayout = (layout, navigationBarHeight, bottomGridInset)
|
||||
self.scheduledLayoutTransitionRequest = nil
|
||||
|
||||
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight)))
|
||||
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
||||
|
||||
transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset)))
|
||||
|
||||
transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight)))
|
||||
|
||||
transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
|
||||
|
||||
let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight))
|
||||
|
||||
if let contentNode = self.contentNode {
|
||||
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize))
|
||||
contentNode.updateLayout(size: gridSize, bottomInset: bottomGridInset, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if let (layout, _, _) = self.containerLayout {
|
||||
var insets = layout.insets(options: [.statusBar, .input])
|
||||
insets.top = max(10.0, insets.top)
|
||||
let cleanInsets = layout.insets(options: [.statusBar])
|
||||
|
||||
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
|
||||
if insets.bottom > 0 {
|
||||
bottomInset -= 12.0
|
||||
}
|
||||
let buttonHeight: CGFloat = 57.0
|
||||
let sectionSpacing: CGFloat = 8.0
|
||||
|
||||
let width = min(layout.size.width, layout.size.height) - 20.0
|
||||
|
||||
let sideInset = floor((layout.size.width - width) / 2.0)
|
||||
|
||||
let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing
|
||||
let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
|
||||
|
||||
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - contentOffset), size: contentFrame.size)
|
||||
if backgroundFrame.minY < contentFrame.minY {
|
||||
backgroundFrame.origin.y = contentFrame.minY
|
||||
}
|
||||
if backgroundFrame.maxY > contentFrame.maxY {
|
||||
backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY
|
||||
}
|
||||
if backgroundFrame.size.height < buttonHeight + 32.0 {
|
||||
backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height
|
||||
backgroundFrame.size.height = buttonHeight + 32.0
|
||||
}
|
||||
transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame)
|
||||
|
||||
if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset {
|
||||
self.animateContentNodeOffsetFromBackgroundOffset = nil
|
||||
let offset = backgroundFrame.minY - animateContentNodeOffsetFromBackgroundOffset
|
||||
if let contentNode = self.contentNode {
|
||||
transition.animatePositionAdditive(node: contentNode, offset: CGPoint(x: 0.0, y: -offset))
|
||||
}
|
||||
if let previousContentNode = self.previousContentNode {
|
||||
transition.updatePosition(node: previousContentNode, position: previousContentNode.position.offsetBy(dx: 0.0, dy: offset))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.cancelButtonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func cancelButtonPressed() {
|
||||
self.cancel?()
|
||||
}
|
||||
|
||||
@objc func installActionButtonPressed() {
|
||||
if let call = self.call {
|
||||
self.join?(call)
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if self.contentNode != nil {
|
||||
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)
|
||||
self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)? = nil) {
|
||||
if self.contentNode != 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()
|
||||
})
|
||||
} else {
|
||||
self.dismiss?()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) {
|
||||
return result
|
||||
}
|
||||
if self.bounds.contains(point) {
|
||||
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) {
|
||||
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.cancelButtonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleInteractiveTransition(_ transition: ContainedViewLayoutTransition) {
|
||||
if let scheduledLayoutTransitionRequest = self.scheduledLayoutTransitionRequest {
|
||||
switch scheduledLayoutTransitionRequest.1 {
|
||||
case .immediate:
|
||||
self.scheduleLayoutTransitionRequest(transition)
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
self.scheduleLayoutTransitionRequest(transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) {
|
||||
let requestId = self.scheduledLayoutTransitionRequestId
|
||||
self.scheduledLayoutTransitionRequestId += 1
|
||||
self.scheduledLayoutTransitionRequest = (requestId, transition)
|
||||
(self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId {
|
||||
strongSelf.scheduledLayoutTransitionRequest = nil
|
||||
strongSelf.requestLayout(currentRequestTransition)
|
||||
}
|
||||
}
|
||||
})
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
func transitionToProgress(signal: Signal<Void, NoError>) {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0)
|
||||
|
||||
self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false), fastOut: true)
|
||||
let timestamp = CACurrentMediaTime()
|
||||
self.disposable.set(signal.start(completed: { [weak self] in
|
||||
let minDelay = 0.6
|
||||
let delay = max(0.0, (timestamp + minDelay) - CACurrentMediaTime())
|
||||
Queue.mainQueue().after(delay, {
|
||||
if let strongSelf = self {
|
||||
strongSelf.cancel?()
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func setPeer(call: CachedChannelData.ActiveCall, peer: Peer, title: String?, memberCount: Int) {
|
||||
self.call = call
|
||||
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.actionButtonNode, alpha: 1.0)
|
||||
transition.updateAlpha(node: self.actionSeparatorNode, alpha: 1.0)
|
||||
transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 1.0)
|
||||
|
||||
self.actionButtonNode.isEnabled = true
|
||||
self.actionButtonNode.setTitle(self.presentationData.strings.Invitation_JoinVoiceChat, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
|
||||
|
||||
self.transitionToContentNode(VoiceChatPreviewContentNode(context: self.context, peer: peer, title: title, memberCount: memberCount, theme: self.presentationData.theme, strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 26.0)
|
||||
|
||||
final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNode {
|
||||
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
private let avatarNode: AvatarNode
|
||||
private let titleNode: ASTextNode
|
||||
private let countNode: ASTextNode
|
||||
|
||||
init(context: AccountContext, peer: Peer, title: String?, memberCount: Int, theme: PresentationTheme, strings: PresentationStrings, displayOrder: PresentationPersonNameOrder) {
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.titleNode = ASTextNode()
|
||||
self.countNode = ASTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.avatarNode.setPeer(context: context, theme: theme, peer: peer, emptyColor: theme.list.mediaPlaceholderColor)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.titleNode.attributedText = NSAttributedString(string: title ?? peer.displayTitle(strings: strings, displayOrder: displayOrder), font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor)
|
||||
|
||||
self.addSubnode(self.countNode)
|
||||
|
||||
self.countNode.isHidden = memberCount == 0
|
||||
self.countNode.attributedText = NSAttributedString(string: memberCount == 0 ? "" : strings.VoiceChat_Panel_Members(Int32(memberCount)), font: Font.regular(16.0), textColor: theme.actionSheet.secondaryTextColor)
|
||||
}
|
||||
|
||||
func activate() {
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
}
|
||||
|
||||
func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) {
|
||||
}
|
||||
|
||||
func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) {
|
||||
self.contentOffsetUpdated = f
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let nodeHeight: CGFloat = self.countNode.isHidden ? 204.0 : 224.0
|
||||
|
||||
let verticalOrigin = size.height - nodeHeight
|
||||
|
||||
let avatarSize: CGFloat = 75.0
|
||||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floor((size.width - avatarSize) / 2.0), y: verticalOrigin + 22.0), size: CGSize(width: avatarSize, height: avatarSize)))
|
||||
|
||||
let titleSize = self.titleNode.measure(size)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + 22.0 + avatarSize + 15.0), size: titleSize))
|
||||
|
||||
let countSize = self.countNode.measure(size)
|
||||
transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 22.0 + avatarSize + 15.0 + titleSize.height + 1.0), size: countSize))
|
||||
|
||||
self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition)
|
||||
}
|
||||
|
||||
func updateSelectedPeers() {
|
||||
}
|
||||
}
|
@ -169,13 +169,13 @@ public final class VoiceChatOverlayController: ViewController {
|
||||
|
||||
if reclaim {
|
||||
self.dismissed = true
|
||||
let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 175.0 / 2.0)
|
||||
let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 200.0 / 2.0)
|
||||
if self.isSlidOffscreen {
|
||||
self.isSlidOffscreen = false
|
||||
self.isButtonHidden = true
|
||||
actionButton.layer.sublayerTransform = CATransform3DIdentity
|
||||
actionButton.update(snap: false, animated: false)
|
||||
actionButton.position = CGPoint(x: targetPosition.x, y: 175.0 / 2.0)
|
||||
actionButton.position = CGPoint(x: targetPosition.x, y: 200.0 / 2.0)
|
||||
|
||||
leftButton.isHidden = false
|
||||
rightButton.isHidden = false
|
||||
@ -191,7 +191,7 @@ public final class VoiceChatOverlayController: ViewController {
|
||||
actionButton.layer.removeAllAnimations()
|
||||
actionButton.layer.sublayerTransform = CATransform3DIdentity
|
||||
actionButton.update(snap: false, animated: false)
|
||||
actionButton.position = CGPoint(x: targetPosition.x, y: 175.0 / 2.0)
|
||||
actionButton.position = CGPoint(x: targetPosition.x, y: 200.0 / 2.0)
|
||||
|
||||
leftButton.isHidden = false
|
||||
rightButton.isHidden = false
|
||||
|
@ -36,31 +36,43 @@ final class VoiceChatRecordingContextItem: ContextMenuCustomItem {
|
||||
|
||||
private let textFont = Font.regular(17.0)
|
||||
|
||||
private class IconNode: ASDisplayNode {
|
||||
class VoiceChatRecordingIconNode: ASDisplayNode {
|
||||
private let backgroundNode: ASImageNode
|
||||
private let dotNode: ASImageNode
|
||||
|
||||
override init() {
|
||||
init(hasBackground: Bool) {
|
||||
let iconSize = 16.0 + (1.0 + UIScreenPixel) * 2.0
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateCircleImage(diameter: iconSize, lineWidth: 1.0 + UIScreenPixel, color: UIColor.white, backgroundColor: nil)
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.dotNode = ASImageNode()
|
||||
self.dotNode.displaysAsynchronously = false
|
||||
self.dotNode.displayWithoutProcessing = true
|
||||
self.dotNode.image = generateFilledCircleImage(diameter: 8.0, color: UIColor(rgb: 0xff3b30))
|
||||
self.dotNode.isLayerBacked = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.isLayerBacked = true
|
||||
|
||||
if hasBackground {
|
||||
self.addSubnode(self.backgroundNode)
|
||||
}
|
||||
self.addSubnode(self.dotNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
override func didEnterHierarchy() {
|
||||
self.setupAnimation()
|
||||
}
|
||||
|
||||
override func didExitHierarchy() {
|
||||
self.dotNode.layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
private func setupAnimation() {
|
||||
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
||||
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber]
|
||||
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
|
||||
@ -89,7 +101,7 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let statusNode: ImmediateTextNode
|
||||
private let iconNode: IconNode
|
||||
private let iconNode: VoiceChatRecordingIconNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
@ -132,7 +144,7 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen
|
||||
self.buttonNode.isAccessibilityElement = true
|
||||
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
|
||||
|
||||
self.iconNode = IconNode()
|
||||
self.iconNode = VoiceChatRecordingIconNode(hasBackground: true)
|
||||
|
||||
super.init()
|
||||
|
||||
|
@ -15,6 +15,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
private let backgroundNode: ASImageNode
|
||||
private let textInputNode: EditableTextNode
|
||||
private let placeholderNode: ASTextNode
|
||||
private let clearButton: HighlightableButtonNode
|
||||
|
||||
var updateHeight: (() -> Void)?
|
||||
var complete: (() -> Void)?
|
||||
@ -30,6 +31,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
set {
|
||||
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
|
||||
self.placeholderNode.isHidden = !newValue.isEmpty
|
||||
self.clearButton.isHidden = newValue.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +67,13 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
self.placeholderNode.displaysAsynchronously = false
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
|
||||
self.clearButton = HighlightableButtonNode()
|
||||
self.clearButton.imageNode.displaysAsynchronously = false
|
||||
self.clearButton.imageNode.displayWithoutProcessing = true
|
||||
self.clearButton.displaysAsynchronously = false
|
||||
self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: [])
|
||||
self.clearButton.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.textInputNode.delegate = self
|
||||
@ -72,6 +81,9 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textInputNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
self.addSubnode(self.clearButton)
|
||||
|
||||
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
@ -81,6 +93,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
|
||||
self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: [])
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
@ -96,7 +109,11 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||
|
||||
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
|
||||
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height)))
|
||||
|
||||
if let image = self.clearButton.image(for: []) {
|
||||
transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
@ -113,6 +130,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
self.updateTextNodeText(animated: true)
|
||||
self.textChanged?(editableTextNode.textView.text)
|
||||
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
|
||||
self.clearButton.isHidden = !self.placeholderNode.isHidden
|
||||
}
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
@ -127,7 +145,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||
|
||||
return min(61.0, max(33.0, unboundTextFieldHeight))
|
||||
}
|
||||
|
@ -556,6 +556,16 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction -> JoinGroupCallResult in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||
} else {
|
||||
return cachedData
|
||||
}
|
||||
})
|
||||
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -300,8 +300,8 @@ public final class AccountContextImpl: AccountContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func joinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?, activeCall: CachedChannelData.ActiveCall) {
|
||||
let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, joinAsPeerId: joinAsPeerId, initialCall: activeCall, endCurrentIfAny: false)
|
||||
public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall) {
|
||||
let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
self.sharedContext.navigateToCurrentCall()
|
||||
@ -323,14 +323,14 @@ public final class AccountContextImpl: AccountContext {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.sharedContext.callManager?.joinGroupCall(context: strongSelf, peerId: peer.id, joinAsPeerId: joinAsPeerId, initialCall: activeCall, endCurrentIfAny: true)
|
||||
let _ = strongSelf.sharedContext.callManager?.joinGroupCall(context: strongSelf, peerId: peer.id, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: true)
|
||||
})]), on: .root)
|
||||
} else {
|
||||
strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressVoiceChatMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.sharedContext.callManager?.joinGroupCall(context: strongSelf, peerId: peer.id, joinAsPeerId: joinAsPeerId, initialCall: activeCall, endCurrentIfAny: true)
|
||||
let _ = strongSelf.sharedContext.callManager?.joinGroupCall(context: strongSelf, peerId: peer.id, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: true)
|
||||
})]), on: .root)
|
||||
}
|
||||
} else {
|
||||
|
@ -534,7 +534,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
case .groupPhoneCall, .inviteToGroupPhoneCall:
|
||||
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
|
||||
strongSelf.joinGroupCall(peerId: message.id.peerId, joinAsPeerId: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title))
|
||||
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title))
|
||||
} else {
|
||||
var canManageGroupCalls = false
|
||||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel {
|
||||
@ -568,7 +568,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.joinGroupCall(peerId: message.id.peerId, joinAsPeerId: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title))
|
||||
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title))
|
||||
}, error: { [weak self] error in
|
||||
dismissStatus?()
|
||||
|
||||
@ -6403,7 +6403,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
strongSelf.joinGroupCall(peerId: peer.id, joinAsPeerId: nil, activeCall: activeCall)
|
||||
strongSelf.joinGroupCall(peerId: peer.id, invite: nil, activeCall: activeCall)
|
||||
}, presentInviteMembers: { [weak self] in
|
||||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
@ -11101,6 +11101,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if case let .url(url) = subject {
|
||||
self?.controllerInteraction?.requestMessageActionUrlAuth(url, subject)
|
||||
}
|
||||
}, joinVoiceChat: { [weak self] peerId, invite, call in
|
||||
self?.joinGroupCall(peerId: peerId, invite: invite, activeCall: call)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, dismissInput: { [weak self] in
|
||||
@ -11851,58 +11853,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return inputShortcuts + otherShortcuts
|
||||
}
|
||||
|
||||
public override func joinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?, activeCall: CachedChannelData.ActiveCall) {
|
||||
let context = self.context
|
||||
let presentationData = self.presentationData
|
||||
|
||||
let proceed: (PeerId) -> Void = { joinAsPeerId in
|
||||
super.joinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId, activeCall: activeCall)
|
||||
public override func joinGroupCall(peerId: PeerId, invite: String?, activeCall: CachedChannelData.ActiveCall) {
|
||||
let proceed = {
|
||||
super.joinGroupCall(peerId: peerId, invite: invite, activeCall: activeCall)
|
||||
}
|
||||
|
||||
let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in
|
||||
let currentAccountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||
|> map { peer in
|
||||
return [FoundPeer(peer: peer, subscribers: nil)]
|
||||
}
|
||||
|
||||
let _ = (combineLatest(currentAccountPeer, cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: peerId))
|
||||
|> map { currentAccountPeer, availablePeers -> [FoundPeer] in
|
||||
var result = currentAccountPeer
|
||||
result.append(contentsOf: availablePeers)
|
||||
return result
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let controller = ActionSheetController(presentationData: presentationData)
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(VoiceChatAccountHeaderActionSheetItem(title: presentationData.strings.VoiceChat_SelectAccount, text: presentationData.strings.VoiceChat_DisplayAsInfo))
|
||||
for peer in peers {
|
||||
var subtitle: String?
|
||||
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
|
||||
subtitle = presentationData.strings.VoiceChat_PersonalAccount
|
||||
} else if let subscribers = peer.subscribers {
|
||||
subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers)
|
||||
}
|
||||
|
||||
items.append(VoiceChatPeerActionSheetItem(context: context, peer: peer.peer, title: peer.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: {
|
||||
dismissAction()
|
||||
proceed(peer.peer.id)
|
||||
}))
|
||||
}
|
||||
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
})
|
||||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||||
proceed()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ import LanguageLinkPreviewUI
|
||||
import PeerInfoUI
|
||||
import InviteLinksUI
|
||||
import UndoUI
|
||||
import TelegramCallsUI
|
||||
|
||||
private final class ChatRecentActionsListOpaqueState {
|
||||
let entries: [ChatRecentActionsEntry]
|
||||
@ -893,6 +894,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
requestMessageActionUrlAuth: nil,
|
||||
joinVoiceChat: nil,
|
||||
present: { c, a in
|
||||
self?.presentController(c, a)
|
||||
}, dismissInput: {
|
||||
@ -908,6 +910,10 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
#endif
|
||||
case .settings:
|
||||
break
|
||||
case let .joinVoiceChat(peerId, invite):
|
||||
strongSelf.presentController(VoiceChatJoinScreen(context: strongSelf.context, peerId: peerId, invite: invite, join: { call in
|
||||
|
||||
}), nil)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -21,6 +21,7 @@ import SettingsUI
|
||||
import UrlHandling
|
||||
import ShareController
|
||||
import ChatInterfaceState
|
||||
import TelegramCallsUI
|
||||
|
||||
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
|
||||
if case .default = navigation {
|
||||
@ -38,7 +39,7 @@ private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatContr
|
||||
}
|
||||
}
|
||||
|
||||
func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)? = nil, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) {
|
||||
func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)? = nil, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
switch resolvedUrl {
|
||||
case let .externalUrl(url):
|
||||
@ -462,5 +463,10 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
}
|
||||
break
|
||||
}
|
||||
case let .joinVoiceChat(peerId, invite):
|
||||
dismissInput()
|
||||
present(VoiceChatJoinScreen(context: context, peerId: peerId, invite: invite, join: { call in
|
||||
joinVoiceChat?(peerId, invite, call)
|
||||
}), nil)
|
||||
}
|
||||
}
|
||||
|
@ -214,7 +214,9 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
requestMessageActionUrlAuth: nil,
|
||||
present: { c, a in
|
||||
joinVoiceChat: { peerId, invite, call in
|
||||
|
||||
}, present: { c, a in
|
||||
context.sharedContext.applicationBindings.dismissNativeController()
|
||||
|
||||
c.presentationArguments = a
|
||||
|
@ -3084,7 +3084,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}, sendFile: nil,
|
||||
sendSticker: { [weak self] f, sourceNode, sourceRect in
|
||||
return false
|
||||
}, requestMessageActionUrlAuth: nil, present: { [weak self] c, a in
|
||||
}, requestMessageActionUrlAuth: nil,
|
||||
joinVoiceChat: { peerId, invite, call in
|
||||
|
||||
}, present: { [weak self] c, a in
|
||||
self?.controller?.present(c, in: .window(.root), with: a)
|
||||
}, dismissInput: { [weak self] in
|
||||
self?.view.endEditing(true)
|
||||
@ -3106,6 +3109,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
requestMessageActionUrlAuth: nil,
|
||||
joinVoiceChat: { peerId, invite, call in
|
||||
|
||||
},
|
||||
present: { c, a in
|
||||
self?.controller?.present(c, in: .window(.root), with: a)
|
||||
}, dismissInput: {
|
||||
@ -3308,7 +3314,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
case .videoCall:
|
||||
self.requestCall(isVideo: true)
|
||||
case .voiceChat:
|
||||
self.openVoiceChatDisplayAsPeerSelection(gesture: gesture, contextController: nil, result: nil, backAction: nil)
|
||||
self.requestCall(isVideo: false)
|
||||
case .mute:
|
||||
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
|
||||
let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start()
|
||||
@ -3529,7 +3535,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, f in
|
||||
self?.openVoiceChatDisplayAsPeerSelection(gesture: nil, contextController: c, result: f, backAction: { c in
|
||||
self?.requestCall(isVideo: false, contextController: c, result: f, backAction: { c in
|
||||
if let mainItemsImpl = mainItemsImpl {
|
||||
c.setItems(mainItemsImpl())
|
||||
}
|
||||
@ -3639,11 +3645,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, f in
|
||||
self?.openVoiceChatDisplayAsPeerSelection(gesture: nil, contextController: c, result: f, backAction: { c in
|
||||
if let mainItemsImpl = mainItemsImpl {
|
||||
c.setItems(mainItemsImpl())
|
||||
}
|
||||
})
|
||||
self?.requestCall(isVideo: false, contextController: c, result: f)
|
||||
})))
|
||||
}
|
||||
|
||||
@ -3816,28 +3818,37 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
self.controller?.present(shareController, in: .window(.root))
|
||||
}
|
||||
|
||||
private func requestCall(isVideo: Bool, joinAsPeerId: PeerId? = nil) {
|
||||
if let peer = self.data?.peer as? TelegramChannel {
|
||||
guard let cachedChannelData = self.data?.cachedData as? CachedChannelData else {
|
||||
return
|
||||
}
|
||||
|
||||
if let activeCall = cachedChannelData.activeCall {
|
||||
self.context.joinGroupCall(peerId: peer.id, joinAsPeerId: joinAsPeerId, activeCall: activeCall)
|
||||
private func requestCall(isVideo: Bool, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) {
|
||||
let peerId = self.peerId
|
||||
let requestCall: (PeerId?, CachedChannelData.ActiveCall?) -> Void = { [weak self] defaultJoinAsPeerId, activeCall in
|
||||
if let activeCall = activeCall {
|
||||
self?.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { completion in
|
||||
if let defaultJoinAsPeerId = defaultJoinAsPeerId {
|
||||
result?(.dismissWithoutContent)
|
||||
completion(defaultJoinAsPeerId)
|
||||
} else {
|
||||
self?.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
|
||||
completion(joinAsPeerId)
|
||||
}, gesture: gesture, contextController: contextController, result: result, backAction: backAction)
|
||||
}
|
||||
}, activeCall: activeCall)
|
||||
} else {
|
||||
self.createAndJoinGroupCall(peerId: peer.id, joinAsPeerId: joinAsPeerId)
|
||||
if let defaultJoinAsPeerId = defaultJoinAsPeerId {
|
||||
result?(.dismissWithoutContent)
|
||||
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId)
|
||||
} else {
|
||||
self?.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
|
||||
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId)
|
||||
}, gesture: gesture, contextController: contextController, result: result, backAction: backAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let cachedChannelData = self.data?.cachedData as? CachedChannelData {
|
||||
requestCall(cachedChannelData.callJoinPeerId, cachedChannelData.activeCall)
|
||||
return
|
||||
} else if let peer = self.data?.peer as? TelegramGroup {
|
||||
guard let cachedGroupData = self.data?.cachedData as? CachedGroupData else {
|
||||
return
|
||||
}
|
||||
|
||||
if let activeCall = cachedGroupData.activeCall {
|
||||
self.context.joinGroupCall(peerId: peer.id, joinAsPeerId: joinAsPeerId, activeCall: activeCall)
|
||||
} else {
|
||||
self.createAndJoinGroupCall(peerId: peer.id, joinAsPeerId: joinAsPeerId)
|
||||
}
|
||||
} else if let cachedGroupData = self.data?.cachedData as? CachedGroupData {
|
||||
requestCall(cachedGroupData.callJoinPeerId, cachedGroupData.activeCall)
|
||||
return
|
||||
}
|
||||
|
||||
@ -3873,7 +3884,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.context.joinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title))
|
||||
strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in
|
||||
result(joinAsPeerId)
|
||||
}, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title))
|
||||
}, error: { [weak self] error in
|
||||
dismissStatus?()
|
||||
|
||||
@ -4192,7 +4205,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
controller.push(statsController)
|
||||
}
|
||||
|
||||
private func openVoiceChatDisplayAsPeerSelection(gesture: ContextGesture?, contextController: ContextController?, result: ((ContextMenuActionResult) -> Void)?, backAction: ((ContextController) -> Void)?) {
|
||||
private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) {
|
||||
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||
|> map { peer in
|
||||
return [FoundPeer(peer: peer, subscribers: nil)]
|
||||
@ -4206,9 +4219,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if peers.count == 1 {
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
result?(.dismissWithoutContent)
|
||||
strongSelf.requestCall(isVideo: false, joinAsPeerId: nil)
|
||||
completion(peer.peer.id)
|
||||
} else {
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
@ -4229,7 +4242,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
strongSelf.requestCall(isVideo: false, joinAsPeerId: peer.peer.id == strongSelf.context.account.peerId ? nil : peer.peer.id)
|
||||
completion(peer.peer.id)
|
||||
})))
|
||||
|
||||
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
|
||||
@ -4323,6 +4336,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
requestMessageActionUrlAuth: nil,
|
||||
joinVoiceChat: nil,
|
||||
present: { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}, dismissInput: { [weak controller] in
|
||||
@ -5153,7 +5167,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
resolvedUrl = .instantView(webPage, customAnchor)
|
||||
}
|
||||
strongSelf.context.sharedContext.openResolvedUrl(resolvedUrl, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, openPeer: { peer, navigation in
|
||||
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, present: { [weak self] controller, arguments in
|
||||
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak self] controller, arguments in
|
||||
self?.controller?.push(controller)
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
}
|
||||
|
@ -1148,8 +1148,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return resolveUrlImpl(account: account, url: url, skipUrlAuth: skipUrlAuth)
|
||||
}
|
||||
|
||||
public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) {
|
||||
openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, requestMessageActionUrlAuth: requestMessageActionUrlAuth, present: present, dismissInput: dismissInput, contentContext: contentContext)
|
||||
public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) {
|
||||
openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext)
|
||||
}
|
||||
|
||||
public func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController {
|
||||
|
@ -45,6 +45,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
requestMessageActionUrlAuth: nil,
|
||||
joinVoiceChat: nil,
|
||||
present: presentImpl, dismissInput: {}, contentContext: nil)
|
||||
}
|
||||
|
||||
|
@ -297,7 +297,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
case let .point(rect, arrowPosition):
|
||||
let backgroundWidth = textSize.width + contentInset * 2.0 + animationSize.width + animationSpacing
|
||||
switch arrowPosition {
|
||||
case .bottom:
|
||||
case .bottom, .top:
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: rect.midX - backgroundWidth / 2.0, y: rect.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
|
||||
case .right:
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: rect.minX - backgroundWidth - bottomInset, y: rect.midY - backgroundHeight / 2.0), size: CGSize(width: backgroundWidth, height: backgroundHeight))
|
||||
@ -313,6 +313,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
backgroundFrame.origin.y = rect.maxY + bottomInset
|
||||
invertArrow = true
|
||||
}
|
||||
if case .top = arrowPosition, !invertArrow {
|
||||
invertArrow = true
|
||||
backgroundFrame.origin.y = rect.maxY + bottomInset
|
||||
}
|
||||
self.isArrowInverted = invertArrow
|
||||
case .top:
|
||||
let backgroundWidth = containerWidth
|
||||
@ -332,7 +336,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
let arrowFrame: CGRect
|
||||
|
||||
switch arrowPosition {
|
||||
case .bottom:
|
||||
case .bottom, .top:
|
||||
if invertArrow {
|
||||
arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: -arrowSize.height), size: arrowSize)
|
||||
} else {
|
||||
@ -404,7 +408,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
|
||||
let startPoint: CGPoint
|
||||
switch arrowPosition {
|
||||
case .bottom:
|
||||
case .bottom, .top:
|
||||
let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY
|
||||
startPoint = CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)
|
||||
case .right:
|
||||
@ -448,7 +452,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
|
||||
let targetPoint: CGPoint
|
||||
switch arrowPosition {
|
||||
case .bottom:
|
||||
case .bottom, .top:
|
||||
let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY
|
||||
targetPoint = CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)
|
||||
case .right:
|
||||
@ -484,8 +488,9 @@ public final class TooltipScreen: ViewController {
|
||||
}
|
||||
|
||||
public enum ArrowPosition {
|
||||
case bottom
|
||||
case top
|
||||
case right
|
||||
case bottom
|
||||
}
|
||||
|
||||
public enum Location {
|
||||
|
@ -340,7 +340,7 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig
|
||||
return .replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId))
|
||||
}
|
||||
case let .voiceChat(invite):
|
||||
return .single(.peer(peer.id, .default))
|
||||
return .single(.joinVoiceChat(peer.id, invite))
|
||||
}
|
||||
} else {
|
||||
if let peer = peer as? TelegramUser, peer.botInfo == nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user