Conference calls

This commit is contained in:
Isaac 2025-04-03 00:37:42 +04:00
parent 5deca3fac7
commit 1a31d83699
18 changed files with 380 additions and 136 deletions

View File

@ -423,6 +423,7 @@ public protocol PresentationGroupCall: AnyObject {
var internalId: CallSessionInternalId { get }
var peerId: EnginePeer.Id? { get }
var callId: Int64? { get }
var currentReference: InternalGroupCallReference? { get }
var hasVideo: Bool { get }
var hasScreencast: Bool { get }

View File

@ -138,16 +138,7 @@ class CallListCallItem: ListViewItem {
func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
var isVideo = false
for media in self.topMessage.media {
if let action = media as? TelegramMediaAction {
if case let .phoneCall(_, _, _, isVideoValue) = action.action {
isVideo = isVideoValue
break
}
}
}
self.interaction.call(self.topMessage.id.peerId, isVideo)
self.interaction.call(self.topMessage)
}
static func mergeType(item: CallListCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
@ -262,15 +253,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
guard let item = self?.layoutParams?.0 else {
return false
}
var isVideo = false
for media in item.topMessage.media {
if let action = media as? TelegramMediaAction {
if case let .phoneCall(_, _, _, isVideoValue) = action.action {
isVideo = isVideoValue
}
}
}
item.interaction.call(item.topMessage.id.peerId, isVideo)
item.interaction.call(item.topMessage)
return true
}
}
@ -390,6 +373,9 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
var hadDuration = false
var callDuration: Int32?
var isConference = false
var conferenceIsDeclined = false
for message in item.messages {
inner: for media in message.media {
if let action = media as? TelegramMediaAction {
@ -411,6 +397,36 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
} else {
callDuration = nil
}
} else if case let .conferenceCall(conferenceCall) = action.action {
isConference = true
isVideo = conferenceCall.flags.contains(.isVideo)
if message.flags.contains(.Incoming) {
hasIncoming = true
//TODO:localize
let missedTimeout: Int32
#if DEBUG
missedTimeout = 5
#else
missedTimeout = 30
#endif
let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) {
titleColor = item.presentationData.theme.list.itemDestructiveColor
conferenceIsDeclined = true
} else if message.timestamp < currentTime - missedTimeout {
titleColor = item.presentationData.theme.list.itemDestructiveColor
hasMissed = true
}
} else {
hasOutgoing = true
}
if callDuration == nil && !hadDuration {
hadDuration = true
callDuration = conferenceCall.duration
} else {
callDuration = nil
}
}
break inner
}
@ -441,7 +457,18 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor)
}
if hasMissed {
if isConference {
//TODO:localize
if conferenceIsDeclined {
statusAttributedString = NSAttributedString(string: "Declined Group Call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
} else if hasMissed {
statusAttributedString = NSAttributedString(string: "Missed Group Call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
} else {
statusAttributedString = NSAttributedString(string: "Group call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
}
statusAccessibilityString = statusAttributedString?.string ?? ""
} else if hasMissed {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallMissedShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
statusAccessibilityString = isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallMissed : item.presentationData.strings.Call_VoiceOver_VoiceCallMissed
} else if hasIncoming && hasOutgoing {

View File

@ -92,6 +92,7 @@ public final class CallListController: TelegramBaseController {
private let createActionDisposable = MetaDisposable()
private let clearDisposable = MetaDisposable()
private var createConferenceCallDisposable: Disposable?
public init(context: AccountContext, mode: CallListControllerMode) {
self.context = context
@ -163,6 +164,7 @@ public final class CallListController: TelegramBaseController {
self.presentationDataDisposable?.dispose()
self.peerViewDisposable.dispose()
self.clearDisposable.dispose()
self.createConferenceCallDisposable?.dispose()
}
private func updateThemeAndStrings() {
@ -210,11 +212,16 @@ public final class CallListController: TelegramBaseController {
guard !self.presentAccountFrozenInfoIfNeeded() else {
return
}
let _ = (self.context.engine.calls.createConferenceCall()
|> deliverOnMainQueue).startStandalone(next: { [weak self] call in
if self.createConferenceCallDisposable != nil {
return
}
self.createConferenceCallDisposable = (self.context.engine.calls.createConferenceCall()
|> deliverOnMainQueue).startStrict(next: { [weak self] call in
guard let self else {
return
}
self.createConferenceCallDisposable?.dispose()
self.createConferenceCallDisposable = nil
let openCall: () -> Void = { [weak self] in
guard let self else {
@ -235,34 +242,51 @@ public final class CallListController: TelegramBaseController {
)
}
let controller = InviteLinkInviteController(context: self.context, updatedPresentationData: nil, mode: .groupCall(link: call.link, isRecentlyCreated: true), parentNavigationController: self.navigationController as? NavigationController, completed: { [weak self] result in
guard let self else {
return
}
if let result {
switch result {
case .linkCopied:
//TODO:localize
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: "View Call", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if case .undo = action {
openCall()
}
return false
}), in: .window(.root))
case .openCall:
openCall()
let controller = InviteLinkInviteController(
context: self.context,
updatedPresentationData: nil,
mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(callId: call.callInfo.id, accessHash: call.callInfo.accessHash, isRecentlyCreated: true, canRevoke: true)),
initialInvite: .link(link: call.link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil),
parentNavigationController: self.navigationController as? NavigationController,
completed: { [weak self] result in
guard let self else {
return
}
if let result {
switch result {
case .linkCopied:
//TODO:localize
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: "View Call", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if case .undo = action {
openCall()
}
return false
}), in: .window(.root))
case .openCall:
openCall()
}
}
}
})
)
self.present(controller, in: .window(.root), with: nil)
})
}
override public func loadDisplayNode() {
self.displayNode = CallListControllerNode(controller: self, context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] peerId, isVideo in
if let strongSelf = self {
strongSelf.call(peerId, isVideo: isVideo)
self.displayNode = CallListControllerNode(controller: self, context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] message in
guard let self else {
return
}
for media in message.media {
if let action = media as? TelegramMediaAction {
if case let .phoneCall(_, _, _, isVideo) = action.action {
self.call(message.id.peerId, isVideo: isVideo)
} else if case .conferenceCall = action.action {
self.openGroupCall(message: message)
}
}
}
}, joinGroupCall: { [weak self] peerId, activeCall in
if let self {
@ -573,6 +597,48 @@ public final class CallListController: TelegramBaseController {
}))
}
private func openGroupCall(message: EngineMessage) {
var action: TelegramMediaAction?
for media in message.media {
if let media = media as? TelegramMediaAction {
action = media
break
}
}
guard case let .conferenceCall(conferenceCall) = action?.action else {
return
}
if conferenceCall.duration != nil {
return
}
if let currentGroupCallController = self.context.sharedContext as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == conferenceCall.callId {
self.context.sharedContext.navigateToCurrentCall()
return
}
let signal = self.context.engine.peers.joinCallInvitationInformation(messageId: message.id)
let _ = (signal
|> deliverOnMainQueue).startStandalone(next: { [weak self] resolvedCallLink in
guard let self else {
return
}
self.context.sharedContext.callManager?.joinConferenceCall(
accountContext: self.context,
initialCall: EngineGroupCallDescription(
id: resolvedCallLink.id,
accessHash: resolvedCallLink.accessHash,
title: nil,
scheduleTimestamp: nil,
subscribedToScheduled: false,
isStream: false
),
reference: .message(id: message.id),
beginWithVideo: conferenceCall.flags.contains(.isVideo)
)
})
}
override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_StartNewCall, icon: { theme in

View File

@ -62,14 +62,14 @@ private extension EngineCallList.Item {
final class CallListNodeInteraction {
let setMessageIdWithRevealedOptions: (EngineMessage.Id?, EngineMessage.Id?) -> Void
let call: (EnginePeer.Id, Bool) -> Void
let call: (EngineMessage) -> Void
let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
let delete: ([EngineMessage.Id]) -> Void
let updateShowCallsTab: (Bool) -> Void
let openGroupCall: (EnginePeer.Id) -> Void
let createGroupCall: () -> Void
init(setMessageIdWithRevealedOptions: @escaping (EngineMessage.Id?, EngineMessage.Id?) -> Void, call: @escaping (EnginePeer.Id, Bool) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, delete: @escaping ([EngineMessage.Id]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void, openGroupCall: @escaping (EnginePeer.Id) -> Void, createGroupCall: @escaping () -> Void) {
init(setMessageIdWithRevealedOptions: @escaping (EngineMessage.Id?, EngineMessage.Id?) -> Void, call: @escaping (EngineMessage) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, delete: @escaping ([EngineMessage.Id]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void, openGroupCall: @escaping (EnginePeer.Id) -> Void, createGroupCall: @escaping () -> Void) {
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
self.call = call
self.openInfo = openInfo
@ -222,7 +222,7 @@ final class CallListControllerNode: ASDisplayNode {
private let emptyButtonIconNode: ASImageNode
private let emptyButtonTextNode: ImmediateTextNode
private let call: (EnginePeer.Id, Bool) -> Void
private let call: (EngineMessage) -> Void
private let joinGroupCall: (EnginePeer.Id, EngineGroupCallDescription) -> Void
private let createGroupCall: () -> Void
private let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
@ -234,7 +234,7 @@ final class CallListControllerNode: ASDisplayNode {
private var previousContentOffset: ListViewVisibleContentOffset?
init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EnginePeer.Id, Bool) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, createGroupCall: @escaping () -> Void) {
init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EngineMessage) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, createGroupCall: @escaping () -> Void) {
self.controller = controller
self.context = context
self.mode = mode
@ -333,8 +333,8 @@ final class CallListControllerNode: ASDisplayNode {
}
}
}
}, call: { [weak self] peerId, isVideo in
self?.call(peerId, isVideo)
}, call: { [weak self] message in
self?.call(message)
}, openInfo: { [weak self] peerId, messages in
self?.openInfo(peerId, messages)
}, delete: { [weak self] messageIds in

View File

@ -154,14 +154,32 @@ private func preparedTransition(from fromEntries: [InviteLinkInviteEntry], to to
return InviteLinkInviteTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading)
}
private func getBackgroundColor(theme: PresentationTheme) -> UIColor {
return theme.actionSheet.opaqueItemBackgroundColor
}
public final class InviteLinkInviteController: ViewController {
private var controllerNode: Node {
return self.displayNode as! Node
}
public enum Mode {
public struct GroupCall {
public let callId: Int64
public let accessHash: Int64
public let isRecentlyCreated: Bool
public let canRevoke: Bool
public init(callId: Int64, accessHash: Int64, isRecentlyCreated: Bool, canRevoke: Bool) {
self.callId = callId
self.accessHash = accessHash
self.isRecentlyCreated = isRecentlyCreated
self.canRevoke = canRevoke
}
}
case groupOrChannel(peerId: EnginePeer.Id)
case groupCall(link: String, isRecentlyCreated: Bool)
case groupCall(GroupCall)
}
public enum CompletionResult {
@ -173,6 +191,7 @@ public final class InviteLinkInviteController: ViewController {
private let context: AccountContext
private let mode: Mode
private let initialInvite: ExportedInvitation?
private weak var parentNavigationController: NavigationController?
private var presentationData: PresentationData
@ -180,9 +199,10 @@ public final class InviteLinkInviteController: ViewController {
fileprivate let completed: ((CompletionResult?) -> Void)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: Mode, parentNavigationController: NavigationController?, completed: ((CompletionResult?) -> Void)? = nil) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: Mode, initialInvite: ExportedInvitation?, parentNavigationController: NavigationController?, completed: ((CompletionResult?) -> Void)? = nil) {
self.context = context
self.mode = mode
self.initialInvite = initialInvite
self.parentNavigationController = parentNavigationController
self.completed = completed
@ -215,7 +235,7 @@ public final class InviteLinkInviteController: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context, presentationData: self.presentationData, mode: self.mode, controller: self)
self.displayNode = Node(context: self.context, presentationData: self.presentationData, mode: self.mode, controller: self, initialInvite: self.initialInvite)
}
private var didAppearOnce: Bool = false
@ -296,7 +316,7 @@ public final class InviteLinkInviteController: ViewController {
private var revokeDisposable = MetaDisposable()
init(context: AccountContext, presentationData: PresentationData, mode: InviteLinkInviteController.Mode, controller: InviteLinkInviteController) {
init(context: AccountContext, presentationData: PresentationData, mode: InviteLinkInviteController.Mode, controller: InviteLinkInviteController, initialInvite: ExportedInvitation?) {
self.context = context
self.mode = mode
@ -319,7 +339,7 @@ public final class InviteLinkInviteController: ViewController {
self.headerNode.clipsToBounds = false
self.headerBackgroundNode = ASDisplayNode()
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
self.headerBackgroundNode.cornerRadius = 16.0
self.headerBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
@ -338,7 +358,7 @@ public final class InviteLinkInviteController: ViewController {
self.historyBackgroundContentNode = ASDisplayNode()
self.historyBackgroundContentNode.isLayerBacked = true
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.historyBackgroundContentNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode)
@ -354,7 +374,7 @@ public final class InviteLinkInviteController: ViewController {
self.backgroundColor = nil
self.isOpaque = false
let mainInvitePromise = ValuePromise<ExportedInvitation?>(nil)
let mainInvitePromise = ValuePromise<ExportedInvitation?>(initialInvite)
self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in
guard let self else {
@ -363,7 +383,6 @@ public final class InviteLinkInviteController: ViewController {
guard let node = node as? ContextReferenceContentNode else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
@ -381,17 +400,17 @@ public final class InviteLinkInviteController: ViewController {
}
})))
if case let .groupOrChannel(peerId) = self.mode {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self else {
return
}
guard let self else {
return
}
if let invite = invite {
if let invite {
if case let .groupOrChannel(peerId) = self.mode {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
@ -404,12 +423,17 @@ public final class InviteLinkInviteController: ViewController {
isGroup = true
}
let updatedPresentationData = (strongSelf.presentationData, strongSelf.presentationDataPromise.get())
let controller = QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup))
let controller = QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel))
strongSelf.controller?.present(controller, in: .window(.root))
})
} else if case .groupCall = self.mode {
let controller = QrCodeScreen(context: context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), subject: .invite(invite: invite, type: .channel))
self.controller?.present(controller, in: .window(.root))
}
})))
}
})))
if case let .groupOrChannel(peerId) = self.mode {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [ weak self] _, f in
@ -454,7 +478,45 @@ public final class InviteLinkInviteController: ViewController {
self?.controller?.present(controller, in: .window(.root))
})
})))
} else if case let .groupCall(groupCall) = self.mode, groupCall.canRevoke {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [ weak self] _, f in
f(.dismissWithoutContent)
guard let self else {
return
}
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
//TODO:localize
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: "Revoke Link"),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { [weak self] in
dismissAction()
guard let self else {
return
}
if let inviteLink = invite?.link {
let _ = (context.engine.calls.revokeConferenceInviteLink(reference: .id(id: groupCall.callId, accessHash: groupCall.accessHash), link: inviteLink) |> deliverOnMainQueue).start(next: { result in
mainInvitePromise.set(.link(link: result.listenerLink, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil))
})
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: presentationData.strings.InviteLink_InviteLinkRevoked), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.controller?.present(controller, in: .window(.root))
})))
}
let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
@ -597,15 +659,16 @@ public final class InviteLinkInviteController: ViewController {
strongSelf.enqueueTransition(transition)
}
})
case let .groupCall(link, isRecentlyCreated):
//TODO:release
let tempInfo: Signal<Void, NoError> = .single(Void()) |> delay(0.0, queue: .mainQueue())
case let .groupCall(groupCall):
// A workaround to skip the first run of the event cycle
let delayOfZero = Signal<Void, NoError>.single(()) |> delay(0.0, queue: .mainQueue())
self.disposable = (combineLatest(queue: .mainQueue(),
self.presentationDataPromise.get(),
tempInfo
mainInvitePromise.get(),
delayOfZero
)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, _ in
|> deliverOnMainQueue).start(next: { [weak self] presentationData, mainInvite, _ in
guard let self else {
return
}
@ -615,9 +678,9 @@ public final class InviteLinkInviteController: ViewController {
let helpText: String = "Anyone on Telegram can join your call by following the link below."
entries.append(.header(title: "Call Link", text: helpText))
let mainInvite: ExportedInvitation = .link(link: link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil)
let mainInvite: ExportedInvitation = .link(link: mainInvite?.link ?? "", title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil)
entries.append(.mainLink(invitation: mainInvite, isCall: true, isRecentlyCreated: isRecentlyCreated))
entries.append(.mainLink(invitation: mainInvite, isCall: true, isRecentlyCreated: groupCall.isRecentlyCreated))
let previousEntries = previousEntries.swap(entries)
@ -675,8 +738,8 @@ public final class InviteLinkInviteController: ViewController {
self.presentationData = presentationData
self.presentationDataPromise.set(.single(presentationData))
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.historyBackgroundContentNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
self.headerBackgroundNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.doneButtonIconNode.image = generateCloseButtonImage(backgroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))!

View File

@ -530,7 +530,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
} else {
isGroup = true
}
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
})
})))
@ -719,7 +719,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
isGroup = true
}
Queue.mainQueue().after(0.2) {
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
}
})
})))

