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.MutedByAdmin" = "Muted by Admin";
"VoiceChat.MutedByAdminHelp" = "Tap if you want to speak"; "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?) case wallet(address: String, amount: Int64?, comment: String?)
#endif #endif
case settings(ResolvedUrlSettingsSection) case settings(ResolvedUrlSettingsSection)
case joinVoiceChat(PeerId, String?)
} }
public enum NavigateToChatKeepStack { 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>) -> Signal<ChatAvailableMessageActions, NoError>
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set<MessageId>, messages: [MessageId: Message], peers: [PeerId: Peer]) -> 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 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 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 openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void)
func presentContactsWarningSuppression(context: AccountContext, present: (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 chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<MessageId?, NoError>
func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex) 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) 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 } var currentGroupCallSignal: Signal<PresentationGroupCall?, NoError> { get }
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult 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 { 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, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: nil, requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: { c, a in present: { c, a in
present(c, a) present(c, a)
}, dismissInput: { }, dismissInput: {

View File

@ -1199,6 +1199,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
}, sendFile: nil, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: nil, requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: { c, a in present: { c, a in
self?.present(c, a) self?.present(c, a)
}, dismissInput: { }, dismissInput: {

View File

@ -1446,6 +1446,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
}, sendFile: nil, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: nil, requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: { c, a in present: { c, a in
presentControllerImpl?(c, a) presentControllerImpl?(c, a)
}, dismissInput: { }, dismissInput: {

View File

@ -180,7 +180,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle
dismissImpl?() dismissImpl?()
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peer, navigation 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
pushControllerImpl?(controller) pushControllerImpl?(controller)
}, dismissInput: {}, contentContext: nil) }, dismissInput: {}, contentContext: nil)
}) })

View File

@ -897,7 +897,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
let _ = (cachedFaqInstantPage(context: context) let _ = (cachedFaqInstantPage(context: context)
|> deliverOnMainQueue).start(next: { resolvedUrl in |> deliverOnMainQueue).start(next: { resolvedUrl in
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peer, navigation 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) present(.push, controller)
}, dismissInput: {}, contentContext: nil) }, dismissInput: {}, contentContext: nil)
}) })

View File

