Add voice chat invite links support

This commit is contained in:
Ilya Laktyushin 2021-03-11 16:29:42 +04:00
parent b9f5db6a5a
commit e2a6a70ea2
31 changed files with 3492 additions and 2684 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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() {
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,6 +45,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate
}, sendFile: nil,
sendSticker: nil,
requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: presentImpl, dismissInput: {}, contentContext: nil)
}

View File

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

View File

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