View File

@ -755,7 +755,7 @@ public final class InviteLinkViewController: ViewController {
isGroup = true
}
let updatedPresentationData = (strongSelf.presentationData, parentController.presentationDataPromise.get())
strongSelf.controller?.present(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), in: .window(.root))
strongSelf.controller?.present(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), in: .window(.root))
})
})))
}

View File

@ -655,7 +655,7 @@ public func channelMembersController(context: AccountContext, updatedPresentatio
}, inviteViaLink: {
if let controller = getControllerImpl?() {
dismissInputImpl?()
presentControllerImpl?(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: peerId), parentNavigationController: controller.navigationController as? NavigationController), nil)
presentControllerImpl?(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: peerId), initialInvite: nil, parentNavigationController: controller.navigationController as? NavigationController), nil)
}
}, updateHideMembers: { value in
let _ = context.engine.peers.updateChannelMembersHidden(peerId: peerId, value: value).start()

View File

@ -1609,7 +1609,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
} else {
isGroup = true
}
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
})
}
})

View File

@ -35,9 +35,15 @@ private func shareQrCode(context: AccountContext, link: String, ecl: String, vie
}
public final class QrCodeScreen: ViewController {
public enum SubjectType {
case group
case channel
case groupCall
}
public enum Subject {
case peer(peer: EnginePeer)
case invite(invite: ExportedInvitation, isGroup: Bool)
case invite(invite: ExportedInvitation, type: SubjectType)
case chatFolder(slug: String)
var link: String {
@ -239,9 +245,17 @@ public final class QrCodeScreen: ViewController {
let title: String
let text: String
switch subject {
case let .invite(_, isGroup):
case let .invite(_, type):
title = self.presentationData.strings.InviteLink_QRCode_Title
text = isGroup ? self.presentationData.strings.InviteLink_QRCode_Info : self.presentationData.strings.InviteLink_QRCode_InfoChannel
switch type {
case .group:
text = self.presentationData.strings.InviteLink_QRCode_Info
case .channel:
text = self.presentationData.strings.InviteLink_QRCode_InfoChannel
case .groupCall:
//TODO:localize
text = "Everyone on Telegram can scan this code to join your group call."
}
case .chatFolder:
title = self.presentationData.strings.InviteLink_QRCodeFolder_Title
text = self.presentationData.strings.InviteLink_QRCodeFolder_Text

View File

@ -161,7 +161,8 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
isVideoEnabled: state.isVideoEnabled,
unmutedVideoLimit: state.unmutedVideoLimit,
isStream: state.isStream
isStream: state.isStream,
isCreator: state.isCreator
),
topParticipants: topParticipants,
participantCount: state.totalCount,
@ -888,6 +889,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void)
private(set) var initialCall: (description: EngineGroupCallDescription, reference: InternalGroupCallReference)?
public var currentReference: InternalGroupCallReference?
public let internalId: CallSessionInternalId
public let peerId: EnginePeer.Id?
private let isChannel: Bool
@ -1208,6 +1210,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.getDeviceAccessData = getDeviceAccessData
self.initialCall = initialCall
self.currentReference = initialCall?.reference
self.callId = initialCall?.description.id
self.internalId = internalId
@ -1963,7 +1966,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
defaultParticipantsAreMuted: callInfo.defaultParticipantsAreMuted ?? state.defaultParticipantsAreMuted,
isVideoEnabled: callInfo.isVideoEnabled,
unmutedVideoLimit: callInfo.unmutedVideoLimit,
isStream: callInfo.isStream
isStream: callInfo.isStream,
isCreator: callInfo.isCreator
)), audioSessionControl: self.audioSessionControl)
} else {
self.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
@ -1979,7 +1983,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
isVideoEnabled: state.isVideoEnabled,
unmutedVideoLimit: state.unmutedVideoLimit,
isStream: callInfo.isStream
isStream: callInfo.isStream,
isCreator: callInfo.isCreator
))))
self.summaryParticipantsState.set(.single(SummaryParticipantsState(
@ -2301,6 +2306,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
guard let self else {
return
}
self.currentReference = .id(id: joinCallResult.callInfo.id, accessHash: joinCallResult.callInfo.accessHash)
let clientParams = joinCallResult.jsonParams
if let data = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] {
if let video = dict["video"] as? [String: Any] {
@ -2910,7 +2918,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
isVideoEnabled: state.isVideoEnabled,
unmutedVideoLimit: state.unmutedVideoLimit,
isStream: callInfo.isStream
isStream: callInfo.isStream,
isCreator: callInfo.isCreator
))))
self.summaryParticipantsState.set(.single(SummaryParticipantsState(

View File

@ -25,6 +25,7 @@ import LegacyComponents
import TooltipUI
import BlurredBackgroundComponent
import CallsEmoji
import InviteLinksUI
extension VideoChatCall {
var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> {
@ -653,6 +654,51 @@ final class VideoChatScreenComponent: Component {
return
}
if groupCall.isConference {
guard let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
return
}
guard let currentReference = groupCall.currentReference, case let .id(callId, accessHash) = currentReference else {
return
}
guard let callState = self.callState else {
return
}
var presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
let controller = InviteLinkInviteController(
context: groupCall.accountContext,
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(
callId: callId,
accessHash: accessHash,
isRecentlyCreated: false,
canRevoke: callState.canManageCall
)),
initialInvite: .link(link: inviteLinks.listenerLink, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: groupCall.accountContext.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil),
parentNavigationController: navigationController,
completed: { [weak self] result in
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
if let result {
switch result {
case .linkCopied:
//TODO:localize
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
return false
}), in: .current)
case .openCall:
break
}
}
}
)
self.environment?.controller()?.present(controller, in: .window(.root), with: nil)
return
}
let formatSendTitle: (String) -> String = { string in
var string = string
if string.contains("[") && string.contains("]") {

View File

@ -16,31 +16,6 @@ extension VideoChatScreenComponent.View {
return
}
/*if groupCall.accountContext.sharedContext.immediateExperimentalUISettings.conferenceDebug {
guard let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
return
}
var presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
let controller = InviteLinkInviteController(context: groupCall.accountContext, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), mode: .groupCall(link: "https://t.me/call/+abbfbffll123", isRecentlyCreated: false), parentNavigationController: navigationController, completed: { [weak self] result in
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
if let result {
switch result {
case .linkCopied:
//TODO:localize
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
return false
}), in: .current)
}
}
})
self.environment?.controller()?.present(controller, in: .window(.root), with: nil)
return
}*/
if groupCall.isConference {
var disablePeerIds: [EnginePeer.Id] = []
disablePeerIds.append(groupCall.accountContext.account.peerId)

View File

@ -93,13 +93,18 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
tags.insert(.webPage)
} else if let action = attachment as? TelegramMediaAction {
switch action.action {
case let .phoneCall(_, discardReason, _, _):
globalTags.insert(.Calls)
if incoming, let discardReason = discardReason, case .missed = discardReason {
globalTags.insert(.MissedCalls)
}
default:
break
case let .phoneCall(_, discardReason, _, _):
globalTags.insert(.Calls)
if incoming, let discardReason = discardReason, case .missed = discardReason {
globalTags.insert(.MissedCalls)
}
case let .conferenceCall(conferenceCall):
globalTags.insert(.Calls)
if incoming, conferenceCall.flags.contains(.isMissed) {
globalTags.insert(.MissedCalls)
}
default:
break
}
} else if let location = attachment as? TelegramMediaMap, location.liveBroadcastingTimeout != nil {
tags.insert(.liveLocation)
@ -118,9 +123,6 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
}
}
if !incoming {
assert(true)
}
return (tags, globalTags)
}