@ -405,7 +405,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
} }
strongSelf.joinGroupCall( strongSelf.joinGroupCall(
peerId: groupCallPanelData.peerId, peerId: groupCallPanelData.peerId,
joinAsPeerId: nil, invite: nil,
activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title) 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) { open func joinGroupCall(peerId: PeerId, invite: String?, activeCall: CachedChannelData.ActiveCall) {
self.context.joinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId, activeCall: 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 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 { if let currentGroupCall = self.currentGroupCallValue {
@ -660,6 +666,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
private func startGroupCall( private func startGroupCall(
accountContext: AccountContext, accountContext: AccountContext,
peerId: PeerId, peerId: PeerId,
invite: String?,
joinAsPeerId: PeerId?, joinAsPeerId: PeerId?,
initialCall: CachedChannelData.ActiveCall, initialCall: CachedChannelData.ActiveCall,
internalId: CallSessionInternalId = CallSessionInternalId() internalId: CallSessionInternalId = CallSessionInternalId()
@ -711,6 +718,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
initialCall: initialCall, initialCall: initialCall,
internalId: internalId, internalId: internalId,
peerId: peerId, peerId: peerId,
invite: invite,
joinAsPeerId: joinAsPeerId joinAsPeerId: joinAsPeerId
) )
strongSelf.updateCurrentGroupCall(call) strongSelf.updateCurrentGroupCall(call)

View File

@ -344,6 +344,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var initialCall: CachedChannelData.ActiveCall? private var initialCall: CachedChannelData.ActiveCall?
public let internalId: CallSessionInternalId public let internalId: CallSessionInternalId
public let peerId: PeerId public let peerId: PeerId
private let invite: String?
private var joinAsPeerId: PeerId private var joinAsPeerId: PeerId
public private(set) var isVideo: Bool public private(set) var isVideo: Bool
@ -526,6 +527,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
initialCall: CachedChannelData.ActiveCall?, initialCall: CachedChannelData.ActiveCall?,
internalId: CallSessionInternalId, internalId: CallSessionInternalId,
peerId: PeerId, peerId: PeerId,
invite: String?,
joinAsPeerId: PeerId? joinAsPeerId: PeerId?
) { ) {
self.account = accountContext.account self.account = accountContext.account
@ -537,6 +539,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.initialCall = initialCall self.initialCall = initialCall
self.internalId = internalId self.internalId = internalId
self.peerId = peerId self.peerId = peerId
self.invite = invite
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title) self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title)
@ -1000,7 +1003,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
callId: callInfo.id, callId: callInfo.id,
accessHash: callInfo.accessHash, accessHash: callInfo.accessHash,
preferMuted: true, preferMuted: true,
joinPayload: joinPayload joinPayload: joinPayload,
inviteHash: strongSelf.invite
) )
|> deliverOnMainQueue).start(next: { joinCallResult in |> deliverOnMainQueue).start(next: { joinCallResult in
guard let strongSelf = self else { 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 subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let totalHeight = titleSize.height + subtitleSize.height + 1.0 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.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) self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)

View File

@ -23,13 +23,14 @@ import PresentationDataUtils
import DirectionalPanGesture import DirectionalPanGesture
import PeerInfoUI import PeerInfoUI
import AvatarNode import AvatarNode
import TooltipUI
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e) private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e) private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
private let fullscreenBackgroundColor = UIColor(rgb: 0x000000) private let fullscreenBackgroundColor = UIColor(rgb: 0x000000)
private let dimColor = UIColor(white: 0.0, alpha: 0.5) private let dimColor = UIColor(white: 0.0, alpha: 0.5)
private let sideButtonSize = CGSize(width: 56.0, height: 56.0) 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? { private func cornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
if !top && !bottom { if !top && !bottom {
@ -63,6 +64,15 @@ private final class VoiceChatControllerTitleNode: ASDisplayNode {
private let titleNode: ASTextNode private let titleNode: ASTextNode
private let infoNode: 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) { init(theme: PresentationTheme) {
self.theme = theme self.theme = theme
@ -79,16 +89,29 @@ private final class VoiceChatControllerTitleNode: ASDisplayNode {
self.infoNode.truncationMode = .byTruncatingTail self.infoNode.truncationMode = .byTruncatingTail
self.infoNode.isOpaque = false self.infoNode.isOpaque = false
self.recordingIconNode = VoiceChatRecordingIconNode(hasBackground: false)
super.init() super.init()
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.infoNode) self.addSubnode(self.infoNode)
self.addSubnode(self.recordingIconNode)
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") 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) { func update(size: CGSize, title: String, subtitle: String, transition: ContainedViewLayoutTransition) {
var titleUpdated = false var titleUpdated = false
if let previousTitle = self.titleNode.attributedText?.string { if let previousTitle = self.titleNode.attributedText?.string {
@ -116,8 +139,13 @@ private final class VoiceChatControllerTitleNode: ASDisplayNode {
let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing 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) 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.closeButton.setContent(.image(closeButtonImage(dark: false)))
self.titleNode = VoiceChatControllerTitleNode(theme: self.presentationData.theme) self.titleNode = VoiceChatControllerTitleNode(theme: self.presentationData.theme)
self.titleNode.isUserInteractionEnabled = false
self.topCornersNode = ASImageNode() self.topCornersNode = ASImageNode()
self.topCornersNode.displaysAsynchronously = false self.topCornersNode.displaysAsynchronously = false
@ -792,6 +819,10 @@ public final class VoiceChatController: ViewController {
let displayAsPeersPromise = Promise<[FoundPeer]>([]) let displayAsPeersPromise = Promise<[FoundPeer]>([])
displayAsPeersPromise.set(displayAsPeers) displayAsPeersPromise.set(displayAsPeers)
let inviteLinksPromise = Promise<GroupCallInviteLinks?>(nil)
inviteLinksPromise.set(.single(nil)
|> then(call.inviteLinks))
self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in
let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted) let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
}, openPeer: { [weak self] peerId in }, openPeer: { [weak self] peerId in
@ -829,10 +860,12 @@ public final class VoiceChatController: ViewController {
} }
let groupPeerId = strongSelf.call.peerId 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) 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 { guard let strongSelf = self else {
return return
} }
@ -840,6 +873,18 @@ public final class VoiceChatController: ViewController {
return 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] = [] var filters: [ChannelMembersSearchFilter] = []
if let (currentCallMembers, _) = strongSelf.currentCallMembers { if let (currentCallMembers, _) = strongSelf.currentCallMembers {
filters.append(.disable(Array(currentCallMembers.map { $0.peer.id }))) 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 let titleAndRecording: Signal<(String?, Bool), NoError> = self.call.state
|> map { state -> String? in |> map { state -> (String?, Bool) in
return state.title 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 { guard let strongSelf = self else {
return return
} }
let (title, isRecording) = titleAndRecording
if let peer = peerViewMainPeer(view) { if let peer = peerViewMainPeer(view) {
strongSelf.peer = peer strongSelf.peer = peer
strongSelf.currentTitleIsCustom = title != nil strongSelf.currentTitleIsCustom = title != nil
strongSelf.currentTitle = title ?? peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) 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 { if !strongSelf.didSetDataReady {
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? ([], nil), invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) 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) 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) let avatarSize = CGSize(width: 28.0, height: 28.0)
self.optionsButton.contextAction = { [weak self] sourceNode, gesture in self.optionsButton.contextAction = { [weak self] sourceNode, gesture in
guard let strongSelf = self, let controller = strongSelf.controller else { guard let strongSelf = self, let controller = strongSelf.controller else {
@ -1629,38 +1670,7 @@ public final class VoiceChatController: ViewController {
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
f(.default) f(.default)
guard let strongSelf = self else { self?.presentShare(inviteLinks)
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))
}))) })))
} }
@ -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.isFullscreen = true
//self.isExpanded = 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) 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 var pressTimer: SwiftSignalKit.Timer?
private func startPressTimer() { private func startPressTimer() {
self.pressTimer?.invalidate() 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 { if reclaim {
self.dismissed = true 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 { if self.isSlidOffscreen {
self.isSlidOffscreen = false self.isSlidOffscreen = false
self.isButtonHidden = true self.isButtonHidden = true
actionButton.layer.sublayerTransform = CATransform3DIdentity actionButton.layer.sublayerTransform = CATransform3DIdentity
actionButton.update(snap: false, animated: false) 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 leftButton.isHidden = false
rightButton.isHidden = false rightButton.isHidden = false
@ -191,7 +191,7 @@ public final class VoiceChatOverlayController: ViewController {
actionButton.layer.removeAllAnimations() actionButton.layer.removeAllAnimations()
actionButton.layer.sublayerTransform = CATransform3DIdentity actionButton.layer.sublayerTransform = CATransform3DIdentity
actionButton.update(snap: false, animated: false) 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 leftButton.isHidden = false
rightButton.isHidden = false rightButton.isHidden = false

View File

@ -36,31 +36,43 @@ final class VoiceChatRecordingContextItem: ContextMenuCustomItem {
private let textFont = Font.regular(17.0) private let textFont = Font.regular(17.0)
private class IconNode: ASDisplayNode { class VoiceChatRecordingIconNode: ASDisplayNode {
private let backgroundNode: ASImageNode private let backgroundNode: ASImageNode
private let dotNode: ASImageNode private let dotNode: ASImageNode
override init() { init(hasBackground: Bool) {
let iconSize = 16.0 + (1.0 + UIScreenPixel) * 2.0 let iconSize = 16.0 + (1.0 + UIScreenPixel) * 2.0
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateCircleImage(diameter: iconSize, lineWidth: 1.0 + UIScreenPixel, color: UIColor.white, backgroundColor: nil) self.backgroundNode.image = generateCircleImage(diameter: iconSize, lineWidth: 1.0 + UIScreenPixel, color: UIColor.white, backgroundColor: nil)
self.backgroundNode.isLayerBacked = true
self.dotNode = ASImageNode() self.dotNode = ASImageNode()
self.dotNode.displaysAsynchronously = false self.dotNode.displaysAsynchronously = false
self.dotNode.displayWithoutProcessing = true self.dotNode.displayWithoutProcessing = true
self.dotNode.image = generateFilledCircleImage(diameter: 8.0, color: UIColor(rgb: 0xff3b30)) self.dotNode.image = generateFilledCircleImage(diameter: 8.0, color: UIColor(rgb: 0xff3b30))
self.dotNode.isLayerBacked = true
super.init() super.init()
self.addSubnode(self.backgroundNode) self.isLayerBacked = true
if hasBackground {
self.addSubnode(self.backgroundNode)
}
self.addSubnode(self.dotNode) self.addSubnode(self.dotNode)
} }
override func didLoad() { override func didEnterHierarchy() {
super.didLoad() self.setupAnimation()
}
override func didExitHierarchy() {
self.dotNode.layer.removeAllAnimations()
}
private func setupAnimation() {
let animation = CAKeyframeAnimation(keyPath: "opacity") let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber] 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] 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 highlightedBackgroundNode: ASDisplayNode
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private let statusNode: ImmediateTextNode private let statusNode: ImmediateTextNode
private let iconNode: IconNode private let iconNode: VoiceChatRecordingIconNode
private let buttonNode: HighlightTrackingButtonNode private let buttonNode: HighlightTrackingButtonNode
private var timer: SwiftSignalKit.Timer? private var timer: SwiftSignalKit.Timer?
@ -132,7 +144,7 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen
self.buttonNode.isAccessibilityElement = true self.buttonNode.isAccessibilityElement = true
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
self.iconNode = IconNode() self.iconNode = VoiceChatRecordingIconNode(hasBackground: true)
super.init() super.init()

View File

@ -15,6 +15,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
private let backgroundNode: ASImageNode private let backgroundNode: ASImageNode
private let textInputNode: EditableTextNode private let textInputNode: EditableTextNode
private let placeholderNode: ASTextNode private let placeholderNode: ASTextNode
private let clearButton: HighlightableButtonNode
var updateHeight: (() -> Void)? var updateHeight: (() -> Void)?
var complete: (() -> Void)? var complete: (() -> Void)?
@ -30,6 +31,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
set { set {
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
self.placeholderNode.isHidden = !newValue.isEmpty 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.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) 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() super.init()
self.textInputNode.delegate = self self.textInputNode.delegate = self
@ -72,6 +81,9 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
self.addSubnode(self.backgroundNode) self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode) self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode) self.addSubnode(self.placeholderNode)
self.addSubnode(self.clearButton)
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
} }
func updateTheme(_ theme: PresentationTheme) { func updateTheme(_ theme: PresentationTheme) {
@ -81,6 +93,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance 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.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.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 { func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
@ -96,7 +109,11 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) 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.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 return panelHeight
} }
@ -113,6 +130,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
self.updateTextNodeText(animated: true) self.updateTextNodeText(animated: true)
self.textChanged?(editableTextNode.textView.text) self.textChanged?(editableTextNode.textView.text)
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
self.clearButton.isHidden = !self.placeholderNode.isHidden
} }
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 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 backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets 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)) 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 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 updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated return updated
}) })

View File

@ -300,8 +300,8 @@ public final class AccountContextImpl: AccountContext {
} }
} }
public func joinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?, activeCall: CachedChannelData.ActiveCall) { 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, joinAsPeerId: joinAsPeerId, initialCall: activeCall, endCurrentIfAny: false) 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 let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peerId { if currentPeerId == peerId {
self.sharedContext.navigateToCurrentCall() self.sharedContext.navigateToCurrentCall()
@ -323,14 +323,14 @@ public final class AccountContextImpl: AccountContext {
guard let strongSelf = self else { guard let strongSelf = self else {
return 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) })]), on: .root)
} else { } 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: { 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 { guard let strongSelf = self else {
return 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) })]), on: .root)
} }
} else { } else {

View File

@ -534,7 +534,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
case .groupPhoneCall, .inviteToGroupPhoneCall: case .groupPhoneCall, .inviteToGroupPhoneCall:
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall { 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 { } else {
var canManageGroupCalls = false var canManageGroupCalls = false
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel { 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 { guard let strongSelf = self else {
return 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 }, error: { [weak self] error in
dismissStatus?() dismissStatus?()
@ -6403,7 +6403,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return return
} }
strongSelf.joinGroupCall(peerId: peer.id, joinAsPeerId: nil, activeCall: activeCall) strongSelf.joinGroupCall(peerId: peer.id, invite: nil, activeCall: activeCall)
}, presentInviteMembers: { [weak self] in }, presentInviteMembers: { [weak self] in
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return return
@ -11101,6 +11101,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if case let .url(url) = subject { if case let .url(url) = subject {
self?.controllerInteraction?.requestMessageActionUrlAuth(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 }, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a) self?.present(c, in: .window(.root), with: a)
}, dismissInput: { [weak self] in }, dismissInput: { [weak self] in
@ -11851,58 +11853,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return inputShortcuts + otherShortcuts return inputShortcuts + otherShortcuts
} }
public override func joinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?, activeCall: CachedChannelData.ActiveCall) { public override func joinGroupCall(peerId: PeerId, invite: String?, activeCall: CachedChannelData.ActiveCall) {
let context = self.context let proceed = {
let presentationData = self.presentationData super.joinGroupCall(peerId: peerId, invite: invite, activeCall: activeCall)
let proceed: (PeerId) -> Void = { joinAsPeerId in
super.joinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId, activeCall: activeCall)
} }
let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in let _ = self.presentVoiceMessageDiscardAlert(action: {
let currentAccountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) proceed()
|> 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))
})
}) })
} }

