diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4269334c82..36b7e04b97 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,6 +22,7 @@ internal: - export PATH=/opt/homebrew/opt/ruby/bin:$PATH - export PATH=`gem environment gemdir`/bin:$PATH - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appcenter-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64 + - python3 -u build-system/Make/DeployToFirebase.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/firebase-configurations/firebase-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: internal diff --git a/build-system/Make/BuildEnvironment.py b/build-system/Make/BuildEnvironment.py index 3f7311aa73..2cb64aae7b 100644 --- a/build-system/Make/BuildEnvironment.py +++ b/build-system/Make/BuildEnvironment.py @@ -11,25 +11,26 @@ def is_apple_silicon(): return False -def get_clean_env(): +def get_clean_env(use_clean_env=True): clean_env = os.environ.copy() - clean_env['PATH'] = '/usr/bin:/bin:/usr/sbin:/sbin' + if use_clean_env: + clean_env['PATH'] = '/usr/bin:/bin:/usr/sbin:/sbin' return clean_env -def resolve_executable(program): +def resolve_executable(program, use_clean_env=True): def is_executable(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - for path in get_clean_env()["PATH"].split(os.pathsep): + for path in get_clean_env(use_clean_env=use_clean_env)["PATH"].split(os.pathsep): executable_file = os.path.join(path, program) if is_executable(executable_file): return executable_file return None -def run_executable_with_output(path, arguments, decode=True, input=None, stderr_to_stdout=True, print_command=False, check_result=False): - executable_path = resolve_executable(path) +def run_executable_with_output(path, arguments, use_clean_env=True, decode=True, input=None, stderr_to_stdout=True, print_command=False, check_result=False): + executable_path = resolve_executable(path, use_clean_env=use_clean_env) if executable_path is None: raise Exception('Could not resolve {} to a valid executable file'.format(path)) @@ -45,7 +46,7 @@ def run_executable_with_output(path, arguments, decode=True, input=None, stderr_ stdout=subprocess.PIPE, stderr=stderr_assignment, stdin=subprocess.PIPE, - env=get_clean_env() + env=get_clean_env(use_clean_env=use_clean_env) ) if input is not None: output_data, _ = process.communicate(input=input) diff --git a/build-system/Make/DeployToFirebase.py b/build-system/Make/DeployToFirebase.py new file mode 100644 index 0000000000..e0dcc994a8 --- /dev/null +++ b/build-system/Make/DeployToFirebase.py @@ -0,0 +1,81 @@ +import os +import sys +import argparse +import json +import re + +from BuildEnvironment import run_executable_with_output + +def deploy_to_firebase(args): + if not os.path.exists(args.configuration): + print('{} does not exist'.format(args.configuration)) + sys.exit(1) + if not os.path.exists(args.ipa): + print('{} does not exist'.format(args.ipa)) + sys.exit(1) + if args.dsyms is not None and not os.path.exists(args.dsyms): + print('{} does not exist'.format(args.dsyms)) + sys.exit(1) + + with open(args.configuration) as file: + configuration_dict = json.load(file) + required_keys = [ + 'app_id', + 'group', + ] + for key in required_keys: + if key not in configuration_dict: + print('Configuration at {} does not contain {}'.format(args.configuration, key)) + sys.exit(1) + + firebase_arguments = [ + 'appdistribution:distribute', + '--app', configuration_dict['app_id'], + '--groups', configuration_dict['group'], + args.ipa + ] + + output = run_executable_with_output( + 'firebase', + firebase_arguments, + use_clean_env=False, + check_result=True + ) + + sharing_link_match = re.search(r'Share this release with testers who have access: (https://\S+)', output) + if sharing_link_match: + print(f"Sharing link: {sharing_link_match.group(1)}") + else: + print("No sharing link found in the output.") + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog='deploy-firebase') + + parser.add_argument( + '--configuration', + required=True, + help='Path to configuration json.' + ) + parser.add_argument( + '--ipa', + required=True, + help='Path to IPA.' + ) + parser.add_argument( + '--dsyms', + required=False, + help='Path to DSYMs.zip.' + ) + parser.add_argument( + '--debug', + action='store_true', + help='Enable debug output for firebase deploy.' + ) + + if len(sys.argv) < 2: + parser.print_help() + sys.exit(1) + + args = parser.parse_args() + + deploy_to_firebase(args) diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index 140a078ed1..191e464c78 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -111,10 +111,27 @@ public final class ContactSelectionControllerParams { public let requirePhoneNumbers: Bool public let allowChannelsInSearch: Bool public let confirmation: (ContactListPeer) -> Signal + public let isPeerEnabled: (ContactListPeer) -> Bool public let openProfile: ((EnginePeer) -> Void)? public let sendMessage: ((EnginePeer) -> Void)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactSelectionControllerMode = .generic, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, allowChannelsInSearch: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) { + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + mode: ContactSelectionControllerMode = .generic, + autoDismiss: Bool = true, + title: @escaping (PresentationStrings) -> String, + options: Signal<[ContactListAdditionalOption], NoError> = .single([]), + displayDeviceContacts: Bool = false, + displayCallIcons: Bool = false, + multipleSelection: Bool = false, + requirePhoneNumbers: Bool = false, + allowChannelsInSearch: Bool = false, + confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }, + isPeerEnabled: @escaping (ContactListPeer) -> Bool = { _ in true }, + openProfile: ((EnginePeer) -> Void)? = nil, + sendMessage: ((EnginePeer) -> Void)? = nil + ) { self.context = context self.updatedPresentationData = updatedPresentationData self.mode = mode @@ -127,6 +144,7 @@ public final class ContactSelectionControllerParams { self.requirePhoneNumbers = requirePhoneNumbers self.allowChannelsInSearch = allowChannelsInSearch self.confirmation = confirmation + self.isPeerEnabled = isPeerEnabled self.openProfile = openProfile self.sendMessage = sendMessage } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 96e424ad00..1939801d42 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -173,7 +173,7 @@ public protocol PresentationCall: AnyObject { func setCurrentAudioOutput(_ output: AudioSessionOutput) func debugInfo() -> Signal<(String, String), NoError> - func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable + func upgradeToConference(invitePeers: [(id: EnginePeer.Id, isVideo: Bool)], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) } @@ -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 } @@ -484,7 +485,7 @@ public protocol PresentationGroupCall: AnyObject { func updateTitle(_ title: String) - func invitePeer(_ peerId: EnginePeer.Id) -> Bool + func invitePeer(_ peerId: EnginePeer.Id, isVideo: Bool) -> Bool func removedPeer(_ peerId: EnginePeer.Id) var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> { get } @@ -550,10 +551,6 @@ public enum PresentationCurrentCall: Equatable { } } -public enum JoinConferenceCallMode { - case joining -} - public protocol PresentationCallManager: AnyObject { var currentCallSignal: Signal { get } var currentGroupCallSignal: Signal { get } @@ -568,6 +565,6 @@ public protocol PresentationCallManager: AnyObject { accountContext: AccountContext, initialCall: EngineGroupCallDescription, reference: InternalGroupCallReference, - mode: JoinConferenceCallMode + beginWithVideo: Bool ) } diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 5c71791f63..e03b3a67c7 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -297,18 +297,19 @@ public final class AvatarNode: ASDisplayNode { private struct Params: Equatable { let peerId: EnginePeer.Id? let resourceId: String? - let clipStyle: AvatarNodeClipStyle let displayDimensions: CGSize + let clipStyle: AvatarNodeClipStyle + init( peerId: EnginePeer.Id?, resourceId: String?, - clipStyle: AvatarNodeClipStyle, - displayDimensions: CGSize + displayDimensions: CGSize, + clipStyle: AvatarNodeClipStyle ) { self.peerId = peerId self.resourceId = resourceId - self.clipStyle = clipStyle self.displayDimensions = displayDimensions + self.clipStyle = clipStyle } } @@ -663,8 +664,8 @@ public final class AvatarNode: ASDisplayNode { let params = Params( peerId: peer?.id, resourceId: smallProfileImage?.resource.id.stringRepresentation, - clipStyle: clipStyle, - displayDimensions: displayDimensions + displayDimensions: displayDimensions, + clipStyle: clipStyle ) if self.params == params { return diff --git a/submodules/CallListUI/Sources/CallListCallItem.swift b/submodules/CallListUI/Sources/CallListCallItem.swift index dd8bd7bea0..e32ac96f5d 100644 --- a/submodules/CallListUI/Sources/CallListCallItem.swift +++ b/submodules/CallListUI/Sources/CallListCallItem.swift @@ -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 { diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index fa47ec33a6..ad70beba8c 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -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 { @@ -231,38 +238,55 @@ public final class CallListController: TelegramBaseController { isStream: false ), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), - mode: .joining + beginWithVideo: false ) } - 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 diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index e4e0c66218..867fc17494 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -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 @@ -519,10 +519,7 @@ final class CallListControllerNode: ASDisplayNode { let canCreateGroupCall = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { configuration -> Bool in - var isConferencePossible = false - if context.sharedContext.immediateExperimentalUISettings.conferenceDebug { - isConferencePossible = true - } + var isConferencePossible = true if let data = configuration.data, let value = data["ios_enable_conference"] as? Double { isConferencePossible = value != 0.0 } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 83f08dc062..79ce46c3fc 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -692,6 +692,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { public func updateIsHighlighted(transition: ContainedViewLayoutTransition) { var reallyHighlighted = self.isHighlighted + if let item = self.item, !item.enabled { + reallyHighlighted = false + } let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0 if let item = self.item { switch item.peer { @@ -1649,6 +1652,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { actionButtonNode.setImage(actionButton.image, for: .normal) transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionButtonImage.size.width - offset, y: floor((nodeLayout.contentSize.height - actionButtonImage.size.height) / 2.0)), size: actionButtonImage.size)) + actionButtonNode.isEnabled = item.enabled + actionButtonNode.alpha = item.enabled ? 1.0 : 0.4 + offset += actionButtonImage.size.width + 12.0 } } diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 6af1749af9..143f6d3439 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -100,7 +100,6 @@ private enum DebugControllerEntry: ItemListNodeEntry { case enableReactionOverrides(Bool) case compressedEmojiCache(Bool) case storiesJpegExperiment(Bool) - case conferenceDebug(Bool) case checkSerializedData(Bool) case enableQuickReactionSwitch(Bool) case disableReloginTokens(Bool) @@ -134,7 +133,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .conferenceDebug, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -243,8 +242,6 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 47 case .disableReloginTokens: return 48 - case .conferenceDebug: - return 49 case .checkSerializedData: return 50 case .enableQuickReactionSwitch: @@ -1311,16 +1308,6 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .conferenceDebug(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Conference Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in - let _ = arguments.sharedContext.accountManager.transaction ({ transaction in - transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in - var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings - settings.conferenceDebug = value - return PreferencesEntry(settings) - }) - }).start() - }) case let .checkSerializedData(value): return ItemListSwitchItem(presentationData: presentationData, title: "Check Serialized Data", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -1552,7 +1539,6 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.storiesJpegExperiment(experimentalSettings.storiesJpegExperiment)) entries.append(.disableReloginTokens(experimentalSettings.disableReloginTokens)) - entries.append(.conferenceDebug(experimentalSettings.conferenceDebug)) entries.append(.checkSerializedData(experimentalSettings.checkSerializedData)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) entries.append(.liveStreamV2(experimentalSettings.liveStreamV2)) diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index e10fadff69..ad03f72960 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -364,7 +364,7 @@ public final class TextNodeLayout: NSObject { fileprivate let backgroundColor: UIColor? fileprivate let constrainedSize: CGSize fileprivate let explicitAlignment: NSTextAlignment - fileprivate let resolvedAlignment: NSTextAlignment + public let resolvedAlignment: NSTextAlignment fileprivate let verticalAlignment: TextVerticalAlignment fileprivate let lineSpacing: CGFloat fileprivate let cutout: TextNodeCutout? diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index a93029cab4..b881c0d803 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -27,13 +27,15 @@ class InviteLinkInviteInteraction { let copyLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void let manageLinks: () -> Void + let openCallAction: () -> Void - init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void) { + init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void, openCallAction: @escaping () -> Void) { self.context = context self.mainLinkContextAction = mainLinkContextAction self.copyLink = copyLink self.shareLink = shareLink self.manageLinks = manageLinks + self.openCallAction = openCallAction } } @@ -131,6 +133,8 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable { }, contextAction: { node, gesture in interaction.mainLinkContextAction(invitation, node, gesture) }, viewAction: { + }, openCallAction: { + interaction.openCallAction() }) case let .manage(text, standalone): return InviteLinkInviteManageItem(theme: presentationData.theme, text: text, standalone: standalone, action: { @@ -150,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 { @@ -169,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 @@ -176,9 +199,10 @@ public final class InviteLinkInviteController: ViewController { fileprivate let completed: ((CompletionResult?) -> Void)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: Mode, parentNavigationController: NavigationController?, completed: ((CompletionResult?) -> Void)? = nil) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = 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 @@ -211,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 @@ -292,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 @@ -315,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] @@ -334,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) @@ -350,7 +374,7 @@ public final class InviteLinkInviteController: ViewController { self.backgroundColor = nil self.isOpaque = false - let mainInvitePromise = ValuePromise(nil) + let mainInvitePromise = ValuePromise(initialInvite) self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in guard let self else { @@ -359,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 @@ -377,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) - - guard let self else { - return - } - - if let invite = invite { + 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 + } + + 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 { @@ -400,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 @@ -450,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) @@ -546,6 +612,12 @@ public final class InviteLinkInviteController: ViewController { strongSelf.controller?.parentNavigationController?.pushViewController(controller) strongSelf.controller?.dismiss() } + }, openCallAction: { [weak self] in + guard let self else { + return + } + self.controller?.completed?(.openCall) + self.controller?.dismiss() }) let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil) @@ -587,15 +659,16 @@ public final class InviteLinkInviteController: ViewController { strongSelf.enqueueTransition(transition) } }) - case let .groupCall(link, isRecentlyCreated): - //TODO:release - let tempInfo: Signal = .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.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 } @@ -605,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) @@ -665,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))! diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 62acacd470..cf7cdf5aeb 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -229,6 +229,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { if let invite = invite { arguments.openLink(invite) } + }, openCallAction: { }) case let .mainLinkOtherInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: nil, style: .blocks, tag: nil) @@ -529,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) }) }))) @@ -718,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) } }) }))) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 17b5068a7b..bc5f73a1e7 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -271,6 +271,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { }, contextAction: invite.link?.hasSuffix("...") == true ? nil : { node, gesture in interaction.contextAction(invite, node, gesture) }, viewAction: { + }, openCallAction: { }) case let .subscriptionHeader(_, title): return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) @@ -754,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)) }) }))) } diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 6178e7cb6b..9c049ea141 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -14,6 +14,7 @@ import Markdown import TextFormat import ComponentFlow import MultilineTextComponent +import TextNodeWithEntities private func actionButtonImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in @@ -46,6 +47,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { let shareAction: (() -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? let viewAction: (() -> Void)? + let openCallAction: (() -> Void)? public let tag: ItemListItemTag? public init( @@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { shareAction: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, viewAction: (() -> Void)?, + openCallAction: (() -> Void)?, tag: ItemListItemTag? = nil ) { self.context = context @@ -83,6 +86,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { self.shareAction = shareAction self.contextAction = contextAction self.viewAction = viewAction + self.openCallAction = openCallAction self.tag = tag } @@ -147,7 +151,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem private var shimmerNode: ShimmerEffectNode? private var absoluteLocation: (CGRect, CGSize)? - private var justCreatedCallTextNode: TextNode? + private var justCreatedCallTextNode: TextNodeWithEntities? private var justCreatedCallLeftSeparatorLayer: SimpleLayer? private var justCreatedCallRightSeparatorLayer: SimpleLayer? private var justCreatedCallSeparatorText: ComponentView? @@ -299,7 +303,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem public func asyncLayout() -> (_ item: ItemListPermanentInviteLinkItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeAddressLayout = TextNode.asyncLayout(self.addressNode) let makeInvitedPeersLayout = TextNode.asyncLayout(self.invitedPeersNode) - let makeJustCreatedCallTextNodeLayout = TextNode.asyncLayout(self.justCreatedCallTextNode) + let makeJustCreatedCallTextNodeLayout = TextNodeWithEntities.asyncLayout(self.justCreatedCallTextNode) let currentItem = self.item let avatarsContext = self.avatarsContext @@ -343,7 +347,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem let (invitedPeersLayout, invitedPeersApply) = makeInvitedPeersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - var justCreatedCallTextNodeLayout: (TextNodeLayout, () -> TextNode?)? + var justCreatedCallTextNodeLayout: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities?)? if item.isCall { let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor) @@ -571,17 +575,39 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) if let justCreatedCallTextNodeLayout { - if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1() { + if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: .gray, + attemptSynchronous: true + )) { if strongSelf.justCreatedCallTextNode !== justCreatedCallTextNode { - strongSelf.justCreatedCallTextNode?.removeFromSupernode() + strongSelf.justCreatedCallTextNode?.textNode.removeFromSupernode() strongSelf.justCreatedCallTextNode = justCreatedCallTextNode - //justCreatedCallTextNode.highlig - - strongSelf.addSubnode(justCreatedCallTextNode) + strongSelf.addSubnode(justCreatedCallTextNode.textNode) } + + justCreatedCallTextNode.linkHighlightColor = item.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.1) + justCreatedCallTextNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + } + justCreatedCallTextNode.tapAttributeAction = { [weak strongSelf] attributes, _ in + guard let strongSelf else { + return + } + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + strongSelf.item?.openCallAction?() + } + } + let justCreatedCallTextNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - justCreatedCallTextNodeLayout.0.size.width) / 2.0), y: shareButtonNode.frame.maxY + justCreatedCallTextSpacing), size: CGSize(width: justCreatedCallTextNodeLayout.0.size.width, height: justCreatedCallTextNodeLayout.0.size.height)) - justCreatedCallTextNode.frame = justCreatedCallTextNodeFrame + justCreatedCallTextNode.textNode.frame = justCreatedCallTextNodeFrame let justCreatedCallSeparatorText: ComponentView if let current = strongSelf.justCreatedCallSeparatorText { @@ -636,7 +662,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem } } else if let justCreatedCallTextNode = strongSelf.justCreatedCallTextNode { strongSelf.justCreatedCallTextNode = nil - justCreatedCallTextNode.removeFromSupernode() + justCreatedCallTextNode.textNode.removeFromSupernode() strongSelf.justCreatedCallLeftSeparatorLayer?.removeFromSuperlayer() strongSelf.justCreatedCallLeftSeparatorLayer = nil diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 854b42cfc0..783f04db24 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -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() diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index dc6379afad..3f27534b60 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -655,6 +655,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { if let invite = invite { arguments.openLink(invite) } + }, openCallAction: { }) case let .editablePublicLink(theme, _, placeholder, currentText): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), clearType: .always, tag: ChannelVisibilityEntryTag.publicLink, sectionId: self.section, textUpdated: { updatedText in @@ -1608,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) }) } }) diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index d4568f4d8b..69e11b3c2e 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -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 diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index bca51c83ae..98cdf8daae 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1050,7 +1050,7 @@ private enum StatsEntry: ItemListNodeEntry { arguments.copyBoostLink(link) }, shareAction: { arguments.shareBoostLink(link) - }, contextAction: nil, viewAction: nil, tag: nil) + }, contextAction: nil, viewAction: nil, openCallAction: nil, tag: nil) case let .boostersPlaceholder(_, text): return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks) case let .boostGifts(theme, title): diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 0a4b863d02..05d00fde09 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -384,6 +384,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) } dict[-1945083841] = { return Api.InputGroupCall.parse_inputGroupCallInviteMessage($0) } dict[-33127873] = { return Api.InputGroupCall.parse_inputGroupCallSlug($0) } + dict[-191267262] = { return Api.InputInvoice.parse_inputInvoiceBusinessBotTransferStars($0) } dict[887591921] = { return Api.InputInvoice.parse_inputInvoiceChatInviteSubscription($0) } dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) } diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 4a461ad283..9634d92981 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -248,6 +248,7 @@ public extension Api { } public extension Api { indirect enum InputInvoice: TypeConstructorDescription { + case inputInvoiceBusinessBotTransferStars(bot: Api.InputUser, stars: Int64) case inputInvoiceChatInviteSubscription(hash: String) case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) @@ -260,6 +261,13 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .inputInvoiceBusinessBotTransferStars(let bot, let stars): + if boxed { + buffer.appendInt32(-191267262) + } + bot.serialize(buffer, true) + serializeInt64(stars, buffer: buffer, boxed: false) + break case .inputInvoiceChatInviteSubscription(let hash): if boxed { buffer.appendInt32(887591921) @@ -329,6 +337,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .inputInvoiceBusinessBotTransferStars(let bot, let stars): + return ("inputInvoiceBusinessBotTransferStars", [("bot", bot as Any), ("stars", stars as Any)]) case .inputInvoiceChatInviteSubscription(let hash): return ("inputInvoiceChatInviteSubscription", [("hash", hash as Any)]) case .inputInvoiceMessage(let peer, let msgId): @@ -350,6 +360,22 @@ public extension Api { } } + public static func parse_inputInvoiceBusinessBotTransferStars(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputUser? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputUser + } + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputInvoice.inputInvoiceBusinessBotTransferStars(bot: _1!, stars: _2!) + } + else { + return nil + } + } public static func parse_inputInvoiceChatInviteSubscription(_ reader: BufferReader) -> InputInvoice? { var _1: String? _1 = parseString(reader) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 7bb1a87e73..22f67ccbb2 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -10136,12 +10136,13 @@ public extension Api.functions.phone { } } public extension Api.functions.phone { - static func inviteConferenceCallParticipant(call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func inviteConferenceCallParticipant(flags: Int32, call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1050474478) + buffer.appendInt32(-1124981115) + serializeInt32(flags, buffer: buffer, boxed: false) call.serialize(buffer, true) userId.serialize(buffer, true) - return (FunctionDescription(name: "phone.inviteConferenceCallParticipant", parameters: [("call", String(describing: call)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "phone.inviteConferenceCallParticipant", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 020a0d321a..c667d45901 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -486,45 +486,86 @@ public final class CallController: ViewController { var disablePeerIds: [EnginePeer.Id] = [] disablePeerIds.append(self.call.context.account.peerId) disablePeerIds.append(self.call.peerId) - let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in + let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, shareLink: nil, completion: { [weak self] peers in guard let self else { return } - let _ = self.call.upgradeToConference(invitePeerIds: peerIds, completion: { _ in + let _ = self.call.upgradeToConference(invitePeers: peers, completion: { _ in }) }) self.push(controller) } - static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([EnginePeer.Id]) -> Void) -> ViewController { + static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], shareLink: (() -> Void)?, completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController { //TODO:localize - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams( + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) + + var options: [ContactListAdditionalOption] = [] + var openShareLinkImpl: (() -> Void)? + if shareLink != nil { + //TODO:localize + options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: { + openShareLinkImpl?() + }, clearHighlightAutomatically: false)) + } + + let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( context: context, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), - title: "Invite Members", - mode: .peerSelection(searchChatList: true, searchGroups: false, searchChannels: false), + mode: .generic, + title: { strings in + //TODO:localize + return "Add Member" + }, + options: .single(options), + displayCallIcons: true, + confirmation: { peer in + switch peer { + case let .peer(peer, _, _): + let peer = EnginePeer(peer) + guard case let .user(user) = peer else { + return .single(false) + } + if disablePeerIds.contains(user.id) { + return .single(false) + } + if user.botInfo != nil { + return .single(false) + } + return .single(true) + default: + return .single(false) + } + }, isPeerEnabled: { peer in - guard case let .user(user) = peer else { + switch peer { + case let .peer(peer, _, _): + let peer = EnginePeer(peer) + guard case let .user(user) = peer else { + return false + } + if disablePeerIds.contains(user.id) { + return false + } + if user.botInfo != nil { + return false + } + return true + default: return false } - if disablePeerIds.contains(user.id) { - return false - } - if user.botInfo != nil { - return false - } - return true } )) + + openShareLinkImpl = { [weak controller] in + controller?.dismiss() + shareLink?() + } + controller.navigationPresentation = .modal let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in - guard case let .result(peerIds, _) = result else { - controller?.dismiss() - return - } - if peerIds.isEmpty { + guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else { controller?.dismiss() return } @@ -533,15 +574,15 @@ public final class CallController: ViewController { controller?.dismiss() } - let invitePeerIds = peerIds.compactMap { item -> EnginePeer.Id? in - if case let .peer(peerId) = item { - return peerId - } else { - return nil - } + var isVideo = false + switch result.1 { + case .videoCall: + isVideo = true + default: + break } - completion(invitePeerIds) + completion([(peer.id, isVideo)]) }) return controller diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 5641bf8f89..9f5f9a24db 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -167,10 +167,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.conferenceAddParticipant?() } - var isConferencePossible = false - if self.call.context.sharedContext.immediateExperimentalUISettings.conferenceDebug { - isConferencePossible = true - } + var isConferencePossible = true if let data = self.call.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_enable_conference"] as? Double { isConferencePossible = value != 0.0 } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index d00ece5286..9d9207912c 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -234,6 +234,7 @@ public final class PresentationCallImpl: PresentationCall { public let peerId: EnginePeer.Id public let isOutgoing: Bool private let incomingConferenceSource: EngineMessage.Id? + private let conferenceStableId: Int64? public var isVideo: Bool public var isVideoPossible: Bool private let enableStunMarking: Bool @@ -368,7 +369,7 @@ public final class PresentationCallImpl: PresentationCall { return self.conferenceStatePromise.get() } - public private(set) var pendingInviteToConferencePeerIds: [EnginePeer.Id] = [] + public private(set) var pendingInviteToConferencePeerIds: [(id: EnginePeer.Id, isVideo: Bool)] = [] private var localVideoEndpointId: String? private var remoteVideoEndpointId: String? @@ -423,6 +424,11 @@ public final class PresentationCallImpl: PresentationCall { self.peerId = peerId self.isOutgoing = isOutgoing self.incomingConferenceSource = incomingConferenceSource + if let _ = incomingConferenceSource { + self.conferenceStableId = Int64.random(in: Int64.min ..< Int64.max) + } else { + self.conferenceStableId = nil + } self.isVideo = initialState?.type == .video self.isVideoPossible = isVideoPossible self.enableStunMarking = enableStunMarking @@ -445,19 +451,67 @@ public final class PresentationCallImpl: PresentationCall { var didReceiveAudioOutputs = false - var callSessionState: Signal = .complete() - if let initialState = initialState { - callSessionState = .single(initialState) - } - callSessionState = callSessionState - |> then(callSessionManager.callState(internalId: internalId)) - - self.sessionStateDisposable = (callSessionState - |> deliverOnMainQueue).start(next: { [weak self] sessionState in - if let strongSelf = self { - strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl) + if let incomingConferenceSource = incomingConferenceSource { + self.sessionStateDisposable = (context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Messages.Message(id: incomingConferenceSource) + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] message in + guard let self else { + return + } + + let state: CallSessionState + if let message = message { + var foundAction: TelegramMediaAction? + for media in message.media { + if let action = media as? TelegramMediaAction { + foundAction = action + break + } + } + + if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action { + if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil { + state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) + } else { + state = .ringing + } + } else { + state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) + } + } else { + state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) + } + + self.updateSessionState( + sessionState: CallSession( + id: self.internalId, + stableId: self.conferenceStableId, + isOutgoing: false, + type: self.isVideo ? .video : .audio, + state: state, + isVideoPossible: true + ), + callContextState: nil, + reception: nil, + audioSessionControl: self.audioSessionControl + ) + }) + } else { + var callSessionState: Signal = .complete() + if let initialState = initialState { + callSessionState = .single(initialState) } - }) + callSessionState = callSessionState + |> then(callSessionManager.callState(internalId: internalId)) + + self.sessionStateDisposable = (callSessionState + |> deliverOnMainQueue).start(next: { [weak self] sessionState in + if let strongSelf = self { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl) + } + }) + } if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_call_device"] { self.sharedAudioContext = nil @@ -933,15 +987,20 @@ public final class PresentationCallImpl: PresentationCall { } let keyPair: TelegramKeyPair? = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() guard let keyPair, let groupCall else { - self.updateSessionState(sessionState: CallSession( - id: self.internalId, - stableId: nil, - isOutgoing: false, - type: .audio, - state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), - isVideoPossible: true - ), - callContextState: nil, reception: nil, audioSessionControl: self.audioSessionControl) + self.sessionStateDisposable?.dispose() + self.updateSessionState( + sessionState: CallSession( + id: self.internalId, + stableId: self.conferenceStableId, + isOutgoing: false, + type: self.isVideo ? .video : .audio, + state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), + isVideoPossible: true + ), + callContextState: nil, + reception: nil, + audioSessionControl: self.audioSessionControl + ) return } @@ -967,14 +1026,15 @@ public final class PresentationCallImpl: PresentationCall { keyPair: keyPair, conferenceSourceId: self.internalId, isConference: true, + beginWithVideo: false, sharedAudioContext: self.sharedAudioContext ) self.conferenceCallImpl = conferenceCall conferenceCall.upgradedConferenceCall = self conferenceCall.setConferenceInvitedPeers(self.pendingInviteToConferencePeerIds) - for peerId in self.pendingInviteToConferencePeerIds { - let _ = conferenceCall.invitePeer(peerId) + for (peerId, isVideo) in self.pendingInviteToConferencePeerIds { + let _ = conferenceCall.invitePeer(peerId, isVideo: isVideo) } conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted) @@ -1067,9 +1127,10 @@ public final class PresentationCallImpl: PresentationCall { guard let self else { return } + self.sessionStateDisposable?.dispose() self.updateSessionState(sessionState: CallSession( id: self.internalId, - stableId: nil, + stableId: self.conferenceStableId, isOutgoing: false, type: .audio, state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), @@ -1341,11 +1402,12 @@ public final class PresentationCallImpl: PresentationCall { if strongSelf.incomingConferenceSource != nil { strongSelf.conferenceStateValue = .preparing strongSelf.isAcceptingIncomingConference = true + strongSelf.sessionStateDisposable?.dispose() strongSelf.updateSessionState(sessionState: CallSession( id: strongSelf.internalId, - stableId: nil, + stableId: strongSelf.conferenceStableId, isOutgoing: false, - type: .audio, + type: strongSelf.isVideo ? .video : .audio, state: .ringing, isVideoPossible: true ), @@ -1365,9 +1427,10 @@ public final class PresentationCallImpl: PresentationCall { if strongSelf.incomingConferenceSource != nil { strongSelf.conferenceStateValue = .preparing strongSelf.isAcceptingIncomingConference = true + strongSelf.sessionStateDisposable?.dispose() strongSelf.updateSessionState(sessionState: CallSession( id: strongSelf.internalId, - stableId: nil, + stableId: strongSelf.conferenceStableId, isOutgoing: false, type: .audio, state: .ringing, @@ -1552,7 +1615,7 @@ public final class PresentationCallImpl: PresentationCall { self.videoCapturer?.setIsVideoEnabled(!isPaused) } - public func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable { + public func upgradeToConference(invitePeers: [(id: EnginePeer.Id, isVideo: Bool)], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable { if self.isMovedToConference { return EmptyDisposable } @@ -1561,7 +1624,7 @@ public final class PresentationCallImpl: PresentationCall { return EmptyDisposable } - self.pendingInviteToConferencePeerIds = invitePeerIds + self.pendingInviteToConferencePeerIds = invitePeers let index = self.upgradedToConferenceCompletions.add({ call in completion(call) }) diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 31031d7e22..7d3b79b3fd 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -850,6 +850,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { keyPair: nil, conferenceSourceId: nil, isConference: false, + beginWithVideo: false, sharedAudioContext: nil ) call.schedule(timestamp: timestamp) @@ -1076,6 +1077,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { keyPair: nil, conferenceSourceId: nil, isConference: false, + beginWithVideo: false, sharedAudioContext: nil ) self.updateCurrentGroupCall(.group(call)) @@ -1085,16 +1087,13 @@ public final class PresentationCallManagerImpl: PresentationCallManager { accountContext: AccountContext, initialCall: EngineGroupCallDescription, reference: InternalGroupCallReference, - mode: JoinConferenceCallMode + beginWithVideo: Bool ) { let keyPair: TelegramKeyPair - switch mode { - case .joining: - guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else { - return - } - keyPair = keyPairValue + guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else { + return } + keyPair = keyPairValue let call = PresentationGroupCallImpl( accountContext: accountContext, @@ -1111,6 +1110,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { keyPair: keyPair, conferenceSourceId: nil, isConference: true, + beginWithVideo: beginWithVideo, sharedAudioContext: nil ) self.updateCurrentGroupCall(.group(call)) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 1b00f4fe9f..b3d7a70f3b 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -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, @@ -621,54 +622,98 @@ private final class PendingConferenceInvitationContext { case ringing } - private let callSessionManager: CallSessionManager + private let engine: TelegramEngine private var requestDisposable: Disposable? private var stateDisposable: Disposable? - private var internalId: CallSessionInternalId? + private(set) var messageId: EngineMessage.Id? + private var hadMessage: Bool = false private var didNotifyEnded: Bool = false - init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, peerId: PeerId, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) { - self.callSessionManager = callSessionManager - - preconditionFailure() - - /*self.requestDisposable = (callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: (groupCall, encryptionKey)) - |> deliverOnMainQueue).startStrict(next: { [weak self] internalId in + init(engine: TelegramEngine, reference: InternalGroupCallReference, peerId: PeerId, isVideo: Bool, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) { + self.engine = engine + self.requestDisposable = (engine.calls.inviteConferenceCallParticipant(reference: reference, peerId: peerId, isVideo: isVideo).startStrict(next: { [weak self] messageId in guard let self else { return } - self.internalId = internalId + guard let messageId else { + if !self.didNotifyEnded { + self.didNotifyEnded = true + onEnded(false) + } + return + } + self.messageId = messageId - self.stateDisposable = (self.callSessionManager.callState(internalId: internalId) - |> deliverOnMainQueue).startStrict(next: { [weak self] state in + onStateUpdated(.ringing) + + let timeout: Double = 30.0 + let timerSignal = Signal.single(Void()) |> then( + Signal.single(Void()) + |> delay(1.0, queue: .mainQueue()) + ) |> restart + + let startTime = CFAbsoluteTimeGetCurrent() + self.stateDisposable = (combineLatest(queue: .mainQueue(), + engine.data.subscribe( + TelegramEngine.EngineData.Item.Messages.Message(id: messageId) + ), + timerSignal + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] message, _ in guard let self else { return } - switch state.state { - case let .requesting(ringing, _): - if ringing { - onStateUpdated(.ringing) + if let message { + self.hadMessage = true + if message.timestamp + Int32(timeout) <= Int32(Date().timeIntervalSince1970) { + if !self.didNotifyEnded { + self.didNotifyEnded = true + onEnded(false) + } + } else { + var isActive = false + var isAccepted = false + var foundAction: TelegramMediaAction? + for media in message.media { + if let action = media as? TelegramMediaAction { + foundAction = action + break + } + } + + if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action { + if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil { + } else { + if conferenceCall.flags.contains(.isActive) { + isAccepted = true + } else { + isActive = true + } + } + } + if !isActive { + if !self.didNotifyEnded { + self.didNotifyEnded = true + onEnded(isAccepted) + } + } } - case let .dropping(reason), let .terminated(_, reason, _): - if !self.didNotifyEnded { - self.didNotifyEnded = true - onEnded(reason == .ended(.switchedToConference)) + } else { + if self.hadMessage || CFAbsoluteTimeGetCurrent() > startTime + 1.0 { + if !self.didNotifyEnded { + self.didNotifyEnded = true + onEnded(false) + } } - default: - break } }) - })*/ + })) } deinit { self.requestDisposable?.dispose() self.stateDisposable?.dispose() - - if let internalId = self.internalId { - self.callSessionManager.drop(internalId: internalId, reason: .hangUp, debugLog: .single(nil)) - } } } @@ -844,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 @@ -1121,6 +1167,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private let sharedAudioContext: SharedCallAudioContext? public let isConference: Bool + private let beginWithVideo: Bool private let conferenceSourceId: CallSessionInternalId? public var conferenceSource: CallSessionInternalId? { @@ -1153,6 +1200,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { keyPair: TelegramKeyPair?, conferenceSourceId: CallSessionInternalId?, isConference: Bool, + beginWithVideo: Bool, sharedAudioContext: SharedCallAudioContext? ) { self.account = accountContext.account @@ -1162,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 @@ -1183,6 +1232,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.isStream = isStream self.conferenceSourceId = conferenceSourceId self.isConference = isConference + self.beginWithVideo = beginWithVideo self.keyPair = keyPair if let keyPair, let initialCall { @@ -1490,6 +1540,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath) })*/ + + if beginWithVideo { + self.requestVideo() + } } deinit { @@ -1912,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( @@ -1928,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( @@ -2250,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] { @@ -2859,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( @@ -2937,6 +2997,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { result.append(OngoingGroupCallContext.MediaChannelDescription( kind: .audio, + peerId: participant.peer.id.id._internalGetInt64Value(), audioSsrc: audioSsrc, videoDescription: nil )) @@ -2948,6 +3009,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { result.append(OngoingGroupCallContext.MediaChannelDescription( kind: .audio, + peerId: participant.peer.id.id._internalGetInt64Value(), audioSsrc: screencastSsrc, videoDescription: nil )) @@ -3838,28 +3900,23 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { })) } - public func invitePeer(_ peerId: PeerId) -> Bool { + public func invitePeer(_ peerId: PeerId, isVideo: Bool) -> Bool { if self.isConference { guard let initialCall = self.initialCall else { return false } - //TODO:release - let _ = self.accountContext.engine.calls.inviteConferenceCallParticipant(callId: initialCall.description.id, accessHash: initialCall.description.accessHash, peerId: peerId).start() - return false - /*guard let initialCall = self.initialCall else { - return false - } - if conferenceInvitationContexts[peerId] != nil { + if self.conferenceInvitationContexts[peerId] != nil { return false } var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)? var onEnded: ((Bool) -> Void)? var didEndAlready = false let invitationContext = PendingConferenceInvitationContext( - callSessionManager: self.accountContext.account.callSessionManager, - groupCall: GroupCallReference(id: initialCall.id, accessHash: initialCall.accessHash), + engine: self.accountContext.engine, + reference: initialCall.reference, peerId: peerId, + isVideo: isVideo, onStateUpdated: { state in onStateUpdated?(state) }, @@ -3906,7 +3963,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - return false*/ + return false } else { guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else { return false @@ -3922,7 +3979,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - func setConferenceInvitedPeers(_ peerIds: [PeerId]) { + func setConferenceInvitedPeers(_ invitedPeers: [(id: PeerId, isVideo: Bool)]) { //TODO:release /*self.invitedPeersValue = peerIds.map { PresentationGroupCallInvitedPeer(id: $0, state: .requesting) @@ -3933,6 +3990,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var updatedInvitedPeers = self.invitedPeersValue updatedInvitedPeers.removeAll(where: { $0.id == peerId}) self.invitedPeersValue = updatedInvitedPeers + + if let conferenceInvitationContext = self.conferenceInvitationContexts[peerId] { + self.conferenceInvitationContexts.removeValue(forKey: peerId) + if let messageId = conferenceInvitationContext.messageId { + self.accountContext.engine.account.callSessionManager.dropOutgoingConferenceRequest(messageId: messageId) + } + } } public func updateTitle(_ title: String) { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift index 73cef654bd..7b416571c0 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift @@ -5,6 +5,179 @@ import ComponentFlow import MultilineTextComponent import BalancedTextComponent import TelegramPresentationData +import CallsEmoji + +private final class EmojiItemComponent: Component { + let emoji: String? + + init(emoji: String?) { + self.emoji = emoji + } + + static func ==(lhs: EmojiItemComponent, rhs: EmojiItemComponent) -> Bool { + if lhs.emoji != rhs.emoji { + return false + } + return true + } + + final class View: UIView { + private let measureEmojiView = ComponentView() + private var pendingContainerView: UIView? + private var pendingEmojiViews: [ComponentView] = [] + private var emojiView: ComponentView? + + private var component: EmojiItemComponent? + private weak var state: EmptyComponentState? + + private var pendingEmojiValues: [String]? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func update(component: EmojiItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let size = self.measureEmojiView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "👍", font: Font.regular(40.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + + let borderEmoji = 2 + let numEmoji = borderEmoji * 2 + 3 + + if let emoji = component.emoji { + let emojiView: ComponentView + var emojiViewTransition = transition + if let current = self.emojiView { + emojiView = current + } else { + emojiViewTransition = .immediate + emojiView = ComponentView() + self.emojiView = emojiView + } + let emojiSize = emojiView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: emoji, font: Font.regular(40.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + let emojiFrame = CGRect(origin: CGPoint(x: floor((size.width - emojiSize.width) * 0.5), y: floor((size.height - emojiSize.height) * 0.5)), size: emojiSize) + if let emojiComponentView = emojiView.view { + if emojiComponentView.superview == nil { + self.addSubview(emojiComponentView) + } + emojiViewTransition.setFrame(view: emojiComponentView, frame: emojiFrame) + } + + self.pendingEmojiValues = nil + } else { + if let emojiView = self.emojiView { + self.emojiView = nil + emojiView.view?.removeFromSuperview() + } + + if self.pendingEmojiValues?.count != numEmoji { + var pendingEmojiValuesValue: [String] = [] + for _ in 0 ..< numEmoji - borderEmoji - 1 { + pendingEmojiValuesValue.append(randomCallsEmoji() ?? "👍") + } + for i in 0 ..< borderEmoji + 1 { + pendingEmojiValuesValue.append(pendingEmojiValuesValue[i]) + } + self.pendingEmojiValues = pendingEmojiValuesValue + } + } + + if let pendingEmojiValues, pendingEmojiValues.count == numEmoji { + let pendingContainerView: UIView + if let current = self.pendingContainerView { + pendingContainerView = current + } else { + pendingContainerView = UIView() + self.pendingContainerView = pendingContainerView + } + + for i in 0 ..< numEmoji { + let pendingEmojiView: ComponentView + if self.pendingEmojiViews.count > i { + pendingEmojiView = self.pendingEmojiViews[i] + } else { + pendingEmojiView = ComponentView() + self.pendingEmojiViews.append(pendingEmojiView) + } + let pendingEmojiViewSize = pendingEmojiView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: pendingEmojiValues[i], font: Font.regular(40.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + if let pendingEmojiComponentView = pendingEmojiView.view { + if pendingEmojiComponentView.superview == nil { + pendingContainerView.addSubview(pendingEmojiComponentView) + } + pendingEmojiComponentView.frame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(i) * size.height), size: pendingEmojiViewSize) + } + } + + pendingContainerView.frame = CGRect(origin: CGPoint(), size: size) + + if pendingContainerView.superview == nil { + self.addSubview(pendingContainerView) + + let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y") + //animation.duration = 4.2 + animation.duration = 0.2 + animation.fromValue = -CGFloat(numEmoji - borderEmoji) * size.height + animation.toValue = CGFloat(borderEmoji - 3) * size.height + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.autoreverses = false + animation.repeatCount = .infinity + + pendingContainerView.layer.add(animation, forKey: "offsetCycle") + } + } else if let pendingContainerView = self.pendingContainerView { + self.pendingContainerView = nil + pendingContainerView.removeFromSuperview() + + for emojiView in self.pendingEmojiViews { + emojiView.view?.removeFromSuperview() + } + self.pendingEmojiViews.removeAll() + } + + //self.layer.borderColor = UIColor.red.cgColor + //self.layer.borderWidth = 4.0 + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} final class VideoChatEncryptionKeyComponent: Component { let theme: PresentationTheme @@ -119,7 +292,7 @@ final class VideoChatEncryptionKeyComponent: Component { let expandedButtonTopInset: CGFloat = 12.0 let expandedButtonBottomInset: CGFloat = 13.0 - let emojiItemSizes = (0 ..< component.emoji.count).map { i -> CGSize in + let emojiItemSizes = (0 ..< 4).map { i -> CGSize in let emojiItem: ComponentView if self.emojiItems.count > i { emojiItem = self.emojiItems[i] @@ -128,9 +301,9 @@ final class VideoChatEncryptionKeyComponent: Component { self.emojiItems.append(emojiItem) } return emojiItem.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.emoji[i], font: Font.regular(40.0), textColor: .white)) + transition: transition, + component: AnyComponent(EmojiItemComponent( + emoji: i < component.emoji.count ? component.emoji[i] : nil )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift index 5e25476df2..8e089cec0f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift @@ -7,16 +7,24 @@ import TelegramPresentationData import BundleIconComponent final class VideoChatListInviteComponent: Component { + enum Icon { + case addUser + case link + } + let title: String + let icon: Icon let theme: PresentationTheme let action: () -> Void init( title: String, + icon: Icon, theme: PresentationTheme, action: @escaping () -> Void ) { self.title = title + self.icon = icon self.theme = theme self.action = action } @@ -25,6 +33,9 @@ final class VideoChatListInviteComponent: Component { if lhs.title != rhs.title { return false } + if lhs.icon != rhs.icon { + return false + } if lhs.theme !== rhs.theme { return false } @@ -116,10 +127,17 @@ final class VideoChatListInviteComponent: Component { titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } + let iconName: String + switch component.icon { + case .addUser: + iconName = "Chat/Context Menu/AddUser" + case .link: + iconName = "Chat/Context Menu/Link" + } let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(BundleIconComponent( - name: "Chat/Context Menu/AddUser", + name: iconName, tintColor: component.theme.list.itemAccentColor )), environment: {}, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 9a1bd9f641..a8ce722e88 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -39,23 +39,33 @@ final class VideoChatParticipantsComponent: Component { } final class Participants: Equatable { - enum InviteType { - case invite + enum InviteType: Equatable { + case invite(isMultipleUsers: Bool) case shareLink } + struct InviteOption: Equatable { + let id: Int + let type: InviteType + + init(id: Int, type: InviteType) { + self.id = id + self.type = type + } + } + let myPeerId: EnginePeer.Id let participants: [GroupCallParticipantsContext.Participant] let totalCount: Int let loadMoreToken: String? - let inviteType: InviteType? + let inviteOptions: [InviteOption] - init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteType: InviteType?) { + init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteOptions: [InviteOption]) { self.myPeerId = myPeerId self.participants = participants self.totalCount = totalCount self.loadMoreToken = loadMoreToken - self.inviteType = inviteType + self.inviteOptions = inviteOptions } static func ==(lhs: Participants, rhs: Participants) -> Bool { @@ -74,7 +84,7 @@ final class VideoChatParticipantsComponent: Component { if lhs.loadMoreToken != rhs.loadMoreToken { return false } - if lhs.inviteType != rhs.inviteType { + if lhs.inviteOptions != rhs.inviteOptions { return false } return true @@ -142,7 +152,7 @@ final class VideoChatParticipantsComponent: Component { let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void let updateIsMainParticipantPinned: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void - let openInviteMembers: () -> Void + let openInviteMembers: (Participants.InviteType) -> Void let visibleParticipantsUpdated: (Set) -> Void init( @@ -162,7 +172,7 @@ final class VideoChatParticipantsComponent: Component { updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void, updateIsExpandedUIHidden: @escaping (Bool) -> Void, - openInviteMembers: @escaping () -> Void, + openInviteMembers: @escaping (Participants.InviteType) -> Void, visibleParticipantsUpdated: @escaping (Set) -> Void ) { self.call = call @@ -379,14 +389,14 @@ final class VideoChatParticipantsComponent: Component { let sideInset: CGFloat let itemCount: Int let itemHeight: CGFloat - let trailingItemHeight: CGFloat + let trailingItemHeights: [CGFloat] - init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeight: CGFloat) { + init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeights: [CGFloat]) { self.containerSize = containerSize self.sideInset = sideInset self.itemCount = itemCount self.itemHeight = itemHeight - self.trailingItemHeight = trailingItemHeight + self.trailingItemHeights = trailingItemHeights } func frame(at index: Int) -> CGRect { @@ -394,8 +404,15 @@ final class VideoChatParticipantsComponent: Component { return frame } - func trailingItemFrame() -> CGRect { - return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeight)) + func trailingItemFrame(index: Int) -> CGRect { + if index < 0 || index >= self.trailingItemHeights.count { + return CGRect() + } + var prefixHeight: CGFloat = 0.0 + for i in 0 ..< index { + prefixHeight += self.trailingItemHeights[i] + } + return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight + prefixHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeights[index])) } func contentHeight() -> CGFloat { @@ -403,7 +420,9 @@ final class VideoChatParticipantsComponent: Component { if self.itemCount != 0 { result = self.frame(at: self.itemCount - 1).maxY } - result += self.trailingItemHeight + for height in self.trailingItemHeights { + result += height + } return result } @@ -439,7 +458,7 @@ final class VideoChatParticipantsComponent: Component { let scrollClippingFrame: CGRect let separateVideoScrollClippingFrame: CGRect - init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) { + init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeights: [CGFloat]) { self.containerSize = containerSize self.layout = layout self.isUIHidden = isUIHidden @@ -465,7 +484,7 @@ final class VideoChatParticipantsComponent: Component { } self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil) - self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight) + self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeights: listTrailingItemHeights) self.spacing = 4.0 if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden { @@ -568,8 +587,8 @@ final class VideoChatParticipantsComponent: Component { } } - func listTrailingItemFrame() -> CGRect { - return self.list.trailingItemFrame() + func listTrailingItemFrame(index: Int) -> CGRect { + return self.list.trailingItemFrame(index: index) } } @@ -641,7 +660,7 @@ final class VideoChatParticipantsComponent: Component { private var listParticipants: [GroupCallParticipantsContext.Participant] = [] private let measureListItemView = ComponentView() - private let inviteListItemView = ComponentView() + private var inviteListItemViews: [Int: ComponentView] = [:] private var gridItemViews: [VideoParticipantKey: GridItem] = [:] private let gridItemViewContainer: UIView @@ -1270,7 +1289,7 @@ final class VideoChatParticipantsComponent: Component { case .requesting: subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral) case .ringing: - subtitle = PeerListItemComponent.Subtitle(text: "ringing...", color: .neutral) + subtitle = PeerListItemComponent.Subtitle(text: "invited", color: .neutral) } peerItemComponent = PeerListItemComponent( @@ -1381,11 +1400,15 @@ final class VideoChatParticipantsComponent: Component { self.listItemViews.removeValue(forKey: itemId) } - do { + var trailingItemIndex = 0 + for inviteOption in component.participants?.inviteOptions ?? [] { + guard let itemView = self.inviteListItemViews[inviteOption.id] else { + continue + } var itemTransition = transition - let itemView = self.inviteListItemView - let itemFrame = itemLayout.listTrailingItemFrame() + let itemFrame = itemLayout.listTrailingItemFrame(index: trailingItemIndex) + trailingItemIndex += 1 if let itemComponentView = itemView.view { if itemComponentView.superview == nil { @@ -1395,6 +1418,17 @@ final class VideoChatParticipantsComponent: Component { itemTransition.setFrame(view: itemComponentView, frame: itemFrame) } } + var removeInviteListItemIds: [Int] = [] + for (id, itemView) in self.inviteListItemViews { + if let participants = component.participants, participants.inviteOptions.contains(where: { $0.id == id }) { + } else { + removeInviteListItemIds.append(id) + itemView.view?.removeFromSuperview() + } + } + for id in removeInviteListItemIds { + self.inviteListItemViews.removeValue(forKey: id) + } transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0) transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY)) @@ -1748,32 +1782,51 @@ final class VideoChatParticipantsComponent: Component { containerSize: CGSize(width: availableSize.width, height: 1000.0) ) - let inviteText: String - if let participants = component.participants, let inviteType = participants.inviteType { - switch inviteType { - case .invite: - inviteText = component.strings.VoiceChat_InviteMember + var inviteListItemSizes: [CGSize] = [] + for (inviteOption) in component.participants?.inviteOptions ?? [] { + let inviteText: String + let iconType: VideoChatListInviteComponent.Icon + switch inviteOption.type { + case let .invite(isMultiple): + //TODO:localize + if isMultiple { + inviteText = component.strings.VoiceChat_InviteMember + } else { + inviteText = "Add Member" + } + iconType = .addUser case .shareLink: inviteText = component.strings.VoiceChat_Share + iconType = .link } - } else { - inviteText = component.strings.VoiceChat_InviteMember - } - let inviteListItemSize = self.inviteListItemView.update( - transition: transition, - component: AnyComponent(VideoChatListInviteComponent( - title: inviteText, - theme: component.theme, - action: { [weak self] in - guard let self, let component = self.component else { - return + + let inviteListItemView: ComponentView + var inviteListItemTransition = transition + if let current = self.inviteListItemViews[inviteOption.id] { + inviteListItemView = current + } else { + inviteListItemView = ComponentView() + self.inviteListItemViews[inviteOption.id] = inviteListItemView + inviteListItemTransition = inviteListItemTransition.withAnimation(.none) + } + + inviteListItemSizes.append(inviteListItemView.update( + transition: inviteListItemTransition, + component: AnyComponent(VideoChatListInviteComponent( + title: inviteText, + icon: iconType, + theme: component.theme, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.openInviteMembers(inviteOption.type) } - component.openInviteMembers() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 1000.0) - ) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + )) + } var gridParticipants: [VideoParticipant] = [] var listParticipants: [GroupCallParticipantsContext.Participant] = [] @@ -1824,7 +1877,7 @@ final class VideoChatParticipantsComponent: Component { gridItemCount: gridParticipants.count, listItemCount: listParticipants.count + component.invitedPeers.count, listItemHeight: measureListItemSize.height, - listTrailingItemHeight: inviteListItemSize.height + listTrailingItemHeights: inviteListItemSizes.map(\.height) ) self.itemLayout = itemLayout diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index d31b49c950..6e89966067 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -25,6 +25,7 @@ import LegacyComponents import TooltipUI import BlurredBackgroundComponent import CallsEmoji +import InviteLinksUI extension VideoChatCall { var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> { @@ -652,6 +653,51 @@ final class VideoChatScreenComponent: Component { guard case let .group(groupCall) = self.currentCall else { 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 @@ -705,7 +751,7 @@ final class VideoChatScreenComponent: Component { peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) ) ) - |> deliverOnMainQueue).start(next: { [weak self] peerList in + |> deliverOnMainQueue).start(next: { [weak self] peerList in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } @@ -1051,7 +1097,7 @@ final class VideoChatScreenComponent: Component { static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [InvitedPeer]), NoError> { let invitedPeers = conferenceSource.context.engine.data.subscribe( - EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0) }) + EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0.id) }) ) let accountPeerId = conferenceSource.context.account.peerId @@ -1759,12 +1805,19 @@ final class VideoChatScreenComponent: Component { } } } - var inviteType: VideoChatParticipantsComponent.Participants.InviteType? - if canInvite { - if inviteIsLink { - inviteType = .shareLink - } else { - inviteType = .invite + var inviteOptions: [VideoChatParticipantsComponent.Participants.InviteOption] = [] + if case let .group(groupCall) = self.currentCall, groupCall.isConference { + inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: .invite(isMultipleUsers: false))) + inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 1, type: .shareLink)) + } else { + if canInvite { + let inviteType: VideoChatParticipantsComponent.Participants.InviteType + if inviteIsLink { + inviteType = .shareLink + } else { + inviteType = .invite(isMultipleUsers: false) + } + inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: inviteType)) } } @@ -1773,7 +1826,7 @@ final class VideoChatScreenComponent: Component { participants: members.participants, totalCount: members.totalCount, loadMoreToken: members.loadMoreToken, - inviteType: inviteType + inviteOptions: inviteOptions ) } @@ -2038,7 +2091,13 @@ final class VideoChatScreenComponent: Component { } var encryptionKeyFrame: CGRect? - if let encryptionKeyEmoji = self.encryptionKeyEmoji { + var isConference = false + if case let .group(groupCall) = self.currentCall { + isConference = groupCall.isConference + } else if case .conferenceSource = self.currentCall { + isConference = true + } + if isConference { navigationHeight -= 2.0 let encryptionKey: ComponentView var encryptionKeyTransition = transition @@ -2055,7 +2114,7 @@ final class VideoChatScreenComponent: Component { component: AnyComponent(VideoChatEncryptionKeyComponent( theme: environment.theme, strings: environment.strings, - emoji: encryptionKeyEmoji, + emoji: self.encryptionKeyEmoji ?? [], isExpanded: self.isEncryptionKeyExpanded, tapAction: { [weak self] in guard let self else { @@ -2326,11 +2385,18 @@ final class VideoChatScreenComponent: Component { self.state?.updated(transition: .spring(duration: 0.4)) } }, - openInviteMembers: { [weak self] in + openInviteMembers: { [weak self] type in guard let self else { return } - self.openInviteMembers() + if case .shareLink = type { + guard let inviteLinks = self.inviteLinks else { + return + } + self.presentShare(inviteLinks) + } else { + self.openInviteMembers() + } }, visibleParticipantsUpdated: { [weak self] visibleParticipants in guard let self else { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift index 422c2cbe5e..1fd32412b3 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -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) @@ -51,13 +26,21 @@ extension VideoChatScreenComponent.View { } } } - let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in + let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, shareLink: { [weak self] in + guard let self else { + return + } + guard let inviteLinks = self.inviteLinks else { + return + } + self.presentShare(inviteLinks) + }, completion: { [weak self] peerIds in guard let self, case let .group(groupCall) = self.currentCall else { return } for peerId in peerIds { - let _ = groupCall.invitePeer(peerId) + let _ = groupCall.invitePeer(peerId.id, isVideo: peerId.isVideo) } }) self.environment?.controller()?.push(controller) @@ -80,7 +63,7 @@ extension VideoChatScreenComponent.View { if inviteIsLink { inviteType = .shareLink } else { - inviteType = .invite + inviteType = .invite(isMultipleUsers: true) } } @@ -146,7 +129,8 @@ extension VideoChatScreenComponent.View { if let participant { dismissController?() - if groupCall.invitePeer(participant.peer.id) { + //TODO:release + if groupCall.invitePeer(participant.peer.id, isVideo: false) { let text: String if case let .channel(channel) = self.peer, case .broadcast = channel.info { text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string @@ -258,7 +242,8 @@ extension VideoChatScreenComponent.View { } dismissController?() - if groupCall.invitePeer(peer.id) { + //TODO:release + if groupCall.invitePeer(peer.id, isVideo: false) { let text: String if case let .channel(channel) = self.peer, case .broadcast = channel.info { text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string @@ -330,7 +315,8 @@ extension VideoChatScreenComponent.View { } dismissController?() - if groupCall.invitePeer(peer.id) { + //TODO:release + if groupCall.invitePeer(peer.id, isVideo: false) { let text: String if case let .channel(channel) = self.peer, case .broadcast = channel.info { text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index be28d3b3af..60cefcd87c 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1256,7 +1256,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController { if let participant = participant { dismissController?() - if strongSelf.call.invitePeer(participant.peer.id) { + //TODO:release + if strongSelf.call.invitePeer(participant.peer.id, isVideo: false) { let text: String if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string @@ -1364,7 +1365,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController { } dismissController?() - if strongSelf.call.invitePeer(peer.id) { + //TODO:release + if strongSelf.call.invitePeer(peer.id, isVideo: false) { let text: String if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string @@ -1432,7 +1434,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController { } dismissController?() - if strongSelf.call.invitePeer(peer.id) { + //TODO:release + if strongSelf.call.invitePeer(peer.id, isVideo: false) { let text: String if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 9feaa78847..27ee6e65f7 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -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) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 304915e48d..3edb3bf37b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -201,12 +201,28 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) case let .messageActionPaidMessagesPrice(stars): return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars)) - case let .messageActionConferenceCall(_, callId, duration, otherParticipants): - return TelegramMediaAction(action: .conferenceCall( + case let .messageActionConferenceCall(flags, callId, duration, otherParticipants): + let isMissed = (flags & (1 << 0)) != 0 + let isActive = (flags & (1 << 1)) != 0 + let isVideo = (flags & (1 << 4)) != 0 + + var mappedFlags = TelegramMediaActionType.ConferenceCall.Flags() + if isMissed { + mappedFlags.insert(.isMissed) + } + if isActive { + mappedFlags.insert(.isActive) + } + if isVideo { + mappedFlags.insert(.isVideo) + } + + return TelegramMediaAction(action: .conferenceCall(TelegramMediaActionType.ConferenceCall( callId: callId, duration: duration, + flags: mappedFlags, otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? [] - )) + ))) } } diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index a1dfc4658e..f418269540 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -382,7 +382,7 @@ private final class CallSessionContext { private final class IncomingConferenceInvitationContext { enum State: Equatable { case pending - case ringing(callId: Int64, otherParticipants: [EnginePeer]) + case ringing(callId: Int64, isVideo: Bool, otherParticipants: [EnginePeer]) case stopped } @@ -420,11 +420,11 @@ private final class IncomingConferenceInvitationContext { } } - if let action = foundAction, case let .conferenceCall(callId, duration, otherParticipants) = action.action { - if duration != nil { + if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action { + if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil { state = .stopped } else { - state = .ringing(callId: callId, otherParticipants: otherParticipants.compactMap { id -> EnginePeer? in + state = .ringing(callId: conferenceCall.callId, isVideo: conferenceCall.flags.contains(.isVideo), otherParticipants: conferenceCall.otherParticipants.compactMap { id -> EnginePeer? in return message.peers[id].flatMap(EnginePeer.init) }) } @@ -639,11 +639,11 @@ private final class CallSessionManagerContext { } } for (id, context) in self.incomingConferenceInvitationContexts { - if case let .ringing(_, otherParticipants) = context.state { + if case let .ringing(_, isVideo, otherParticipants) = context.state { ringingContexts.append(CallSessionRingingState( id: context.internalId, peerId: id.peerId, - isVideo: false, + isVideo: isVideo, isVideoPossible: true, conferenceSource: id, otherParticipants: otherParticipants @@ -718,6 +718,23 @@ private final class CallSessionManagerContext { } } + func dropOutgoingConferenceRequest(messageId: MessageId) { + let addUpdates = self.addUpdates + let rejectSignal = self.network.request(Api.functions.phone.declineConferenceCallInvite(msgId: messageId.id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates { + addUpdates(updates) + } + return .complete() + } + + self.rejectConferenceInvitationDisposables.add(rejectSignal.startStrict()) + } + func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal) { for (id, context) in self.incomingConferenceInvitationContexts { if context.internalId == internalId { @@ -1383,6 +1400,12 @@ public final class CallSessionManager { } } + public func dropOutgoingConferenceRequest(messageId: MessageId) { + self.withContext { context in + context.dropOutgoingConferenceRequest(messageId: messageId) + } + } + func drop(stableId: CallSessionStableId, reason: DropCallReason) { self.withContext { context in context.drop(stableId: stableId, reason: reason) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 5af6c51f2f..9800d6db6b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -86,6 +86,31 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } } + public struct ConferenceCall: Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let isVideo = Flags(rawValue: 1 << 0) + public static let isActive = Flags(rawValue: 1 << 1) + public static let isMissed = Flags(rawValue: 1 << 2) + } + + public let callId: Int64 + public let duration: Int32? + public let flags: Flags + public let otherParticipants: [PeerId] + + public init(callId: Int64, duration: Int32?, flags: Flags, otherParticipants: [PeerId]) { + self.callId = callId + self.duration = duration + self.flags = flags + self.otherParticipants = otherParticipants + } + } + case unknown case groupCreated(title: String) case addedMembers(peerIds: [PeerId]) @@ -134,7 +159,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64) - case conferenceCall(callId: Int64, duration: Int32?, otherParticipants: [PeerId]) + case conferenceCall(ConferenceCall) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -264,7 +289,12 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 47: self = .paidMessagesPriceEdited(stars: decoder.decodeInt64ForKey("stars", orElse: 0)) case 48: - self = .conferenceCall(callId: decoder.decodeInt64ForKey("cid", orElse: 0), duration: decoder.decodeOptionalInt32ForKey("dur"), otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init)) + self = .conferenceCall(ConferenceCall( + callId: decoder.decodeInt64ForKey("cid", orElse: 0), + duration: decoder.decodeOptionalInt32ForKey("dur"), + flags: ConferenceCall.Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)), + otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init) + )) default: self = .unknown } @@ -642,15 +672,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .paidMessagesPriceEdited(stars): encoder.encodeInt32(47, forKey: "_rawValue") encoder.encodeInt64(stars, forKey: "stars") - case let .conferenceCall(callId, duration, otherParticipants): + case let .conferenceCall(conferenceCall): encoder.encodeInt32(48, forKey: "_rawValue") - encoder.encodeInt64(callId, forKey: "cid") - if let duration { + encoder.encodeInt64(conferenceCall.callId, forKey: "cid") + if let duration = conferenceCall.duration { encoder.encodeInt32(duration, forKey: "dur") } else { encoder.encodeNil(forKey: "dur") } - encoder.encodeInt64Array(otherParticipants.map({ $0.toInt64() }), forKey: "part") + encoder.encodeInt32(conferenceCall.flags.rawValue, forKey: "flags") + encoder.encodeInt64Array(conferenceCall.otherParticipants.map({ $0.toInt64() }), forKey: "part") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 19b50bbb48..eaa73c54d8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -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 { 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, @@ -831,25 +835,32 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, } } -func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, accessHash: Int64, peerId: EnginePeer.Id) -> Signal { +func _internal_inviteConferenceCallParticipant(account: Account, reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal { return account.postbox.transaction { transaction -> Api.InputUser? in return transaction.getPeer(peerId).flatMap(apiInputUser) } - |> mapToSignal { inputPeer -> Signal in + |> mapToSignal { inputPeer -> Signal in guard let inputPeer else { return .complete() } - return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(call: .inputGroupCall(id: callId, accessHash: accessHash), userId: inputPeer)) + var flags: Int32 = 0 + if isVideo { + flags |= 1 << 0 + } + return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: reference.apiInputGroupCall, userId: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in if let result { account.stateManager.addUpdates(result) + if let message = result.messageIds.first { + return .single(message) + } } - return .complete() + return .single(nil) } } } @@ -2952,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 { + return account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 1, call: reference.apiInputGroupCall, joinMuted: .boolFalse)) + |> mapError { _ -> RevokeConferenceInviteLinkError in + return .generic + } + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return _internal_groupCallInviteLinks(account: account, reference: reference, isConference: true) + |> castError(RevokeConferenceInviteLinkError.self) + |> mapToSignal { result -> Signal in + guard let result = result else { + return .fail(.generic) + } + return .single(result) + } + } +} + public enum ConfirmAddConferenceParticipantError { case generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift index c5e06f565d..9ebc1e1fff 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift @@ -96,6 +96,10 @@ public extension TelegramEngine { public func createConferenceCall() -> Signal { return _internal_createConferenceCall(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId) } + + public func revokeConferenceInviteLink(reference: InternalGroupCallReference, link: String) -> Signal { + 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) @@ -105,8 +109,8 @@ public extension TelegramEngine { return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block) } - public func inviteConferenceCallParticipant(callId: Int64, accessHash: Int64, peerId: EnginePeer.Id) -> Signal { - return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId) + public func inviteConferenceCallParticipant(reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal { + return _internal_inviteConferenceCallParticipant(account: self.account, reference: reference, peerId: peerId, isVideo: isVideo) } public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD index 17460293d1..77e9f52615 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD @@ -10,6 +10,7 @@ swift_library( "-warnings-as-errors", ], deps = [ + "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index 642e0657e5..75987f4d84 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -9,6 +9,7 @@ import AppBundle import ChatMessageBubbleContentNode import ChatMessageItemCommon import ChatMessageDateAndStatusNode +import SwiftSignalKit private let titleFont: UIFont = Font.medium(16.0) private let labelFont: UIFont = Font.regular(13.0) @@ -25,6 +26,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { private let iconNode: ASImageNode private let buttonNode: HighlightableButtonNode + private var activeConferenceUpdateTimer: SwiftSignalKit.Timer? + required public init() { self.titleNode = TextNode() self.labelNode = TextNode() @@ -57,6 +60,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside) } + deinit { + self.activeConferenceUpdateTimer?.invalidate() + } + override public func accessibilityActivate() -> Bool { self.callButtonPressed() return true @@ -90,6 +97,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { var callDuration: Int32? var callSuccessful = true var isVideo = false + var hasCallButton = true + var updateConferenceTimerEndTimeout: Int32? for media in item.message.media { if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action { isVideo = isVideoValue @@ -123,11 +132,32 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { } } break - } else if let action = media as? TelegramMediaAction, case let .conferenceCall(_, duration, _) = action.action { - isVideo = false - callDuration = duration + } else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action { + isVideo = conferenceCall.flags.contains(.isVideo) + callDuration = conferenceCall.duration //TODO:localize - titleString = "Group Call" + let missedTimeout: Int32 + #if DEBUG + missedTimeout = 5 + #else + missedTimeout = 30 + #endif + let currentTime = Int32(Date().timeIntervalSince1970) + if conferenceCall.flags.contains(.isMissed) { + titleString = "Declined Group Call" + } else if item.message.timestamp < currentTime - missedTimeout { + titleString = "Missed Group Call" + } else if conferenceCall.duration != nil { + titleString = "Cancelled Group Call" + hasCallButton = true + } else { + if incoming { + titleString = "Incoming Group Call" + } else { + titleString = "Outgoing Group Call" + } + updateConferenceTimerEndTimeout = (item.message.timestamp + missedTimeout) - currentTime + } break } } @@ -211,7 +241,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom - boundingSize.width += 54.0 + if hasCallButton { + boundingSize.width += 54.0 + } return (boundingSize.width, { boundingWidth in return (boundingSize, { [weak self] animation, _, _ in @@ -234,6 +266,22 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { if let buttonImage = buttonImage { strongSelf.buttonNode.setImage(buttonImage, for: []) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size) + strongSelf.buttonNode.isHidden = !hasCallButton + } + + if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer { + activeConferenceUpdateTimer.invalidate() + strongSelf.activeConferenceUpdateTimer = nil + } + if let updateConferenceTimerEndTimeout, updateConferenceTimerEndTimeout >= 0 { + strongSelf.activeConferenceUpdateTimer?.invalidate() + strongSelf.activeConferenceUpdateTimer = SwiftSignalKit.Timer(timeout: Double(updateConferenceTimerEndTimeout) + 0.5, repeat: false, completion: { [weak strongSelf] in + guard let strongSelf else { + return + } + strongSelf.requestInlineUpdate?() + }, queue: .mainQueue()) + strongSelf.activeConferenceUpdateTimer?.start() } } }) @@ -270,6 +318,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if self.buttonNode.isHidden { + return ChatMessageBubbleContentTapAction(content: .none) + } + if self.buttonNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .ignore) } else if self.bounds.contains(point), let item = self.item { diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift index 3a4dcdb3b6..467fe9e858 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -405,7 +405,7 @@ private final class JoinSubjectScreenComponent: Component { isStream: false ), reference: .link(slug: groupCall.slug), - mode: .joining + beginWithVideo: false ) self.environment?.controller()?.dismiss() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c992656120..c48f21a471 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -14125,7 +14125,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) diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 76198269d7..3e392a1ea7 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -136,6 +136,20 @@ public final class TextNodeWithEntities { } } + private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? + private var linkHighlightingNode: LinkHighlightingNode? + + public var linkHighlightColor: UIColor? + public var linkHighlightInset: UIEdgeInsets = .zero + + public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? { + didSet { + self.updateInteractiveActions() + } + } + public init() { self.textNode = TextNode() } @@ -301,6 +315,83 @@ public final class TextNodeWithEntities { self.inlineStickerItemLayers.removeValue(forKey: key) } } + + private func updateInteractiveActions() { + if self.highlightAttributeAction != nil { + if self.tapRecognizer == nil { + let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:))) + tapRecognizer.highlight = { [weak self] point in + if let strongSelf = self, let cachedLayout = strongSelf.textNode.cachedLayout { + var rects: [CGRect]? + if let point = point { + if let (index, attributes) = strongSelf.textNode.attributesAtPoint(CGPoint(x: point.x, y: point.y)) { + if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) { + let initialRects = strongSelf.textNode.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index) + if let initialRects = initialRects, case .center = cachedLayout.resolvedAlignment { + var mappedRects: [CGRect] = [] + for i in 0 ..< initialRects.count { + let lineRect = initialRects[i].0 + var itemRect = initialRects[i].1 + itemRect.origin.x = floor((strongSelf.textNode.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x + mappedRects.append(itemRect) + } + rects = mappedRects + } else { + rects = strongSelf.textNode.attributeRects(name: selectedAttribute.rawValue, at: index) + } + } + } + } + + if var rects, !rects.isEmpty { + let linkHighlightingNode: LinkHighlightingNode + if let current = strongSelf.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear) + strongSelf.linkHighlightingNode = linkHighlightingNode + strongSelf.textNode.addSubnode(linkHighlightingNode) + } + linkHighlightingNode.frame = strongSelf.textNode.bounds + rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset) + linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) }) + } else if let linkHighlightingNode = strongSelf.linkHighlightingNode { + strongSelf.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } + self.textNode.view.addGestureRecognizer(tapRecognizer) + } + } else if let tapRecognizer = self.tapRecognizer { + self.tapRecognizer = nil + self.textNode.view.removeGestureRecognizer(tapRecognizer) + } + } + + @objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x, y: location.y)) { + self.tapAttributeAction?(attributes, index) + } + case .longTap: + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x, y: location.y)) { + self.longTapAttributeAction?(attributes, index) + } + default: + break + } + } + default: + break + } + } } public class ImmediateTextNodeWithEntities: TextNode { diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 5aae2416fa..1f0026c3e4 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -398,6 +398,8 @@ final class AuthorizedApplicationContext { if let action = media as? TelegramMediaAction { if case .messageAutoremoveTimeoutUpdated = action.action { return + } else if case .conferenceCall = action.action { + return } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1c7eaf7718..c989df76e6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2890,14 +2890,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - guard case let .conferenceCall(callId, duration, _) = action?.action else { + guard case let .conferenceCall(conferenceCall) = action?.action else { return } - if duration != nil { + if conferenceCall.duration != nil { return } - if let currentGroupCallController = self.context.sharedContext as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == callId { + 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 } @@ -2919,7 +2919,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isStream: false ), reference: .message(id: message.id), - mode: .joining + beginWithVideo: conferenceCall.flags.contains(.isVideo) ) }) }, longTap: { [weak self] action, params in diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 31e7b99618..0cb897a7d2 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -60,6 +60,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } private let confirmation: (ContactListPeer) -> Signal + private let isPeerEnabled: (ContactListPeer) -> Bool var dismissed: (() -> Void)? var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in } @@ -107,6 +108,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.displayDeviceContacts = params.displayDeviceContacts self.displayCallIcons = params.displayCallIcons self.confirmation = params.confirmation + self.isPeerEnabled = params.isPeerEnabled self.multipleSelection = params.multipleSelection self.requirePhoneNumbers = params.requirePhoneNumbers self.allowChannelsInSearch = params.allowChannelsInSearch @@ -218,7 +220,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } override func loadDisplayNode() { - self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch) + self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch, isPeerEnabled: self.isPeerEnabled) self._ready.set(self.contactsNode.contactListNode.ready) self.contactsNode.navigationBar = self.navigationBar diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index ebbfd6d3f3..58abf88ff3 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -44,6 +44,8 @@ final class ContactSelectionControllerNode: ASDisplayNode { var cancelSearch: (() -> Void)? var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)? + let isPeerEnabled: (ContactListPeer) -> Bool + var presentationData: PresentationData { didSet { self.presentationDataPromise.set(.single(self.presentationData)) @@ -57,12 +59,13 @@ final class ContactSelectionControllerNode: ASDisplayNode { var searchContainerNode: ContactsSearchContainerNode? - init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool) { + init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool, isPeerEnabled: @escaping (ContactListPeer) -> Bool) { self.context = context self.presentationData = presentationData self.displayDeviceContacts = displayDeviceContacts self.displayCallIcons = displayCallIcons self.allowChannelsInSearch = allowChannelsInSearch + self.isPeerEnabled = isPeerEnabled var excludeSelf = true @@ -124,7 +127,9 @@ final class ContactSelectionControllerNode: ASDisplayNode { } var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in + self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, isPeerEnabled: { peer in + return isPeerEnabled(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)) + }, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in contextActionImpl?(peer, node, gesture, nil) } : nil, multipleSelection: multipleSelection) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 2900d2d7ef..bcb6f012d2 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -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( diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index f731d15fed..aed5d502c4 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -270,11 +270,13 @@ public final class OngoingGroupCallContext { } public var kind: Kind + public var peerId: Int64 public var audioSsrc: UInt32 public var videoDescription: String? - public init(kind: Kind, audioSsrc: UInt32, videoDescription: String?) { + public init(kind: Kind, peerId: Int64, audioSsrc: UInt32, videoDescription: String?) { self.kind = kind + self.peerId = peerId self.audioSsrc = audioSsrc self.videoDescription = videoDescription } @@ -575,6 +577,7 @@ public final class OngoingGroupCallContext { } return OngoingGroupCallMediaChannelDescription( type: mappedType, + peerId: channel.peerId, audioSsrc: channel.audioSsrc, videoDescription: channel.videoDescription ) @@ -688,6 +691,7 @@ public final class OngoingGroupCallContext { } return OngoingGroupCallMediaChannelDescription( type: mappedType, + peerId: channel.peerId, audioSsrc: channel.audioSsrc, videoDescription: channel.videoDescription ) diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 25ec5e072c..bc2349f7b4 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -332,10 +332,12 @@ typedef NS_ENUM(int32_t, OngoingGroupCallMediaChannelType) { @interface OngoingGroupCallMediaChannelDescription : NSObject @property (nonatomic, readonly) OngoingGroupCallMediaChannelType type; +@property (nonatomic, readonly) uint64_t peerId; @property (nonatomic, readonly) uint32_t audioSsrc; @property (nonatomic, strong, readonly) NSString * _Nullable videoDescription; - (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type + peerId:(int64_t)peerId audioSsrc:(uint32_t)audioSsrc videoDescription:(NSString * _Nullable)videoDescription; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 2b7c910bbf..aade0011a9 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -2670,7 +2670,7 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp }, .createWrappedAudioDeviceModule = [audioDeviceModule, isActiveByDefault](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr { if (audioDeviceModule) { - auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault); + auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault || true); return result; } else { return nullptr; @@ -3029,11 +3029,13 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp @implementation OngoingGroupCallMediaChannelDescription - (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type - audioSsrc:(uint32_t)audioSsrc - videoDescription:(NSString * _Nullable)videoDescription { + peerId:(int64_t)peerId + audioSsrc:(uint32_t)audioSsrc + videoDescription:(NSString * _Nullable)videoDescription { self = [super init]; if (self != nil) { _type = type; + _peerId = peerId; _audioSsrc = audioSsrc; _videoDescription = videoDescription; } diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 16cb0d2956..a15014304d 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 16cb0d29562576be221a7ac2b8bdd81fdb954bc4 +Subproject commit a15014304d25193157ee809e8faceaca95dd8192