View File

@ -96,6 +96,7 @@ public struct GroupCallInfo: Equatable {
public var isVideoEnabled: Bool
public var unmutedVideoLimit: Int
public var isStream: Bool
public var isCreator: Bool
public init(
id: Int64,
@ -110,7 +111,8 @@ public struct GroupCallInfo: Equatable {
defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?,
isVideoEnabled: Bool,
unmutedVideoLimit: Int,
isStream: Bool
isStream: Bool,
isCreator: Bool
) {
self.id = id
self.accessHash = accessHash
@ -125,6 +127,7 @@ public struct GroupCallInfo: Equatable {
self.isVideoEnabled = isVideoEnabled
self.unmutedVideoLimit = unmutedVideoLimit
self.isStream = isStream
self.isCreator = isCreator
}
}
@ -150,7 +153,8 @@ extension GroupCallInfo {
defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: (flags & (1 << 1)) != 0, canChange: (flags & (1 << 2)) != 0),
isVideoEnabled: (flags & (1 << 9)) != 0,
unmutedVideoLimit: Int(unmutedVideoLimit),
isStream: (flags & (1 << 12)) != 0
isStream: (flags & (1 << 12)) != 0,
isCreator: (flags & (1 << 15)) != 0
)
case .groupCallDiscarded:
return nil
@ -454,17 +458,17 @@ public enum GetGroupCallParticipantsError {
func _internal_getGroupCallParticipants(account: Account, reference: InternalGroupCallReference, offset: String, ssrcs: [UInt32], limit: Int32, sortAscending: Bool?) -> Signal<GroupCallParticipantsContext.State, GetGroupCallParticipantsError> {
let accountPeerId = account.peerId
let sortAscendingValue: Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int, Bool), GetGroupCallParticipantsError>
let sortAscendingValue: Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int, Bool, Bool), GetGroupCallParticipantsError>
sortAscendingValue = _internal_getCurrentGroupCall(account: account, reference: reference)
|> mapError { _ -> GetGroupCallParticipantsError in
return .generic
}
|> mapToSignal { result -> Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int, Bool), GetGroupCallParticipantsError> in
|> mapToSignal { result -> Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int, Bool, Bool), GetGroupCallParticipantsError> in
guard let result = result else {
return .fail(.generic)
}
return .single((sortAscending ?? result.info.sortAscending, result.info.scheduleTimestamp, result.info.subscribedToScheduled, result.info.defaultParticipantsAreMuted, result.info.isVideoEnabled, result.info.unmutedVideoLimit, result.info.isStream))
return .single((sortAscending ?? result.info.sortAscending, result.info.scheduleTimestamp, result.info.subscribedToScheduled, result.info.defaultParticipantsAreMuted, result.info.isVideoEnabled, result.info.unmutedVideoLimit, result.info.isStream, result.info.isCreator))
}
return combineLatest(
@ -481,7 +485,7 @@ func _internal_getGroupCallParticipants(account: Account, reference: InternalGro
let version: Int32
let nextParticipantsFetchOffset: String?
let (sortAscendingValue, scheduleTimestamp, subscribedToScheduled, defaultParticipantsAreMuted, isVideoEnabled, unmutedVideoLimit, isStream) = sortAscendingAndScheduleTimestamp
let (sortAscendingValue, scheduleTimestamp, subscribedToScheduled, defaultParticipantsAreMuted, isVideoEnabled, unmutedVideoLimit, isStream, isCreator) = sortAscendingAndScheduleTimestamp
switch result {
case let .groupParticipants(count, participants, nextOffset, chats, users, apiVersion):
@ -506,7 +510,7 @@ func _internal_getGroupCallParticipants(account: Account, reference: InternalGro
participants: parsedParticipants,
nextParticipantsFetchOffset: nextParticipantsFetchOffset,
adminIds: Set(),
isCreator: false,
isCreator: isCreator,
defaultParticipantsAreMuted: defaultParticipantsAreMuted ?? GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false),
sortAscending: sortAscendingValue,
recordingStartTimestamp: nil,
@ -2959,6 +2963,29 @@ func _internal_createConferenceCall(postbox: Postbox, network: Network, accountP
}
}
public enum RevokeConferenceInviteLinkError {
case generic
}
func _internal_revokeConferenceInviteLink(account: Account, reference: InternalGroupCallReference, link: String) -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> {
return account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 1, call: reference.apiInputGroupCall, joinMuted: .boolFalse))
|> mapError { _ -> RevokeConferenceInviteLinkError in
return .generic
}
|> mapToSignal { result -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> in
account.stateManager.addUpdates(result)
return _internal_groupCallInviteLinks(account: account, reference: reference, isConference: true)
|> castError(RevokeConferenceInviteLinkError.self)
|> mapToSignal { result -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> in
guard let result = result else {
return .fail(.generic)
}
return .single(result)
}
}
}
public enum ConfirmAddConferenceParticipantError {
case generic
}