View File

@ -22,6 +22,7 @@ import LanguageLinkPreviewUI
import PeerInfoUI import PeerInfoUI
import InviteLinksUI import InviteLinksUI
import UndoUI import UndoUI
import TelegramCallsUI
private final class ChatRecentActionsListOpaqueState { private final class ChatRecentActionsListOpaqueState {
let entries: [ChatRecentActionsEntry] let entries: [ChatRecentActionsEntry]
@ -893,6 +894,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, sendFile: nil, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: nil, requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: { c, a in present: { c, a in
self?.presentController(c, a) self?.presentController(c, a)
}, dismissInput: { }, dismissInput: {
@ -908,6 +910,10 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
#endif #endif
case .settings: case .settings:
break 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 UrlHandling
import ShareController import ShareController
import ChatInterfaceState import ChatInterfaceState
import TelegramCallsUI
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
if case .default = navigation { 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 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch resolvedUrl { switch resolvedUrl {
case let .externalUrl(url): case let .externalUrl(url):
@ -462,5 +463,10 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
} }
break 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, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: nil, requestMessageActionUrlAuth: nil,
present: { c, a in joinVoiceChat: { peerId, invite, call in
}, present: { c, a in
context.sharedContext.applicationBindings.dismissNativeController() context.sharedContext.applicationBindings.dismissNativeController()
c.presentationArguments = a c.presentationArguments = a

View File

@ -3084,7 +3084,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, sendFile: nil, }, sendFile: nil,
sendSticker: { [weak self] f, sourceNode, sourceRect in sendSticker: { [weak self] f, sourceNode, sourceRect in
return false 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) self?.controller?.present(c, in: .window(.root), with: a)
}, dismissInput: { [weak self] in }, dismissInput: { [weak self] in
self?.view.endEditing(true) self?.view.endEditing(true)
@ -3106,6 +3109,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, sendFile: nil, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: nil, requestMessageActionUrlAuth: nil,
joinVoiceChat: { peerId, invite, call in
},
present: { c, a in present: { c, a in
self?.controller?.present(c, in: .window(.root), with: a) self?.controller?.present(c, in: .window(.root), with: a)
}, dismissInput: { }, dismissInput: {
@ -3308,7 +3314,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
case .videoCall: case .videoCall:
self.requestCall(isVideo: true) self.requestCall(isVideo: true)
case .voiceChat: case .voiceChat:
self.openVoiceChatDisplayAsPeerSelection(gesture: gesture, contextController: nil, result: nil, backAction: nil) self.requestCall(isVideo: false)
case .mute: case .mute:
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState { if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start() 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 items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in }, 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 { if let mainItemsImpl = mainItemsImpl {
c.setItems(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 items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in }, 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)
if let mainItemsImpl = mainItemsImpl {
c.setItems(mainItemsImpl())
}
})
}))) })))
} }
@ -3816,28 +3818,37 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
self.controller?.present(shareController, in: .window(.root)) self.controller?.present(shareController, in: .window(.root))
} }
private func requestCall(isVideo: Bool, joinAsPeerId: PeerId? = nil) { private func requestCall(isVideo: Bool, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) {
if let peer = self.data?.peer as? TelegramChannel { let peerId = self.peerId
guard let cachedChannelData = self.data?.cachedData as? CachedChannelData else { let requestCall: (PeerId?, CachedChannelData.ActiveCall?) -> Void = { [weak self] defaultJoinAsPeerId, activeCall in
return if let activeCall = activeCall {
} self?.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { completion in
if let defaultJoinAsPeerId = defaultJoinAsPeerId {
if let activeCall = cachedChannelData.activeCall { result?(.dismissWithoutContent)
self.context.joinGroupCall(peerId: peer.id, joinAsPeerId: joinAsPeerId, activeCall: activeCall) completion(defaultJoinAsPeerId)
} else {
self?.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
completion(joinAsPeerId)
}, gesture: gesture, contextController: contextController, result: result, backAction: backAction)
}
}, activeCall: activeCall)
} else { } 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 return
} else if let peer = self.data?.peer as? TelegramGroup { } else if let cachedGroupData = self.data?.cachedData as? CachedGroupData {
guard let cachedGroupData = self.data?.cachedData as? CachedGroupData else { requestCall(cachedGroupData.callJoinPeerId, cachedGroupData.activeCall)
return
}
if let activeCall = cachedGroupData.activeCall {
self.context.joinGroupCall(peerId: peer.id, joinAsPeerId: joinAsPeerId, activeCall: activeCall)
} else {
self.createAndJoinGroupCall(peerId: peer.id, joinAsPeerId: joinAsPeerId)
}
return return
} }
@ -3873,7 +3884,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
guard let strongSelf = self else { guard let strongSelf = self else {
return 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 }, error: { [weak self] error in
dismissStatus?() dismissStatus?()
@ -4192,7 +4205,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
controller.push(statsController) 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) let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map { peer in |> map { peer in
return [FoundPeer(peer: peer, subscribers: nil)] return [FoundPeer(peer: peer, subscribers: nil)]
@ -4206,9 +4219,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
if peers.count == 1 { if peers.count == 1, let peer = peers.first {
result?(.dismissWithoutContent) result?(.dismissWithoutContent)
strongSelf.requestCall(isVideo: false, joinAsPeerId: nil) completion(peer.peer.id)
} else { } else {
var items: [ContextMenuItem] = [] 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 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) 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 { if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
@ -4323,6 +4336,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, sendFile: nil, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: nil, requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: { [weak controller] c, a in present: { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a) controller?.present(c, in: .window(.root), with: a)
}, dismissInput: { [weak controller] in }, dismissInput: { [weak controller] in
@ -5153,7 +5167,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
resolvedUrl = .instantView(webPage, customAnchor) resolvedUrl = .instantView(webPage, customAnchor)
} }
strongSelf.context.sharedContext.openResolvedUrl(resolvedUrl, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, openPeer: { peer, navigation in 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) self?.controller?.push(controller)
}, dismissInput: {}, contentContext: nil) }, dismissInput: {}, contentContext: nil)
} }

View File

@ -1148,8 +1148,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return resolveUrlImpl(account: account, url: url, skipUrlAuth: skipUrlAuth) 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?) { 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, present: present, dismissInput: dismissInput, contentContext: contentContext) 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 { 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, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: nil, requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: presentImpl, dismissInput: {}, contentContext: nil) present: presentImpl, dismissInput: {}, contentContext: nil)
} }

View File

@ -297,7 +297,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
case let .point(rect, arrowPosition): case let .point(rect, arrowPosition):
let backgroundWidth = textSize.width + contentInset * 2.0 + animationSize.width + animationSpacing let backgroundWidth = textSize.width + contentInset * 2.0 + animationSize.width + animationSpacing
switch arrowPosition { 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)) backgroundFrame = CGRect(origin: CGPoint(x: rect.midX - backgroundWidth / 2.0, y: rect.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
case .right: case .right:
backgroundFrame = CGRect(origin: CGPoint(x: rect.minX - backgroundWidth - bottomInset, y: rect.midY - backgroundHeight / 2.0), size: CGSize(width: backgroundWidth, height: backgroundHeight)) 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 backgroundFrame.origin.y = rect.maxY + bottomInset
invertArrow = true invertArrow = true
} }
if case .top = arrowPosition, !invertArrow {
invertArrow = true
backgroundFrame.origin.y = rect.maxY + bottomInset
}
self.isArrowInverted = invertArrow self.isArrowInverted = invertArrow
case .top: case .top:
let backgroundWidth = containerWidth let backgroundWidth = containerWidth
@ -332,7 +336,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
let arrowFrame: CGRect let arrowFrame: CGRect
switch arrowPosition { switch arrowPosition {
case .bottom: case .bottom, .top:
if invertArrow { if invertArrow {
arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: -arrowSize.height), size: arrowSize) arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: -arrowSize.height), size: arrowSize)
} else { } else {
@ -404,7 +408,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
let startPoint: CGPoint let startPoint: CGPoint
switch arrowPosition { switch arrowPosition {
case .bottom: case .bottom, .top:
let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY 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) startPoint = CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)
case .right: case .right:
@ -448,7 +452,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
let targetPoint: CGPoint let targetPoint: CGPoint
switch arrowPosition { switch arrowPosition {
case .bottom: case .bottom, .top:
let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY 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) targetPoint = CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)
case .right: case .right:
@ -484,8 +488,9 @@ public final class TooltipScreen: ViewController {
} }
public enum ArrowPosition { public enum ArrowPosition {
case bottom case top
case right case right
case bottom
} }
public enum Location { 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)) return .replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId))
} }
case let .voiceChat(invite): case let .voiceChat(invite):
return .single(.peer(peer.id, .default)) return .single(.joinVoiceChat(peer.id, invite))
} }
} else { } else {
if let peer = peer as? TelegramUser, peer.botInfo == nil { if let peer = peer as? TelegramUser, peer.botInfo == nil {