View File

@ -97,6 +97,10 @@ public extension TelegramEngine {
return _internal_createConferenceCall(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId)
}
public func revokeConferenceInviteLink(reference: InternalGroupCallReference, link: String) -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> {
return _internal_revokeConferenceInviteLink(account: self.account, reference: reference, link: link)
}
public func pollConferenceCallBlockchain(reference: InternalGroupCallReference, subChainId: Int, offset: Int, limit: Int) -> Signal<(blocks: [Data], nextOffset: Int)?, NoError> {
return _internal_pollConferenceCallBlockchain(network: self.account.network, reference: reference, subChainId: subChainId, offset: offset, limit: limit)
}

View File

@ -14124,7 +14124,7 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa
createInviteLinkImpl = { [weak contactsController] in
contactsController?.view.window?.endEditing(true)
contactsController?.present(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: groupPeer.id), parentNavigationController: contactsController?.navigationController as? NavigationController), in: .window(.root))
contactsController?.present(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: groupPeer.id), initialInvite: nil, parentNavigationController: contactsController?.navigationController as? NavigationController), in: .window(.root))
}
parentController?.push(contactsController)

View File

@ -402,6 +402,15 @@ func openResolvedUrlImpl(
members: resolvedCallLink.members,
totalMemberCount: resolvedCallLink.totalMemberCount
))))
}, error: { _ in
var elevatedLayout = true
if case .chat = urlContext {
elevatedLayout = false
}
//TODO:localize
present(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: "This link is no longer active"), elevatedLayout: elevatedLayout, animateInAsReplacement: false, action: { _ in
return true
}), nil)
})
case let .localization(identifier):
dismissInput()
@ -788,6 +797,7 @@ func openResolvedUrlImpl(
}
if let currentState = starsContext.currentState, currentState.balance >= StarsAmount(value: amount, nanos: 0) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let controller = UndoOverlayController(
presentationData: presentationData,
content: .universal(