Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2025-04-03 17:07:12 +04:00
commit 13669eee96
54 changed files with 1483 additions and 413 deletions

View File

@ -22,6 +22,7 @@ internal:
- export PATH=/opt/homebrew/opt/ruby/bin:$PATH - export PATH=/opt/homebrew/opt/ruby/bin:$PATH
- export PATH=`gem environment gemdir`/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/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" - 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: environment:
name: internal name: internal

View File

@ -11,25 +11,26 @@ def is_apple_silicon():
return False return False
def get_clean_env(): def get_clean_env(use_clean_env=True):
clean_env = os.environ.copy() 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 return clean_env
def resolve_executable(program): def resolve_executable(program, use_clean_env=True):
def is_executable(fpath): def is_executable(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 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) executable_file = os.path.join(path, program)
if is_executable(executable_file): if is_executable(executable_file):
return executable_file return executable_file
return None return None
def run_executable_with_output(path, arguments, decode=True, input=None, stderr_to_stdout=True, print_command=False, check_result=False): 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) executable_path = resolve_executable(path, use_clean_env=use_clean_env)
if executable_path is None: if executable_path is None:
raise Exception('Could not resolve {} to a valid executable file'.format(path)) 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, stdout=subprocess.PIPE,
stderr=stderr_assignment, stderr=stderr_assignment,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
env=get_clean_env() env=get_clean_env(use_clean_env=use_clean_env)
) )
if input is not None: if input is not None:
output_data, _ = process.communicate(input=input) output_data, _ = process.communicate(input=input)

View File

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

View File

@ -111,10 +111,27 @@ public final class ContactSelectionControllerParams {
public let requirePhoneNumbers: Bool public let requirePhoneNumbers: Bool
public let allowChannelsInSearch: Bool public let allowChannelsInSearch: Bool
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError> public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
public let isPeerEnabled: (ContactListPeer) -> Bool
public let openProfile: ((EnginePeer) -> Void)? public let openProfile: ((EnginePeer) -> Void)?
public let sendMessage: ((EnginePeer) -> Void)? public let sendMessage: ((EnginePeer) -> Void)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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<Bool, NoError> = { _ in .single(true) }, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) { public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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<Bool, NoError> = { _ in .single(true) },
isPeerEnabled: @escaping (ContactListPeer) -> Bool = { _ in true },
openProfile: ((EnginePeer) -> Void)? = nil,
sendMessage: ((EnginePeer) -> Void)? = nil
) {
self.context = context self.context = context
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
self.mode = mode self.mode = mode
@ -127,6 +144,7 @@ public final class ContactSelectionControllerParams {
self.requirePhoneNumbers = requirePhoneNumbers self.requirePhoneNumbers = requirePhoneNumbers
self.allowChannelsInSearch = allowChannelsInSearch self.allowChannelsInSearch = allowChannelsInSearch
self.confirmation = confirmation self.confirmation = confirmation
self.isPeerEnabled = isPeerEnabled
self.openProfile = openProfile self.openProfile = openProfile
self.sendMessage = sendMessage self.sendMessage = sendMessage
} }

View File

@ -173,7 +173,7 @@ public protocol PresentationCall: AnyObject {
func setCurrentAudioOutput(_ output: AudioSessionOutput) func setCurrentAudioOutput(_ output: AudioSessionOutput)
func debugInfo() -> Signal<(String, String), NoError> 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) func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
} }
@ -423,6 +423,7 @@ public protocol PresentationGroupCall: AnyObject {
var internalId: CallSessionInternalId { get } var internalId: CallSessionInternalId { get }
var peerId: EnginePeer.Id? { get } var peerId: EnginePeer.Id? { get }
var callId: Int64? { get } var callId: Int64? { get }
var currentReference: InternalGroupCallReference? { get }
var hasVideo: Bool { get } var hasVideo: Bool { get }
var hasScreencast: Bool { get } var hasScreencast: Bool { get }
@ -484,7 +485,7 @@ public protocol PresentationGroupCall: AnyObject {
func updateTitle(_ title: String) func updateTitle(_ title: String)
func invitePeer(_ peerId: EnginePeer.Id) -> Bool func invitePeer(_ peerId: EnginePeer.Id, isVideo: Bool) -> Bool
func removedPeer(_ peerId: EnginePeer.Id) func removedPeer(_ peerId: EnginePeer.Id)
var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> { get } var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> { get }
@ -550,10 +551,6 @@ public enum PresentationCurrentCall: Equatable {
} }
} }
public enum JoinConferenceCallMode {
case joining
}
public protocol PresentationCallManager: AnyObject { public protocol PresentationCallManager: AnyObject {
var currentCallSignal: Signal<PresentationCall?, NoError> { get } var currentCallSignal: Signal<PresentationCall?, NoError> { get }
var currentGroupCallSignal: Signal<VideoChatCall?, NoError> { get } var currentGroupCallSignal: Signal<VideoChatCall?, NoError> { get }
@ -568,6 +565,6 @@ public protocol PresentationCallManager: AnyObject {
accountContext: AccountContext, accountContext: AccountContext,
initialCall: EngineGroupCallDescription, initialCall: EngineGroupCallDescription,
reference: InternalGroupCallReference, reference: InternalGroupCallReference,
mode: JoinConferenceCallMode beginWithVideo: Bool
) )
} }

View File

@ -297,18 +297,19 @@ public final class AvatarNode: ASDisplayNode {
private struct Params: Equatable { private struct Params: Equatable {
let peerId: EnginePeer.Id? let peerId: EnginePeer.Id?
let resourceId: String? let resourceId: String?
let clipStyle: AvatarNodeClipStyle
let displayDimensions: CGSize let displayDimensions: CGSize
let clipStyle: AvatarNodeClipStyle
init( init(
peerId: EnginePeer.Id?, peerId: EnginePeer.Id?,
resourceId: String?, resourceId: String?,
clipStyle: AvatarNodeClipStyle, displayDimensions: CGSize,
displayDimensions: CGSize clipStyle: AvatarNodeClipStyle
) { ) {
self.peerId = peerId self.peerId = peerId
self.resourceId = resourceId self.resourceId = resourceId
self.clipStyle = clipStyle
self.displayDimensions = displayDimensions self.displayDimensions = displayDimensions
self.clipStyle = clipStyle
} }
} }
@ -663,8 +664,8 @@ public final class AvatarNode: ASDisplayNode {
let params = Params( let params = Params(
peerId: peer?.id, peerId: peer?.id,
resourceId: smallProfileImage?.resource.id.stringRepresentation, resourceId: smallProfileImage?.resource.id.stringRepresentation,
clipStyle: clipStyle, displayDimensions: displayDimensions,
displayDimensions: displayDimensions clipStyle: clipStyle
) )
if self.params == params { if self.params == params {
return return

View File

@ -138,16 +138,7 @@ class CallListCallItem: ListViewItem {
func selected(listView: ListView) { func selected(listView: ListView) {
listView.clearHighlightAnimated(true) listView.clearHighlightAnimated(true)
var isVideo = false self.interaction.call(self.topMessage)
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)
} }
static func mergeType(item: CallListCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) { 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 { guard let item = self?.layoutParams?.0 else {
return false return false
} }
var isVideo = false item.interaction.call(item.topMessage)
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)
return true return true
} }
} }
@ -390,6 +373,9 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
var hadDuration = false var hadDuration = false
var callDuration: Int32? var callDuration: Int32?
var isConference = false
var conferenceIsDeclined = false
for message in item.messages { for message in item.messages {
inner: for media in message.media { inner: for media in message.media {
if let action = media as? TelegramMediaAction { if let action = media as? TelegramMediaAction {
@ -411,6 +397,36 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
} else { } else {
callDuration = nil 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 break inner
} }
@ -441,7 +457,18 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor) 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) 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 statusAccessibilityString = isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallMissed : item.presentationData.strings.Call_VoiceOver_VoiceCallMissed
} else if hasIncoming && hasOutgoing { } else if hasIncoming && hasOutgoing {

View File

@ -92,6 +92,7 @@ public final class CallListController: TelegramBaseController {
private let createActionDisposable = MetaDisposable() private let createActionDisposable = MetaDisposable()
private let clearDisposable = MetaDisposable() private let clearDisposable = MetaDisposable()
private var createConferenceCallDisposable: Disposable?
public init(context: AccountContext, mode: CallListControllerMode) { public init(context: AccountContext, mode: CallListControllerMode) {
self.context = context self.context = context
@ -163,6 +164,7 @@ public final class CallListController: TelegramBaseController {
self.presentationDataDisposable?.dispose() self.presentationDataDisposable?.dispose()
self.peerViewDisposable.dispose() self.peerViewDisposable.dispose()
self.clearDisposable.dispose() self.clearDisposable.dispose()
self.createConferenceCallDisposable?.dispose()
} }
private func updateThemeAndStrings() { private func updateThemeAndStrings() {
@ -210,11 +212,16 @@ public final class CallListController: TelegramBaseController {
guard !self.presentAccountFrozenInfoIfNeeded() else { guard !self.presentAccountFrozenInfoIfNeeded() else {
return return
} }
let _ = (self.context.engine.calls.createConferenceCall() if self.createConferenceCallDisposable != nil {
|> deliverOnMainQueue).startStandalone(next: { [weak self] call in return
}
self.createConferenceCallDisposable = (self.context.engine.calls.createConferenceCall()
|> deliverOnMainQueue).startStrict(next: { [weak self] call in
guard let self else { guard let self else {
return return
} }
self.createConferenceCallDisposable?.dispose()
self.createConferenceCallDisposable = nil
let openCall: () -> Void = { [weak self] in let openCall: () -> Void = { [weak self] in
guard let self else { guard let self else {
@ -231,38 +238,55 @@ public final class CallListController: TelegramBaseController {
isStream: false isStream: false
), ),
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), 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 let controller = InviteLinkInviteController(
guard let self else { context: self.context,
return updatedPresentationData: nil,
} mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(callId: call.callInfo.id, accessHash: call.callInfo.accessHash, isRecentlyCreated: true, canRevoke: true)),
if let result { 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),
switch result { parentNavigationController: self.navigationController as? NavigationController,
case .linkCopied: completed: { [weak self] result in
//TODO:localize guard let self else {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } return
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 { if let result {
openCall() switch result {
} case .linkCopied:
return false //TODO:localize
}), in: .window(.root)) let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
case .openCall: 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
openCall() if case .undo = action {
openCall()
}
return false
}), in: .window(.root))
case .openCall:
openCall()
}
} }
} }
}) )
self.present(controller, in: .window(.root), with: nil) self.present(controller, in: .window(.root), with: nil)
}) })
} }
override public func loadDisplayNode() { override public func loadDisplayNode() {
self.displayNode = CallListControllerNode(controller: self, context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] peerId, isVideo in self.displayNode = CallListControllerNode(controller: self, context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] message in
if let strongSelf = self { guard let self else {
strongSelf.call(peerId, isVideo: isVideo) 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 }, joinGroupCall: { [weak self] peerId, activeCall in
if let self { 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) { override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_StartNewCall, icon: { theme in items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_StartNewCall, icon: { theme in

View File

@ -62,14 +62,14 @@ private extension EngineCallList.Item {
final class CallListNodeInteraction { final class CallListNodeInteraction {
let setMessageIdWithRevealedOptions: (EngineMessage.Id?, EngineMessage.Id?) -> Void let setMessageIdWithRevealedOptions: (EngineMessage.Id?, EngineMessage.Id?) -> Void
let call: (EnginePeer.Id, Bool) -> Void let call: (EngineMessage) -> Void
let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
let delete: ([EngineMessage.Id]) -> Void let delete: ([EngineMessage.Id]) -> Void
let updateShowCallsTab: (Bool) -> Void let updateShowCallsTab: (Bool) -> Void
let openGroupCall: (EnginePeer.Id) -> Void let openGroupCall: (EnginePeer.Id) -> Void
let createGroupCall: () -> 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.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
self.call = call self.call = call
self.openInfo = openInfo self.openInfo = openInfo
@ -222,7 +222,7 @@ final class CallListControllerNode: ASDisplayNode {
private let emptyButtonIconNode: ASImageNode private let emptyButtonIconNode: ASImageNode
private let emptyButtonTextNode: ImmediateTextNode 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 joinGroupCall: (EnginePeer.Id, EngineGroupCallDescription) -> Void
private let createGroupCall: () -> Void private let createGroupCall: () -> Void
private let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void private let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
@ -234,7 +234,7 @@ final class CallListControllerNode: ASDisplayNode {
private var previousContentOffset: ListViewVisibleContentOffset? 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.controller = controller
self.context = context self.context = context
self.mode = mode self.mode = mode
@ -333,8 +333,8 @@ final class CallListControllerNode: ASDisplayNode {
} }
} }
} }
}, call: { [weak self] peerId, isVideo in }, call: { [weak self] message in
self?.call(peerId, isVideo) self?.call(message)
}, openInfo: { [weak self] peerId, messages in }, openInfo: { [weak self] peerId, messages in
self?.openInfo(peerId, messages) self?.openInfo(peerId, messages)
}, delete: { [weak self] messageIds in }, delete: { [weak self] messageIds in
@ -519,10 +519,7 @@ final class CallListControllerNode: ASDisplayNode {
let canCreateGroupCall = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) let canCreateGroupCall = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App())
|> map { configuration -> Bool in |> map { configuration -> Bool in
var isConferencePossible = false var isConferencePossible = true
if context.sharedContext.immediateExperimentalUISettings.conferenceDebug {
isConferencePossible = true
}
if let data = configuration.data, let value = data["ios_enable_conference"] as? Double { if let data = configuration.data, let value = data["ios_enable_conference"] as? Double {
isConferencePossible = value != 0.0 isConferencePossible = value != 0.0
} }

View File

@ -692,6 +692,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) { public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
var reallyHighlighted = self.isHighlighted var reallyHighlighted = self.isHighlighted
if let item = self.item, !item.enabled {
reallyHighlighted = false
}
let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0 let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0
if let item = self.item { if let item = self.item {
switch item.peer { switch item.peer {
@ -1649,6 +1652,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
actionButtonNode.setImage(actionButton.image, for: .normal) 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)) 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 offset += actionButtonImage.size.width + 12.0
} }
} }

View File

@ -100,7 +100,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case enableReactionOverrides(Bool) case enableReactionOverrides(Bool)
case compressedEmojiCache(Bool) case compressedEmojiCache(Bool)
case storiesJpegExperiment(Bool) case storiesJpegExperiment(Bool)
case conferenceDebug(Bool)
case checkSerializedData(Bool) case checkSerializedData(Bool)
case enableQuickReactionSwitch(Bool) case enableQuickReactionSwitch(Bool)
case disableReloginTokens(Bool) case disableReloginTokens(Bool)
@ -134,7 +133,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.web.rawValue return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue 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 return DebugControllerSection.experiments.rawValue
case .logTranslationRecognition, .resetTranslationStates: case .logTranslationRecognition, .resetTranslationStates:
return DebugControllerSection.translation.rawValue return DebugControllerSection.translation.rawValue
@ -243,8 +242,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 47 return 47
case .disableReloginTokens: case .disableReloginTokens:
return 48 return 48
case .conferenceDebug:
return 49
case .checkSerializedData: case .checkSerializedData:
return 50 return 50
case .enableQuickReactionSwitch: case .enableQuickReactionSwitch:
@ -1311,16 +1308,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
}) })
}).start() }).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): case let .checkSerializedData(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Check Serialized Data", value: value, sectionId: self.section, style: .blocks, updated: { value in 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 let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
@ -1552,7 +1539,6 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.storiesJpegExperiment(experimentalSettings.storiesJpegExperiment)) entries.append(.storiesJpegExperiment(experimentalSettings.storiesJpegExperiment))
entries.append(.disableReloginTokens(experimentalSettings.disableReloginTokens)) entries.append(.disableReloginTokens(experimentalSettings.disableReloginTokens))
entries.append(.conferenceDebug(experimentalSettings.conferenceDebug))
entries.append(.checkSerializedData(experimentalSettings.checkSerializedData)) entries.append(.checkSerializedData(experimentalSettings.checkSerializedData))
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
entries.append(.liveStreamV2(experimentalSettings.liveStreamV2)) entries.append(.liveStreamV2(experimentalSettings.liveStreamV2))

View File

@ -364,7 +364,7 @@ public final class TextNodeLayout: NSObject {
fileprivate let backgroundColor: UIColor? fileprivate let backgroundColor: UIColor?
fileprivate let constrainedSize: CGSize fileprivate let constrainedSize: CGSize
fileprivate let explicitAlignment: NSTextAlignment fileprivate let explicitAlignment: NSTextAlignment
fileprivate let resolvedAlignment: NSTextAlignment public let resolvedAlignment: NSTextAlignment
fileprivate let verticalAlignment: TextVerticalAlignment fileprivate let verticalAlignment: TextVerticalAlignment
fileprivate let lineSpacing: CGFloat fileprivate let lineSpacing: CGFloat
fileprivate let cutout: TextNodeCutout? fileprivate let cutout: TextNodeCutout?

View File

@ -27,13 +27,15 @@ class InviteLinkInviteInteraction {
let copyLink: (ExportedInvitation) -> Void let copyLink: (ExportedInvitation) -> Void
let shareLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void
let manageLinks: () -> 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.context = context
self.mainLinkContextAction = mainLinkContextAction self.mainLinkContextAction = mainLinkContextAction
self.copyLink = copyLink self.copyLink = copyLink
self.shareLink = shareLink self.shareLink = shareLink
self.manageLinks = manageLinks self.manageLinks = manageLinks
self.openCallAction = openCallAction
} }
} }
@ -131,6 +133,8 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
}, contextAction: { node, gesture in }, contextAction: { node, gesture in
interaction.mainLinkContextAction(invitation, node, gesture) interaction.mainLinkContextAction(invitation, node, gesture)
}, viewAction: { }, viewAction: {
}, openCallAction: {
interaction.openCallAction()
}) })
case let .manage(text, standalone): case let .manage(text, standalone):
return InviteLinkInviteManageItem(theme: presentationData.theme, text: text, standalone: standalone, action: { 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) 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 { public final class InviteLinkInviteController: ViewController {
private var controllerNode: Node { private var controllerNode: Node {
return self.displayNode as! Node return self.displayNode as! Node
} }
public enum Mode { 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 groupOrChannel(peerId: EnginePeer.Id)
case groupCall(link: String, isRecentlyCreated: Bool) case groupCall(GroupCall)
} }
public enum CompletionResult { public enum CompletionResult {
@ -169,6 +191,7 @@ public final class InviteLinkInviteController: ViewController {
private let context: AccountContext private let context: AccountContext
private let mode: Mode private let mode: Mode
private let initialInvite: ExportedInvitation?
private weak var parentNavigationController: NavigationController? private weak var parentNavigationController: NavigationController?
private var presentationData: PresentationData private var presentationData: PresentationData
@ -176,9 +199,10 @@ public final class InviteLinkInviteController: ViewController {
fileprivate let completed: ((CompletionResult?) -> Void)? fileprivate let completed: ((CompletionResult?) -> Void)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: Mode, parentNavigationController: NavigationController?, completed: ((CompletionResult?) -> Void)? = nil) { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: Mode, initialInvite: ExportedInvitation?, parentNavigationController: NavigationController?, completed: ((CompletionResult?) -> Void)? = nil) {
self.context = context self.context = context
self.mode = mode self.mode = mode
self.initialInvite = initialInvite
self.parentNavigationController = parentNavigationController self.parentNavigationController = parentNavigationController
self.completed = completed self.completed = completed
@ -211,7 +235,7 @@ public final class InviteLinkInviteController: ViewController {
} }
override public func loadDisplayNode() { 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 private var didAppearOnce: Bool = false
@ -292,7 +316,7 @@ public final class InviteLinkInviteController: ViewController {
private var revokeDisposable = MetaDisposable() 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.context = context
self.mode = mode self.mode = mode
@ -315,7 +339,7 @@ public final class InviteLinkInviteController: ViewController {
self.headerNode.clipsToBounds = false self.headerNode.clipsToBounds = false
self.headerBackgroundNode = ASDisplayNode() 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.cornerRadius = 16.0
self.headerBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] self.headerBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
@ -334,7 +358,7 @@ public final class InviteLinkInviteController: ViewController {
self.historyBackgroundContentNode = ASDisplayNode() self.historyBackgroundContentNode = ASDisplayNode()
self.historyBackgroundContentNode.isLayerBacked = true 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) self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode)
@ -350,7 +374,7 @@ public final class InviteLinkInviteController: ViewController {
self.backgroundColor = nil self.backgroundColor = nil
self.isOpaque = false self.isOpaque = false
let mainInvitePromise = ValuePromise<ExportedInvitation?>(nil) let mainInvitePromise = ValuePromise<ExportedInvitation?>(initialInvite)
self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in
guard let self else { guard let self else {
@ -359,7 +383,6 @@ public final class InviteLinkInviteController: ViewController {
guard let node = node as? ContextReferenceContentNode else { guard let node = node as? ContextReferenceContentNode else {
return return
} }
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in 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
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
}, action: { [weak self] _, f in f(.dismissWithoutContent)
f(.dismissWithoutContent)
guard let self else { guard let self else {
return return
} }
if let invite = invite { if let invite {
if case let .groupOrChannel(peerId) = self.mode {
let _ = (context.account.postbox.loadedPeerWithId(peerId) let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in |> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -400,12 +423,17 @@ public final class InviteLinkInviteController: ViewController {
isGroup = true isGroup = true
} }
let updatedPresentationData = (strongSelf.presentationData, strongSelf.presentationDataPromise.get()) 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)) 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 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [ weak self] _, f in }, action: { [ weak self] _, f in
@ -450,7 +478,45 @@ public final class InviteLinkInviteController: ViewController {
self?.controller?.present(controller, in: .window(.root)) 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) 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?.parentNavigationController?.pushViewController(controller)
strongSelf.controller?.dismiss() 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) let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
@ -587,15 +659,16 @@ public final class InviteLinkInviteController: ViewController {
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
} }
}) })
case let .groupCall(link, isRecentlyCreated): case let .groupCall(groupCall):
//TODO:release // A workaround to skip the first run of the event cycle
let tempInfo: Signal<Void, NoError> = .single(Void()) |> delay(0.0, queue: .mainQueue()) let delayOfZero = Signal<Void, NoError>.single(()) |> delay(0.0, queue: .mainQueue())
self.disposable = (combineLatest(queue: .mainQueue(), self.disposable = (combineLatest(queue: .mainQueue(),
self.presentationDataPromise.get(), 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 { guard let self else {
return 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." let helpText: String = "Anyone on Telegram can join your call by following the link below."
entries.append(.header(title: "Call Link", text: helpText)) 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) let previousEntries = previousEntries.swap(entries)
@ -665,8 +738,8 @@ public final class InviteLinkInviteController: ViewController {
self.presentationData = presentationData self.presentationData = presentationData
self.presentationDataPromise.set(.single(presentationData)) self.presentationDataPromise.set(.single(presentationData))
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.historyBackgroundContentNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor 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.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))! self.doneButtonIconNode.image = generateCloseButtonImage(backgroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))!

View File

@ -229,6 +229,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
if let invite = invite { if let invite = invite {
arguments.openLink(invite) arguments.openLink(invite)
} }
}, openCallAction: {
}) })
case let .mainLinkOtherInfo(_, text): case let .mainLinkOtherInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: nil, style: .blocks, tag: nil) 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 { } else {
isGroup = true 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 isGroup = true
} }
Queue.mainQueue().after(0.2) { Queue.mainQueue().after(0.2) {
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil) presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
} }
}) })
}))) })))

View File

@ -271,6 +271,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
}, contextAction: invite.link?.hasSuffix("...") == true ? nil : { node, gesture in }, contextAction: invite.link?.hasSuffix("...") == true ? nil : { node, gesture in
interaction.contextAction(invite, node, gesture) interaction.contextAction(invite, node, gesture)
}, viewAction: { }, viewAction: {
}, openCallAction: {
}) })
case let .subscriptionHeader(_, title): case let .subscriptionHeader(_, title):
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
@ -754,7 +755,7 @@ public final class InviteLinkViewController: ViewController {
isGroup = true isGroup = true
} }
let updatedPresentationData = (strongSelf.presentationData, parentController.presentationDataPromise.get()) let updatedPresentationData = (strongSelf.presentationData, parentController.presentationDataPromise.get())
strongSelf.controller?.present(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), in: .window(.root)) strongSelf.controller?.present(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), in: .window(.root))
}) })
}))) })))
} }

View File

@ -14,6 +14,7 @@ import Markdown
import TextFormat import TextFormat
import ComponentFlow import ComponentFlow
import MultilineTextComponent import MultilineTextComponent
import TextNodeWithEntities
private func actionButtonImage(color: UIColor) -> UIImage? { private func actionButtonImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in 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 shareAction: (() -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
let viewAction: (() -> Void)? let viewAction: (() -> Void)?
let openCallAction: (() -> Void)?
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public init( public init(
@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
shareAction: (() -> Void)?, shareAction: (() -> Void)?,
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?,
viewAction: (() -> Void)?, viewAction: (() -> Void)?,
openCallAction: (() -> Void)?,
tag: ItemListItemTag? = nil tag: ItemListItemTag? = nil
) { ) {
self.context = context self.context = context
@ -83,6 +86,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.shareAction = shareAction self.shareAction = shareAction
self.contextAction = contextAction self.contextAction = contextAction
self.viewAction = viewAction self.viewAction = viewAction
self.openCallAction = openCallAction
self.tag = tag self.tag = tag
} }
@ -147,7 +151,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
private var shimmerNode: ShimmerEffectNode? private var shimmerNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)? private var absoluteLocation: (CGRect, CGSize)?
private var justCreatedCallTextNode: TextNode? private var justCreatedCallTextNode: TextNodeWithEntities?
private var justCreatedCallLeftSeparatorLayer: SimpleLayer? private var justCreatedCallLeftSeparatorLayer: SimpleLayer?
private var justCreatedCallRightSeparatorLayer: SimpleLayer? private var justCreatedCallRightSeparatorLayer: SimpleLayer?
private var justCreatedCallSeparatorText: ComponentView<Empty>? private var justCreatedCallSeparatorText: ComponentView<Empty>?
@ -299,7 +303,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
public func asyncLayout() -> (_ item: ItemListPermanentInviteLinkItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { public func asyncLayout() -> (_ item: ItemListPermanentInviteLinkItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeAddressLayout = TextNode.asyncLayout(self.addressNode) let makeAddressLayout = TextNode.asyncLayout(self.addressNode)
let makeInvitedPeersLayout = TextNode.asyncLayout(self.invitedPeersNode) let makeInvitedPeersLayout = TextNode.asyncLayout(self.invitedPeersNode)
let makeJustCreatedCallTextNodeLayout = TextNode.asyncLayout(self.justCreatedCallTextNode) let makeJustCreatedCallTextNodeLayout = TextNodeWithEntities.asyncLayout(self.justCreatedCallTextNode)
let currentItem = self.item let currentItem = self.item
let avatarsContext = self.avatarsContext 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())) 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 { if item.isCall {
let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor) 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) shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
if let justCreatedCallTextNodeLayout { 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 { if strongSelf.justCreatedCallTextNode !== justCreatedCallTextNode {
strongSelf.justCreatedCallTextNode?.removeFromSupernode() strongSelf.justCreatedCallTextNode?.textNode.removeFromSupernode()
strongSelf.justCreatedCallTextNode = justCreatedCallTextNode strongSelf.justCreatedCallTextNode = justCreatedCallTextNode
//justCreatedCallTextNode.highlig strongSelf.addSubnode(justCreatedCallTextNode.textNode)
strongSelf.addSubnode(justCreatedCallTextNode)
} }
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)) 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<Empty> let justCreatedCallSeparatorText: ComponentView<Empty>
if let current = strongSelf.justCreatedCallSeparatorText { if let current = strongSelf.justCreatedCallSeparatorText {
@ -636,7 +662,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
} }
} else if let justCreatedCallTextNode = strongSelf.justCreatedCallTextNode { } else if let justCreatedCallTextNode = strongSelf.justCreatedCallTextNode {
strongSelf.justCreatedCallTextNode = nil strongSelf.justCreatedCallTextNode = nil
justCreatedCallTextNode.removeFromSupernode() justCreatedCallTextNode.textNode.removeFromSupernode()
strongSelf.justCreatedCallLeftSeparatorLayer?.removeFromSuperlayer() strongSelf.justCreatedCallLeftSeparatorLayer?.removeFromSuperlayer()
strongSelf.justCreatedCallLeftSeparatorLayer = nil strongSelf.justCreatedCallLeftSeparatorLayer = nil

View File

@ -655,7 +655,7 @@ public func channelMembersController(context: AccountContext, updatedPresentatio
}, inviteViaLink: { }, inviteViaLink: {
if let controller = getControllerImpl?() { if let controller = getControllerImpl?() {
dismissInputImpl?() 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 }, updateHideMembers: { value in
let _ = context.engine.peers.updateChannelMembersHidden(peerId: peerId, value: value).start() let _ = context.engine.peers.updateChannelMembersHidden(peerId: peerId, value: value).start()

View File

@ -655,6 +655,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
if let invite = invite { if let invite = invite {
arguments.openLink(invite) arguments.openLink(invite)
} }
}, openCallAction: {
}) })
case let .editablePublicLink(theme, _, placeholder, currentText): 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 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 { } else {
isGroup = true isGroup = true
} }
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil) presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
}) })
} }
}) })

View File

@ -35,9 +35,15 @@ private func shareQrCode(context: AccountContext, link: String, ecl: String, vie
} }
public final class QrCodeScreen: ViewController { public final class QrCodeScreen: ViewController {
public enum SubjectType {
case group
case channel
case groupCall
}
public enum Subject { public enum Subject {
case peer(peer: EnginePeer) case peer(peer: EnginePeer)
case invite(invite: ExportedInvitation, isGroup: Bool) case invite(invite: ExportedInvitation, type: SubjectType)
case chatFolder(slug: String) case chatFolder(slug: String)
var link: String { var link: String {
@ -239,9 +245,17 @@ public final class QrCodeScreen: ViewController {
let title: String let title: String
let text: String let text: String
switch subject { switch subject {
case let .invite(_, isGroup): case let .invite(_, type):
title = self.presentationData.strings.InviteLink_QRCode_Title 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: case .chatFolder:
title = self.presentationData.strings.InviteLink_QRCodeFolder_Title title = self.presentationData.strings.InviteLink_QRCodeFolder_Title
text = self.presentationData.strings.InviteLink_QRCodeFolder_Text text = self.presentationData.strings.InviteLink_QRCodeFolder_Text

View File

@ -1050,7 +1050,7 @@ private enum StatsEntry: ItemListNodeEntry {
arguments.copyBoostLink(link) arguments.copyBoostLink(link)
}, shareAction: { }, shareAction: {
arguments.shareBoostLink(link) arguments.shareBoostLink(link)
}, contextAction: nil, viewAction: nil, tag: nil) }, contextAction: nil, viewAction: nil, openCallAction: nil, tag: nil)
case let .boostersPlaceholder(_, text): case let .boostersPlaceholder(_, text):
return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks) return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks)
case let .boostGifts(theme, title): case let .boostGifts(theme, title):

View File

@ -384,6 +384,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) } dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) }
dict[-1945083841] = { return Api.InputGroupCall.parse_inputGroupCallInviteMessage($0) } dict[-1945083841] = { return Api.InputGroupCall.parse_inputGroupCallInviteMessage($0) }
dict[-33127873] = { return Api.InputGroupCall.parse_inputGroupCallSlug($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[887591921] = { return Api.InputInvoice.parse_inputInvoiceChatInviteSubscription($0) }
dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) }
dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) } dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) }

View File

@ -248,6 +248,7 @@ public extension Api {
} }
public extension Api { public extension Api {
indirect enum InputInvoice: TypeConstructorDescription { indirect enum InputInvoice: TypeConstructorDescription {
case inputInvoiceBusinessBotTransferStars(bot: Api.InputUser, stars: Int64)
case inputInvoiceChatInviteSubscription(hash: String) case inputInvoiceChatInviteSubscription(hash: String)
case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32)
case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption)
@ -260,6 +261,13 @@ public extension Api {
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { 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): case .inputInvoiceChatInviteSubscription(let hash):
if boxed { if boxed {
buffer.appendInt32(887591921) buffer.appendInt32(887591921)
@ -329,6 +337,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .inputInvoiceBusinessBotTransferStars(let bot, let stars):
return ("inputInvoiceBusinessBotTransferStars", [("bot", bot as Any), ("stars", stars as Any)])
case .inputInvoiceChatInviteSubscription(let hash): case .inputInvoiceChatInviteSubscription(let hash):
return ("inputInvoiceChatInviteSubscription", [("hash", hash as Any)]) return ("inputInvoiceChatInviteSubscription", [("hash", hash as Any)])
case .inputInvoiceMessage(let peer, let msgId): 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? { public static func parse_inputInvoiceChatInviteSubscription(_ reader: BufferReader) -> InputInvoice? {
var _1: String? var _1: String?
_1 = parseString(reader) _1 = parseString(reader)

View File

@ -10136,12 +10136,13 @@ public extension Api.functions.phone {
} }
} }
public extension Api.functions.phone { public extension Api.functions.phone {
static func inviteConferenceCallParticipant(call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) { static func inviteConferenceCallParticipant(flags: Int32, call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(1050474478) buffer.appendInt32(-1124981115)
serializeInt32(flags, buffer: buffer, boxed: false)
call.serialize(buffer, true) call.serialize(buffer, true)
userId.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) let reader = BufferReader(buffer)
var result: Api.Updates? var result: Api.Updates?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {

View File

@ -486,45 +486,86 @@ public final class CallController: ViewController {
var disablePeerIds: [EnginePeer.Id] = [] var disablePeerIds: [EnginePeer.Id] = []
disablePeerIds.append(self.call.context.account.peerId) disablePeerIds.append(self.call.context.account.peerId)
disablePeerIds.append(self.call.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 { guard let self else {
return return
} }
let _ = self.call.upgradeToConference(invitePeerIds: peerIds, completion: { _ in let _ = self.call.upgradeToConference(invitePeers: peers, completion: { _ in
}) })
}) })
self.push(controller) 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 //TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
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, context: context,
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
title: "Invite Members", mode: .generic,
mode: .peerSelection(searchChatList: true, searchGroups: false, searchChannels: false), 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 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 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 controller.navigationPresentation = .modal
let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
guard case let .result(peerIds, _) = result else { guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else {
controller?.dismiss()
return
}
if peerIds.isEmpty {
controller?.dismiss() controller?.dismiss()
return return
} }
@ -533,15 +574,15 @@ public final class CallController: ViewController {
controller?.dismiss() controller?.dismiss()
} }
let invitePeerIds = peerIds.compactMap { item -> EnginePeer.Id? in var isVideo = false
if case let .peer(peerId) = item { switch result.1 {
return peerId case .videoCall:
} else { isVideo = true
return nil default:
} break
} }
completion(invitePeerIds) completion([(peer.id, isVideo)])
}) })
return controller return controller

View File

@ -167,10 +167,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.conferenceAddParticipant?() self.conferenceAddParticipant?()
} }
var isConferencePossible = false var isConferencePossible = true
if self.call.context.sharedContext.immediateExperimentalUISettings.conferenceDebug {
isConferencePossible = true
}
if let data = self.call.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_enable_conference"] as? Double { if let data = self.call.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_enable_conference"] as? Double {
isConferencePossible = value != 0.0 isConferencePossible = value != 0.0
} }

View File

@ -234,6 +234,7 @@ public final class PresentationCallImpl: PresentationCall {
public let peerId: EnginePeer.Id public let peerId: EnginePeer.Id
public let isOutgoing: Bool public let isOutgoing: Bool
private let incomingConferenceSource: EngineMessage.Id? private let incomingConferenceSource: EngineMessage.Id?
private let conferenceStableId: Int64?
public var isVideo: Bool public var isVideo: Bool
public var isVideoPossible: Bool public var isVideoPossible: Bool
private let enableStunMarking: Bool private let enableStunMarking: Bool
@ -368,7 +369,7 @@ public final class PresentationCallImpl: PresentationCall {
return self.conferenceStatePromise.get() 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 localVideoEndpointId: String?
private var remoteVideoEndpointId: String? private var remoteVideoEndpointId: String?
@ -423,6 +424,11 @@ public final class PresentationCallImpl: PresentationCall {
self.peerId = peerId self.peerId = peerId
self.isOutgoing = isOutgoing self.isOutgoing = isOutgoing
self.incomingConferenceSource = incomingConferenceSource 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.isVideo = initialState?.type == .video
self.isVideoPossible = isVideoPossible self.isVideoPossible = isVideoPossible
self.enableStunMarking = enableStunMarking self.enableStunMarking = enableStunMarking
@ -445,19 +451,67 @@ public final class PresentationCallImpl: PresentationCall {
var didReceiveAudioOutputs = false var didReceiveAudioOutputs = false
var callSessionState: Signal<CallSession, NoError> = .complete() if let incomingConferenceSource = incomingConferenceSource {
if let initialState = initialState { self.sessionStateDisposable = (context.engine.data.subscribe(
callSessionState = .single(initialState) TelegramEngine.EngineData.Item.Messages.Message(id: incomingConferenceSource)
} )
callSessionState = callSessionState |> deliverOnMainQueue).startStrict(next: { [weak self] message in
|> then(callSessionManager.callState(internalId: internalId)) guard let self else {
return
}
self.sessionStateDisposable = (callSessionState let state: CallSessionState
|> deliverOnMainQueue).start(next: { [weak self] sessionState in if let message = message {
if let strongSelf = self { var foundAction: TelegramMediaAction?
strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl) 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<CallSession, NoError> = .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"] { if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_call_device"] {
self.sharedAudioContext = nil self.sharedAudioContext = nil
@ -933,15 +987,20 @@ public final class PresentationCallImpl: PresentationCall {
} }
let keyPair: TelegramKeyPair? = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() let keyPair: TelegramKeyPair? = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair()
guard let keyPair, let groupCall else { guard let keyPair, let groupCall else {
self.updateSessionState(sessionState: CallSession( self.sessionStateDisposable?.dispose()
id: self.internalId, self.updateSessionState(
stableId: nil, sessionState: CallSession(
isOutgoing: false, id: self.internalId,
type: .audio, stableId: self.conferenceStableId,
state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), isOutgoing: false,
isVideoPossible: true type: self.isVideo ? .video : .audio,
), state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()),
callContextState: nil, reception: nil, audioSessionControl: self.audioSessionControl) isVideoPossible: true
),
callContextState: nil,
reception: nil,
audioSessionControl: self.audioSessionControl
)
return return
} }
@ -967,14 +1026,15 @@ public final class PresentationCallImpl: PresentationCall {
keyPair: keyPair, keyPair: keyPair,
conferenceSourceId: self.internalId, conferenceSourceId: self.internalId,
isConference: true, isConference: true,
beginWithVideo: false,
sharedAudioContext: self.sharedAudioContext sharedAudioContext: self.sharedAudioContext
) )
self.conferenceCallImpl = conferenceCall self.conferenceCallImpl = conferenceCall
conferenceCall.upgradedConferenceCall = self conferenceCall.upgradedConferenceCall = self
conferenceCall.setConferenceInvitedPeers(self.pendingInviteToConferencePeerIds) conferenceCall.setConferenceInvitedPeers(self.pendingInviteToConferencePeerIds)
for peerId in self.pendingInviteToConferencePeerIds { for (peerId, isVideo) in self.pendingInviteToConferencePeerIds {
let _ = conferenceCall.invitePeer(peerId) let _ = conferenceCall.invitePeer(peerId, isVideo: isVideo)
} }
conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted) conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted)
@ -1067,9 +1127,10 @@ public final class PresentationCallImpl: PresentationCall {
guard let self else { guard let self else {
return return
} }
self.sessionStateDisposable?.dispose()
self.updateSessionState(sessionState: CallSession( self.updateSessionState(sessionState: CallSession(
id: self.internalId, id: self.internalId,
stableId: nil, stableId: self.conferenceStableId,
isOutgoing: false, isOutgoing: false,
type: .audio, type: .audio,
state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()),
@ -1341,11 +1402,12 @@ public final class PresentationCallImpl: PresentationCall {
if strongSelf.incomingConferenceSource != nil { if strongSelf.incomingConferenceSource != nil {
strongSelf.conferenceStateValue = .preparing strongSelf.conferenceStateValue = .preparing
strongSelf.isAcceptingIncomingConference = true strongSelf.isAcceptingIncomingConference = true
strongSelf.sessionStateDisposable?.dispose()
strongSelf.updateSessionState(sessionState: CallSession( strongSelf.updateSessionState(sessionState: CallSession(
id: strongSelf.internalId, id: strongSelf.internalId,
stableId: nil, stableId: strongSelf.conferenceStableId,
isOutgoing: false, isOutgoing: false,
type: .audio, type: strongSelf.isVideo ? .video : .audio,
state: .ringing, state: .ringing,
isVideoPossible: true isVideoPossible: true
), ),
@ -1365,9 +1427,10 @@ public final class PresentationCallImpl: PresentationCall {
if strongSelf.incomingConferenceSource != nil { if strongSelf.incomingConferenceSource != nil {
strongSelf.conferenceStateValue = .preparing strongSelf.conferenceStateValue = .preparing
strongSelf.isAcceptingIncomingConference = true strongSelf.isAcceptingIncomingConference = true
strongSelf.sessionStateDisposable?.dispose()
strongSelf.updateSessionState(sessionState: CallSession( strongSelf.updateSessionState(sessionState: CallSession(
id: strongSelf.internalId, id: strongSelf.internalId,
stableId: nil, stableId: strongSelf.conferenceStableId,
isOutgoing: false, isOutgoing: false,
type: .audio, type: .audio,
state: .ringing, state: .ringing,
@ -1552,7 +1615,7 @@ public final class PresentationCallImpl: PresentationCall {
self.videoCapturer?.setIsVideoEnabled(!isPaused) 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 { if self.isMovedToConference {
return EmptyDisposable return EmptyDisposable
} }
@ -1561,7 +1624,7 @@ public final class PresentationCallImpl: PresentationCall {
return EmptyDisposable return EmptyDisposable
} }
self.pendingInviteToConferencePeerIds = invitePeerIds self.pendingInviteToConferencePeerIds = invitePeers
let index = self.upgradedToConferenceCompletions.add({ call in let index = self.upgradedToConferenceCompletions.add({ call in
completion(call) completion(call)
}) })

View File

@ -850,6 +850,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
keyPair: nil, keyPair: nil,
conferenceSourceId: nil, conferenceSourceId: nil,
isConference: false, isConference: false,
beginWithVideo: false,
sharedAudioContext: nil sharedAudioContext: nil
) )
call.schedule(timestamp: timestamp) call.schedule(timestamp: timestamp)
@ -1076,6 +1077,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
keyPair: nil, keyPair: nil,
conferenceSourceId: nil, conferenceSourceId: nil,
isConference: false, isConference: false,
beginWithVideo: false,
sharedAudioContext: nil sharedAudioContext: nil
) )
self.updateCurrentGroupCall(.group(call)) self.updateCurrentGroupCall(.group(call))
@ -1085,16 +1087,13 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
accountContext: AccountContext, accountContext: AccountContext,
initialCall: EngineGroupCallDescription, initialCall: EngineGroupCallDescription,
reference: InternalGroupCallReference, reference: InternalGroupCallReference,
mode: JoinConferenceCallMode beginWithVideo: Bool
) { ) {
let keyPair: TelegramKeyPair let keyPair: TelegramKeyPair
switch mode { guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
case .joining: return
guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
return
}
keyPair = keyPairValue
} }
keyPair = keyPairValue
let call = PresentationGroupCallImpl( let call = PresentationGroupCallImpl(
accountContext: accountContext, accountContext: accountContext,
@ -1111,6 +1110,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
keyPair: keyPair, keyPair: keyPair,
conferenceSourceId: nil, conferenceSourceId: nil,
isConference: true, isConference: true,
beginWithVideo: beginWithVideo,
sharedAudioContext: nil sharedAudioContext: nil
) )
self.updateCurrentGroupCall(.group(call)) self.updateCurrentGroupCall(.group(call))

View File

@ -161,7 +161,8 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
isVideoEnabled: state.isVideoEnabled, isVideoEnabled: state.isVideoEnabled,
unmutedVideoLimit: state.unmutedVideoLimit, unmutedVideoLimit: state.unmutedVideoLimit,
isStream: state.isStream isStream: state.isStream,
isCreator: state.isCreator
), ),
topParticipants: topParticipants, topParticipants: topParticipants,
participantCount: state.totalCount, participantCount: state.totalCount,
@ -621,54 +622,98 @@ private final class PendingConferenceInvitationContext {
case ringing case ringing
} }
private let callSessionManager: CallSessionManager private let engine: TelegramEngine
private var requestDisposable: Disposable? private var requestDisposable: Disposable?
private var stateDisposable: 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 private var didNotifyEnded: Bool = false
init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, peerId: PeerId, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) { init(engine: TelegramEngine, reference: InternalGroupCallReference, peerId: PeerId, isVideo: Bool, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) {
self.callSessionManager = callSessionManager self.engine = engine
self.requestDisposable = (engine.calls.inviteConferenceCallParticipant(reference: reference, peerId: peerId, isVideo: isVideo).startStrict(next: { [weak self] messageId in
preconditionFailure()
/*self.requestDisposable = (callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: (groupCall, encryptionKey))
|> deliverOnMainQueue).startStrict(next: { [weak self] internalId in
guard let self else { guard let self else {
return 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) onStateUpdated(.ringing)
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
let timeout: Double = 30.0
let timerSignal = Signal<Void, NoError>.single(Void()) |> then(
Signal<Void, NoError>.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 { guard let self else {
return return
} }
switch state.state { if let message {
case let .requesting(ringing, _): self.hadMessage = true
if ringing { if message.timestamp + Int32(timeout) <= Int32(Date().timeIntervalSince1970) {
onStateUpdated(.ringing) 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, _): } else {
if !self.didNotifyEnded { if self.hadMessage || CFAbsoluteTimeGetCurrent() > startTime + 1.0 {
self.didNotifyEnded = true if !self.didNotifyEnded {
onEnded(reason == .ended(.switchedToConference)) self.didNotifyEnded = true
onEnded(false)
}
} }
default:
break
} }
}) })
})*/ }))
} }
deinit { deinit {
self.requestDisposable?.dispose() self.requestDisposable?.dispose()
self.stateDisposable?.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 let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void)
private(set) var initialCall: (description: EngineGroupCallDescription, reference: InternalGroupCallReference)? private(set) var initialCall: (description: EngineGroupCallDescription, reference: InternalGroupCallReference)?
public var currentReference: InternalGroupCallReference?
public let internalId: CallSessionInternalId public let internalId: CallSessionInternalId
public let peerId: EnginePeer.Id? public let peerId: EnginePeer.Id?
private let isChannel: Bool private let isChannel: Bool
@ -1121,6 +1167,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private let sharedAudioContext: SharedCallAudioContext? private let sharedAudioContext: SharedCallAudioContext?
public let isConference: Bool public let isConference: Bool
private let beginWithVideo: Bool
private let conferenceSourceId: CallSessionInternalId? private let conferenceSourceId: CallSessionInternalId?
public var conferenceSource: CallSessionInternalId? { public var conferenceSource: CallSessionInternalId? {
@ -1153,6 +1200,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
keyPair: TelegramKeyPair?, keyPair: TelegramKeyPair?,
conferenceSourceId: CallSessionInternalId?, conferenceSourceId: CallSessionInternalId?,
isConference: Bool, isConference: Bool,
beginWithVideo: Bool,
sharedAudioContext: SharedCallAudioContext? sharedAudioContext: SharedCallAudioContext?
) { ) {
self.account = accountContext.account self.account = accountContext.account
@ -1162,6 +1210,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.getDeviceAccessData = getDeviceAccessData self.getDeviceAccessData = getDeviceAccessData
self.initialCall = initialCall self.initialCall = initialCall
self.currentReference = initialCall?.reference
self.callId = initialCall?.description.id self.callId = initialCall?.description.id
self.internalId = internalId self.internalId = internalId
@ -1183,6 +1232,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.isStream = isStream self.isStream = isStream
self.conferenceSourceId = conferenceSourceId self.conferenceSourceId = conferenceSourceId
self.isConference = isConference self.isConference = isConference
self.beginWithVideo = beginWithVideo
self.keyPair = keyPair self.keyPair = keyPair
if let keyPair, let initialCall { if let keyPair, let initialCall {
@ -1490,6 +1540,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath) strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath)
})*/ })*/
if beginWithVideo {
self.requestVideo()
}
} }
deinit { deinit {
@ -1912,7 +1966,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
defaultParticipantsAreMuted: callInfo.defaultParticipantsAreMuted ?? state.defaultParticipantsAreMuted, defaultParticipantsAreMuted: callInfo.defaultParticipantsAreMuted ?? state.defaultParticipantsAreMuted,
isVideoEnabled: callInfo.isVideoEnabled, isVideoEnabled: callInfo.isVideoEnabled,
unmutedVideoLimit: callInfo.unmutedVideoLimit, unmutedVideoLimit: callInfo.unmutedVideoLimit,
isStream: callInfo.isStream isStream: callInfo.isStream,
isCreator: callInfo.isCreator
)), audioSessionControl: self.audioSessionControl) )), audioSessionControl: self.audioSessionControl)
} else { } else {
self.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( self.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
@ -1928,7 +1983,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
isVideoEnabled: state.isVideoEnabled, isVideoEnabled: state.isVideoEnabled,
unmutedVideoLimit: state.unmutedVideoLimit, unmutedVideoLimit: state.unmutedVideoLimit,
isStream: callInfo.isStream isStream: callInfo.isStream,
isCreator: callInfo.isCreator
)))) ))))
self.summaryParticipantsState.set(.single(SummaryParticipantsState( self.summaryParticipantsState.set(.single(SummaryParticipantsState(
@ -2250,6 +2306,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
guard let self else { guard let self else {
return return
} }
self.currentReference = .id(id: joinCallResult.callInfo.id, accessHash: joinCallResult.callInfo.accessHash)
let clientParams = joinCallResult.jsonParams let clientParams = joinCallResult.jsonParams
if let data = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] { 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] { if let video = dict["video"] as? [String: Any] {
@ -2859,7 +2918,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
isVideoEnabled: state.isVideoEnabled, isVideoEnabled: state.isVideoEnabled,
unmutedVideoLimit: state.unmutedVideoLimit, unmutedVideoLimit: state.unmutedVideoLimit,
isStream: callInfo.isStream isStream: callInfo.isStream,
isCreator: callInfo.isCreator
)))) ))))
self.summaryParticipantsState.set(.single(SummaryParticipantsState( self.summaryParticipantsState.set(.single(SummaryParticipantsState(
@ -2937,6 +2997,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
result.append(OngoingGroupCallContext.MediaChannelDescription( result.append(OngoingGroupCallContext.MediaChannelDescription(
kind: .audio, kind: .audio,
peerId: participant.peer.id.id._internalGetInt64Value(),
audioSsrc: audioSsrc, audioSsrc: audioSsrc,
videoDescription: nil videoDescription: nil
)) ))
@ -2948,6 +3009,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
result.append(OngoingGroupCallContext.MediaChannelDescription( result.append(OngoingGroupCallContext.MediaChannelDescription(
kind: .audio, kind: .audio,
peerId: participant.peer.id.id._internalGetInt64Value(),
audioSsrc: screencastSsrc, audioSsrc: screencastSsrc,
videoDescription: nil 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 { if self.isConference {
guard let initialCall = self.initialCall else { guard let initialCall = self.initialCall else {
return false return false
} }
//TODO:release if self.conferenceInvitationContexts[peerId] != nil {
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 {
return false return false
} }
var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)? var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)?
var onEnded: ((Bool) -> Void)? var onEnded: ((Bool) -> Void)?
var didEndAlready = false var didEndAlready = false
let invitationContext = PendingConferenceInvitationContext( let invitationContext = PendingConferenceInvitationContext(
callSessionManager: self.accountContext.account.callSessionManager, engine: self.accountContext.engine,
groupCall: GroupCallReference(id: initialCall.id, accessHash: initialCall.accessHash), reference: initialCall.reference,
peerId: peerId, peerId: peerId,
isVideo: isVideo,
onStateUpdated: { state in onStateUpdated: { state in
onStateUpdated?(state) onStateUpdated?(state)
}, },
@ -3906,7 +3963,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
} }
return false*/ return false
} else { } else {
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else { guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else {
return false return false
@ -3922,7 +3979,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
} }
func setConferenceInvitedPeers(_ peerIds: [PeerId]) { func setConferenceInvitedPeers(_ invitedPeers: [(id: PeerId, isVideo: Bool)]) {
//TODO:release //TODO:release
/*self.invitedPeersValue = peerIds.map { /*self.invitedPeersValue = peerIds.map {
PresentationGroupCallInvitedPeer(id: $0, state: .requesting) PresentationGroupCallInvitedPeer(id: $0, state: .requesting)
@ -3933,6 +3990,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
var updatedInvitedPeers = self.invitedPeersValue var updatedInvitedPeers = self.invitedPeersValue
updatedInvitedPeers.removeAll(where: { $0.id == peerId}) updatedInvitedPeers.removeAll(where: { $0.id == peerId})
self.invitedPeersValue = updatedInvitedPeers 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) { public func updateTitle(_ title: String) {

View File

@ -5,6 +5,179 @@ import ComponentFlow
import MultilineTextComponent import MultilineTextComponent
import BalancedTextComponent import BalancedTextComponent
import TelegramPresentationData 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<Empty>()
private var pendingContainerView: UIView?
private var pendingEmojiViews: [ComponentView<Empty>] = []
private var emojiView: ComponentView<Empty>?
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<Empty>, 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<Empty>
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<Empty>
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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class VideoChatEncryptionKeyComponent: Component { final class VideoChatEncryptionKeyComponent: Component {
let theme: PresentationTheme let theme: PresentationTheme
@ -119,7 +292,7 @@ final class VideoChatEncryptionKeyComponent: Component {
let expandedButtonTopInset: CGFloat = 12.0 let expandedButtonTopInset: CGFloat = 12.0
let expandedButtonBottomInset: CGFloat = 13.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<Empty> let emojiItem: ComponentView<Empty>
if self.emojiItems.count > i { if self.emojiItems.count > i {
emojiItem = self.emojiItems[i] emojiItem = self.emojiItems[i]
@ -128,9 +301,9 @@ final class VideoChatEncryptionKeyComponent: Component {
self.emojiItems.append(emojiItem) self.emojiItems.append(emojiItem)
} }
return emojiItem.update( return emojiItem.update(
transition: .immediate, transition: transition,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(EmojiItemComponent(
text: .plain(NSAttributedString(string: component.emoji[i], font: Font.regular(40.0), textColor: .white)) emoji: i < component.emoji.count ? component.emoji[i] : nil
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 200.0, height: 200.0) containerSize: CGSize(width: 200.0, height: 200.0)

View File

@ -7,16 +7,24 @@ import TelegramPresentationData
import BundleIconComponent import BundleIconComponent
final class VideoChatListInviteComponent: Component { final class VideoChatListInviteComponent: Component {
enum Icon {
case addUser
case link
}
let title: String let title: String
let icon: Icon
let theme: PresentationTheme let theme: PresentationTheme
let action: () -> Void let action: () -> Void
init( init(
title: String, title: String,
icon: Icon,
theme: PresentationTheme, theme: PresentationTheme,
action: @escaping () -> Void action: @escaping () -> Void
) { ) {
self.title = title self.title = title
self.icon = icon
self.theme = theme self.theme = theme
self.action = action self.action = action
} }
@ -25,6 +33,9 @@ final class VideoChatListInviteComponent: Component {
if lhs.title != rhs.title { if lhs.title != rhs.title {
return false return false
} }
if lhs.icon != rhs.icon {
return false
}
if lhs.theme !== rhs.theme { if lhs.theme !== rhs.theme {
return false return false
} }
@ -116,10 +127,17 @@ final class VideoChatListInviteComponent: Component {
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) 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( let iconSize = self.icon.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(BundleIconComponent( component: AnyComponent(BundleIconComponent(
name: "Chat/Context Menu/AddUser", name: iconName,
tintColor: component.theme.list.itemAccentColor tintColor: component.theme.list.itemAccentColor
)), )),
environment: {}, environment: {},

View File

@ -39,23 +39,33 @@ final class VideoChatParticipantsComponent: Component {
} }
final class Participants: Equatable { final class Participants: Equatable {
enum InviteType { enum InviteType: Equatable {
case invite case invite(isMultipleUsers: Bool)
case shareLink 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 myPeerId: EnginePeer.Id
let participants: [GroupCallParticipantsContext.Participant] let participants: [GroupCallParticipantsContext.Participant]
let totalCount: Int let totalCount: Int
let loadMoreToken: String? 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.myPeerId = myPeerId
self.participants = participants self.participants = participants
self.totalCount = totalCount self.totalCount = totalCount
self.loadMoreToken = loadMoreToken self.loadMoreToken = loadMoreToken
self.inviteType = inviteType self.inviteOptions = inviteOptions
} }
static func ==(lhs: Participants, rhs: Participants) -> Bool { static func ==(lhs: Participants, rhs: Participants) -> Bool {
@ -74,7 +84,7 @@ final class VideoChatParticipantsComponent: Component {
if lhs.loadMoreToken != rhs.loadMoreToken { if lhs.loadMoreToken != rhs.loadMoreToken {
return false return false
} }
if lhs.inviteType != rhs.inviteType { if lhs.inviteOptions != rhs.inviteOptions {
return false return false
} }
return true return true
@ -142,7 +152,7 @@ final class VideoChatParticipantsComponent: Component {
let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void
let updateIsMainParticipantPinned: (Bool) -> Void let updateIsMainParticipantPinned: (Bool) -> Void
let updateIsExpandedUIHidden: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void
let openInviteMembers: () -> Void let openInviteMembers: (Participants.InviteType) -> Void
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
init( init(
@ -162,7 +172,7 @@ final class VideoChatParticipantsComponent: Component {
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void, updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
updateIsMainParticipantPinned: @escaping (Bool) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void,
updateIsExpandedUIHidden: @escaping (Bool) -> Void, updateIsExpandedUIHidden: @escaping (Bool) -> Void,
openInviteMembers: @escaping () -> Void, openInviteMembers: @escaping (Participants.InviteType) -> Void,
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
) { ) {
self.call = call self.call = call
@ -379,14 +389,14 @@ final class VideoChatParticipantsComponent: Component {
let sideInset: CGFloat let sideInset: CGFloat
let itemCount: Int let itemCount: Int
let itemHeight: CGFloat 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.containerSize = containerSize
self.sideInset = sideInset self.sideInset = sideInset
self.itemCount = itemCount self.itemCount = itemCount
self.itemHeight = itemHeight self.itemHeight = itemHeight
self.trailingItemHeight = trailingItemHeight self.trailingItemHeights = trailingItemHeights
} }
func frame(at index: Int) -> CGRect { func frame(at index: Int) -> CGRect {
@ -394,8 +404,15 @@ final class VideoChatParticipantsComponent: Component {
return frame return frame
} }
func trailingItemFrame() -> CGRect { func trailingItemFrame(index: Int) -> 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)) 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 { func contentHeight() -> CGFloat {
@ -403,7 +420,9 @@ final class VideoChatParticipantsComponent: Component {
if self.itemCount != 0 { if self.itemCount != 0 {
result = self.frame(at: self.itemCount - 1).maxY result = self.frame(at: self.itemCount - 1).maxY
} }
result += self.trailingItemHeight for height in self.trailingItemHeights {
result += height
}
return result return result
} }
@ -439,7 +458,7 @@ final class VideoChatParticipantsComponent: Component {
let scrollClippingFrame: CGRect let scrollClippingFrame: CGRect
let separateVideoScrollClippingFrame: 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.containerSize = containerSize
self.layout = layout self.layout = layout
self.isUIHidden = isUIHidden 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.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 self.spacing = 4.0
if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden { if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden {
@ -568,8 +587,8 @@ final class VideoChatParticipantsComponent: Component {
} }
} }
func listTrailingItemFrame() -> CGRect { func listTrailingItemFrame(index: Int) -> CGRect {
return self.list.trailingItemFrame() return self.list.trailingItemFrame(index: index)
} }
} }
@ -641,7 +660,7 @@ final class VideoChatParticipantsComponent: Component {
private var listParticipants: [GroupCallParticipantsContext.Participant] = [] private var listParticipants: [GroupCallParticipantsContext.Participant] = []
private let measureListItemView = ComponentView<Empty>() private let measureListItemView = ComponentView<Empty>()
private let inviteListItemView = ComponentView<Empty>() private var inviteListItemViews: [Int: ComponentView<Empty>] = [:]
private var gridItemViews: [VideoParticipantKey: GridItem] = [:] private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
private let gridItemViewContainer: UIView private let gridItemViewContainer: UIView
@ -1270,7 +1289,7 @@ final class VideoChatParticipantsComponent: Component {
case .requesting: case .requesting:
subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral) subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral)
case .ringing: case .ringing:
subtitle = PeerListItemComponent.Subtitle(text: "ringing...", color: .neutral) subtitle = PeerListItemComponent.Subtitle(text: "invited", color: .neutral)
} }
peerItemComponent = PeerListItemComponent( peerItemComponent = PeerListItemComponent(
@ -1381,11 +1400,15 @@ final class VideoChatParticipantsComponent: Component {
self.listItemViews.removeValue(forKey: itemId) 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 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 let itemComponentView = itemView.view {
if itemComponentView.superview == nil { if itemComponentView.superview == nil {
@ -1395,6 +1418,17 @@ final class VideoChatParticipantsComponent: Component {
itemTransition.setFrame(view: itemComponentView, frame: itemFrame) 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.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)) 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) containerSize: CGSize(width: availableSize.width, height: 1000.0)
) )
let inviteText: String var inviteListItemSizes: [CGSize] = []
if let participants = component.participants, let inviteType = participants.inviteType { for (inviteOption) in component.participants?.inviteOptions ?? [] {
switch inviteType { let inviteText: String
case .invite: let iconType: VideoChatListInviteComponent.Icon
inviteText = component.strings.VoiceChat_InviteMember switch inviteOption.type {
case let .invite(isMultiple):
//TODO:localize
if isMultiple {
inviteText = component.strings.VoiceChat_InviteMember
} else {
inviteText = "Add Member"
}
iconType = .addUser
case .shareLink: case .shareLink:
inviteText = component.strings.VoiceChat_Share inviteText = component.strings.VoiceChat_Share
iconType = .link
} }
} else {
inviteText = component.strings.VoiceChat_InviteMember let inviteListItemView: ComponentView<Empty>
} var inviteListItemTransition = transition
let inviteListItemSize = self.inviteListItemView.update( if let current = self.inviteListItemViews[inviteOption.id] {
transition: transition, inviteListItemView = current
component: AnyComponent(VideoChatListInviteComponent( } else {
title: inviteText, inviteListItemView = ComponentView()
theme: component.theme, self.inviteListItemViews[inviteOption.id] = inviteListItemView
action: { [weak self] in inviteListItemTransition = inviteListItemTransition.withAnimation(.none)
guard let self, let component = self.component else { }
return
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 gridParticipants: [VideoParticipant] = []
var listParticipants: [GroupCallParticipantsContext.Participant] = [] var listParticipants: [GroupCallParticipantsContext.Participant] = []
@ -1824,7 +1877,7 @@ final class VideoChatParticipantsComponent: Component {
gridItemCount: gridParticipants.count, gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count + component.invitedPeers.count, listItemCount: listParticipants.count + component.invitedPeers.count,
listItemHeight: measureListItemSize.height, listItemHeight: measureListItemSize.height,
listTrailingItemHeight: inviteListItemSize.height listTrailingItemHeights: inviteListItemSizes.map(\.height)
) )
self.itemLayout = itemLayout self.itemLayout = itemLayout

View File

@ -25,6 +25,7 @@ import LegacyComponents
import TooltipUI import TooltipUI
import BlurredBackgroundComponent import BlurredBackgroundComponent
import CallsEmoji import CallsEmoji
import InviteLinksUI
extension VideoChatCall { extension VideoChatCall {
var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> { var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> {
@ -653,6 +654,51 @@ final class VideoChatScreenComponent: Component {
return 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 let formatSendTitle: (String) -> String = { string in
var string = string var string = string
if string.contains("[") && string.contains("]") { if string.contains("[") && string.contains("]") {
@ -705,7 +751,7 @@ final class VideoChatScreenComponent: Component {
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) 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 { guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return return
} }
@ -1051,7 +1097,7 @@ final class VideoChatScreenComponent: Component {
static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [InvitedPeer]), NoError> { static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [InvitedPeer]), NoError> {
let invitedPeers = conferenceSource.context.engine.data.subscribe( 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 let accountPeerId = conferenceSource.context.account.peerId
@ -1759,12 +1805,19 @@ final class VideoChatScreenComponent: Component {
} }
} }
} }
var inviteType: VideoChatParticipantsComponent.Participants.InviteType? var inviteOptions: [VideoChatParticipantsComponent.Participants.InviteOption] = []
if canInvite { if case let .group(groupCall) = self.currentCall, groupCall.isConference {
if inviteIsLink { inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: .invite(isMultipleUsers: false)))
inviteType = .shareLink inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 1, type: .shareLink))
} else { } else {
inviteType = .invite 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, participants: members.participants,
totalCount: members.totalCount, totalCount: members.totalCount,
loadMoreToken: members.loadMoreToken, loadMoreToken: members.loadMoreToken,
inviteType: inviteType inviteOptions: inviteOptions
) )
} }
@ -2038,7 +2091,13 @@ final class VideoChatScreenComponent: Component {
} }
var encryptionKeyFrame: CGRect? 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 navigationHeight -= 2.0
let encryptionKey: ComponentView<Empty> let encryptionKey: ComponentView<Empty>
var encryptionKeyTransition = transition var encryptionKeyTransition = transition
@ -2055,7 +2114,7 @@ final class VideoChatScreenComponent: Component {
component: AnyComponent(VideoChatEncryptionKeyComponent( component: AnyComponent(VideoChatEncryptionKeyComponent(
theme: environment.theme, theme: environment.theme,
strings: environment.strings, strings: environment.strings,
emoji: encryptionKeyEmoji, emoji: self.encryptionKeyEmoji ?? [],
isExpanded: self.isEncryptionKeyExpanded, isExpanded: self.isEncryptionKeyExpanded,
tapAction: { [weak self] in tapAction: { [weak self] in
guard let self else { guard let self else {
@ -2326,11 +2385,18 @@ final class VideoChatScreenComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4)) self.state?.updated(transition: .spring(duration: 0.4))
} }
}, },
openInviteMembers: { [weak self] in openInviteMembers: { [weak self] type in
guard let self else { guard let self else {
return 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 visibleParticipantsUpdated: { [weak self] visibleParticipants in
guard let self else { guard let self else {

View File

@ -16,31 +16,6 @@ extension VideoChatScreenComponent.View {
return 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 { if groupCall.isConference {
var disablePeerIds: [EnginePeer.Id] = [] var disablePeerIds: [EnginePeer.Id] = []
disablePeerIds.append(groupCall.accountContext.account.peerId) 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 { guard let self, case let .group(groupCall) = self.currentCall else {
return return
} }
for peerId in peerIds { for peerId in peerIds {
let _ = groupCall.invitePeer(peerId) let _ = groupCall.invitePeer(peerId.id, isVideo: peerId.isVideo)
} }
}) })
self.environment?.controller()?.push(controller) self.environment?.controller()?.push(controller)
@ -80,7 +63,7 @@ extension VideoChatScreenComponent.View {
if inviteIsLink { if inviteIsLink {
inviteType = .shareLink inviteType = .shareLink
} else { } else {
inviteType = .invite inviteType = .invite(isMultipleUsers: true)
} }
} }
@ -146,7 +129,8 @@ extension VideoChatScreenComponent.View {
if let participant { if let participant {
dismissController?() dismissController?()
if groupCall.invitePeer(participant.peer.id) { //TODO:release
if groupCall.invitePeer(participant.peer.id, isVideo: false) {
let text: String let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info { 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 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?() dismissController?()
if groupCall.invitePeer(peer.id) { //TODO:release
if groupCall.invitePeer(peer.id, isVideo: false) {
let text: String let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info { 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 text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
@ -330,7 +315,8 @@ extension VideoChatScreenComponent.View {
} }
dismissController?() dismissController?()
if groupCall.invitePeer(peer.id) { //TODO:release
if groupCall.invitePeer(peer.id, isVideo: false) {
let text: String let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info { 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 text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string

View File

@ -1256,7 +1256,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
if let participant = participant { if let participant = participant {
dismissController?() dismissController?()
if strongSelf.call.invitePeer(participant.peer.id) { //TODO:release
if strongSelf.call.invitePeer(participant.peer.id, isVideo: false) {
let text: String let text: String
if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { 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 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?() dismissController?()
if strongSelf.call.invitePeer(peer.id) { //TODO:release
if strongSelf.call.invitePeer(peer.id, isVideo: false) {
let text: String let text: String
if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { 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 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?() dismissController?()
if strongSelf.call.invitePeer(peer.id) { //TODO:release
if strongSelf.call.invitePeer(peer.id, isVideo: false) {
let text: String let text: String
if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { 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 text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string

View File

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

View File

@ -201,12 +201,28 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))
case let .messageActionPaidMessagesPrice(stars): case let .messageActionPaidMessagesPrice(stars):
return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars)) return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars))
case let .messageActionConferenceCall(_, callId, duration, otherParticipants): case let .messageActionConferenceCall(flags, callId, duration, otherParticipants):
return TelegramMediaAction(action: .conferenceCall( 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, callId: callId,
duration: duration, duration: duration,
flags: mappedFlags,
otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? [] otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? []
)) )))
} }
} }

View File

@ -382,7 +382,7 @@ private final class CallSessionContext {
private final class IncomingConferenceInvitationContext { private final class IncomingConferenceInvitationContext {
enum State: Equatable { enum State: Equatable {
case pending case pending
case ringing(callId: Int64, otherParticipants: [EnginePeer]) case ringing(callId: Int64, isVideo: Bool, otherParticipants: [EnginePeer])
case stopped case stopped
} }
@ -420,11 +420,11 @@ private final class IncomingConferenceInvitationContext {
} }
} }
if let action = foundAction, case let .conferenceCall(callId, duration, otherParticipants) = action.action { if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action {
if duration != nil { if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil {
state = .stopped state = .stopped
} else { } 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) return message.peers[id].flatMap(EnginePeer.init)
}) })
} }
@ -639,11 +639,11 @@ private final class CallSessionManagerContext {
} }
} }
for (id, context) in self.incomingConferenceInvitationContexts { for (id, context) in self.incomingConferenceInvitationContexts {
if case let .ringing(_, otherParticipants) = context.state { if case let .ringing(_, isVideo, otherParticipants) = context.state {
ringingContexts.append(CallSessionRingingState( ringingContexts.append(CallSessionRingingState(
id: context.internalId, id: context.internalId,
peerId: id.peerId, peerId: id.peerId,
isVideo: false, isVideo: isVideo,
isVideoPossible: true, isVideoPossible: true,
conferenceSource: id, conferenceSource: id,
otherParticipants: otherParticipants 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<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates {
addUpdates(updates)
}
return .complete()
}
self.rejectConferenceInvitationDisposables.add(rejectSignal.startStrict())
}
func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal<String?, NoError>) { func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal<String?, NoError>) {
for (id, context) in self.incomingConferenceInvitationContexts { for (id, context) in self.incomingConferenceInvitationContexts {
if context.internalId == internalId { 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) { func drop(stableId: CallSessionStableId, reason: DropCallReason) {
self.withContext { context in self.withContext { context in
context.drop(stableId: stableId, reason: reason) context.drop(stableId: stableId, reason: reason)

View File

@ -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 unknown
case groupCreated(title: String) case groupCreated(title: String)
case addedMembers(peerIds: [PeerId]) 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 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 paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64) case paidMessagesPriceEdited(stars: Int64)
case conferenceCall(callId: Int64, duration: Int32?, otherParticipants: [PeerId]) case conferenceCall(ConferenceCall)
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
@ -264,7 +289,12 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 47: case 47:
self = .paidMessagesPriceEdited(stars: decoder.decodeInt64ForKey("stars", orElse: 0)) self = .paidMessagesPriceEdited(stars: decoder.decodeInt64ForKey("stars", orElse: 0))
case 48: 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: default:
self = .unknown self = .unknown
} }
@ -642,15 +672,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case let .paidMessagesPriceEdited(stars): case let .paidMessagesPriceEdited(stars):
encoder.encodeInt32(47, forKey: "_rawValue") encoder.encodeInt32(47, forKey: "_rawValue")
encoder.encodeInt64(stars, forKey: "stars") encoder.encodeInt64(stars, forKey: "stars")
case let .conferenceCall(callId, duration, otherParticipants): case let .conferenceCall(conferenceCall):
encoder.encodeInt32(48, forKey: "_rawValue") encoder.encodeInt32(48, forKey: "_rawValue")
encoder.encodeInt64(callId, forKey: "cid") encoder.encodeInt64(conferenceCall.callId, forKey: "cid")
if let duration { if let duration = conferenceCall.duration {
encoder.encodeInt32(duration, forKey: "dur") encoder.encodeInt32(duration, forKey: "dur")
} else { } else {
encoder.encodeNil(forKey: "dur") 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")
} }
} }

View File

@ -96,6 +96,7 @@ public struct GroupCallInfo: Equatable {
public var isVideoEnabled: Bool public var isVideoEnabled: Bool
public var unmutedVideoLimit: Int public var unmutedVideoLimit: Int
public var isStream: Bool public var isStream: Bool
public var isCreator: Bool
public init( public init(
id: Int64, id: Int64,
@ -110,7 +111,8 @@ public struct GroupCallInfo: Equatable {
defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?,
isVideoEnabled: Bool, isVideoEnabled: Bool,
unmutedVideoLimit: Int, unmutedVideoLimit: Int,
isStream: Bool isStream: Bool,
isCreator: Bool
) { ) {
self.id = id self.id = id
self.accessHash = accessHash self.accessHash = accessHash
@ -125,6 +127,7 @@ public struct GroupCallInfo: Equatable {
self.isVideoEnabled = isVideoEnabled self.isVideoEnabled = isVideoEnabled
self.unmutedVideoLimit = unmutedVideoLimit self.unmutedVideoLimit = unmutedVideoLimit
self.isStream = isStream 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), defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: (flags & (1 << 1)) != 0, canChange: (flags & (1 << 2)) != 0),
isVideoEnabled: (flags & (1 << 9)) != 0, isVideoEnabled: (flags & (1 << 9)) != 0,
unmutedVideoLimit: Int(unmutedVideoLimit), unmutedVideoLimit: Int(unmutedVideoLimit),
isStream: (flags & (1 << 12)) != 0 isStream: (flags & (1 << 12)) != 0,
isCreator: (flags & (1 << 15)) != 0
) )
case .groupCallDiscarded: case .groupCallDiscarded:
return nil return nil
@ -454,17 +458,17 @@ public enum GetGroupCallParticipantsError {
func _internal_getGroupCallParticipants(account: Account, reference: InternalGroupCallReference, offset: String, ssrcs: [UInt32], limit: Int32, sortAscending: Bool?) -> Signal<GroupCallParticipantsContext.State, GetGroupCallParticipantsError> { func _internal_getGroupCallParticipants(account: Account, reference: InternalGroupCallReference, offset: String, ssrcs: [UInt32], limit: Int32, sortAscending: Bool?) -> Signal<GroupCallParticipantsContext.State, GetGroupCallParticipantsError> {
let accountPeerId = account.peerId 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) sortAscendingValue = _internal_getCurrentGroupCall(account: account, reference: reference)
|> mapError { _ -> GetGroupCallParticipantsError in |> mapError { _ -> GetGroupCallParticipantsError in
return .generic 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 { guard let result = result else {
return .fail(.generic) 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( return combineLatest(
@ -481,7 +485,7 @@ func _internal_getGroupCallParticipants(account: Account, reference: InternalGro
let version: Int32 let version: Int32
let nextParticipantsFetchOffset: String? let nextParticipantsFetchOffset: String?
let (sortAscendingValue, scheduleTimestamp, subscribedToScheduled, defaultParticipantsAreMuted, isVideoEnabled, unmutedVideoLimit, isStream) = sortAscendingAndScheduleTimestamp let (sortAscendingValue, scheduleTimestamp, subscribedToScheduled, defaultParticipantsAreMuted, isVideoEnabled, unmutedVideoLimit, isStream, isCreator) = sortAscendingAndScheduleTimestamp
switch result { switch result {
case let .groupParticipants(count, participants, nextOffset, chats, users, apiVersion): case let .groupParticipants(count, participants, nextOffset, chats, users, apiVersion):
@ -506,7 +510,7 @@ func _internal_getGroupCallParticipants(account: Account, reference: InternalGro
participants: parsedParticipants, participants: parsedParticipants,
nextParticipantsFetchOffset: nextParticipantsFetchOffset, nextParticipantsFetchOffset: nextParticipantsFetchOffset,
adminIds: Set(), adminIds: Set(),
isCreator: false, isCreator: isCreator,
defaultParticipantsAreMuted: defaultParticipantsAreMuted ?? GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), defaultParticipantsAreMuted: defaultParticipantsAreMuted ?? GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false),
sortAscending: sortAscendingValue, sortAscending: sortAscendingValue,
recordingStartTimestamp: nil, 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<Never, NoError> { func _internal_inviteConferenceCallParticipant(account: Account, reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<MessageId?, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(peerId).flatMap(apiInputUser) return transaction.getPeer(peerId).flatMap(apiInputUser)
} }
|> mapToSignal { inputPeer -> Signal<Never, NoError> in |> mapToSignal { inputPeer -> Signal<MessageId?, NoError> in
guard let inputPeer else { guard let inputPeer else {
return .complete() 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) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in |> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil) return .single(nil)
} }
|> mapToSignal { result -> Signal<Never, NoError> in |> mapToSignal { result -> Signal<MessageId?, NoError> in
if let result { if let result {
account.stateManager.addUpdates(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<GroupCallInviteLinks, RevokeConferenceInviteLinkError> {
return account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 1, call: reference.apiInputGroupCall, joinMuted: .boolFalse))
|> mapError { _ -> RevokeConferenceInviteLinkError in
return .generic
}
|> mapToSignal { result -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> in
account.stateManager.addUpdates(result)
return _internal_groupCallInviteLinks(account: account, reference: reference, isConference: true)
|> castError(RevokeConferenceInviteLinkError.self)
|> mapToSignal { result -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> in
guard let result = result else {
return .fail(.generic)
}
return .single(result)
}
}
}
public enum ConfirmAddConferenceParticipantError { public enum ConfirmAddConferenceParticipantError {
case generic case generic
} }

View File

@ -97,6 +97,10 @@ public extension TelegramEngine {
return _internal_createConferenceCall(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId) return _internal_createConferenceCall(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId)
} }
public func revokeConferenceInviteLink(reference: InternalGroupCallReference, link: String) -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> {
return _internal_revokeConferenceInviteLink(account: self.account, reference: reference, link: link)
}
public func pollConferenceCallBlockchain(reference: InternalGroupCallReference, subChainId: Int, offset: Int, limit: Int) -> Signal<(blocks: [Data], nextOffset: Int)?, NoError> { 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) 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) return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block)
} }
public func inviteConferenceCallParticipant(callId: Int64, accessHash: Int64, peerId: EnginePeer.Id) -> Signal<Never, NoError> { public func inviteConferenceCallParticipant(reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<EngineMessage.Id?, NoError> {
return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId) return _internal_inviteConferenceCallParticipant(account: self.account, reference: reference, peerId: peerId, isVideo: isVideo)
} }
public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> { public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {

View File

@ -10,6 +10,7 @@ swift_library(
"-warnings-as-errors", "-warnings-as-errors",
], ],
deps = [ deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit", "//submodules/AsyncDisplayKit",
"//submodules/Display", "//submodules/Display",
"//submodules/TelegramCore", "//submodules/TelegramCore",

View File

@ -9,6 +9,7 @@ import AppBundle
import ChatMessageBubbleContentNode import ChatMessageBubbleContentNode
import ChatMessageItemCommon import ChatMessageItemCommon
import ChatMessageDateAndStatusNode import ChatMessageDateAndStatusNode
import SwiftSignalKit
private let titleFont: UIFont = Font.medium(16.0) private let titleFont: UIFont = Font.medium(16.0)
private let labelFont: UIFont = Font.regular(13.0) private let labelFont: UIFont = Font.regular(13.0)
@ -25,6 +26,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let buttonNode: HighlightableButtonNode private let buttonNode: HighlightableButtonNode
private var activeConferenceUpdateTimer: SwiftSignalKit.Timer?
required public init() { required public init() {
self.titleNode = TextNode() self.titleNode = TextNode()
self.labelNode = TextNode() self.labelNode = TextNode()
@ -57,6 +60,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside) self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside)
} }
deinit {
self.activeConferenceUpdateTimer?.invalidate()
}
override public func accessibilityActivate() -> Bool { override public func accessibilityActivate() -> Bool {
self.callButtonPressed() self.callButtonPressed()
return true return true
@ -90,6 +97,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
var callDuration: Int32? var callDuration: Int32?
var callSuccessful = true var callSuccessful = true
var isVideo = false var isVideo = false
var hasCallButton = true
var updateConferenceTimerEndTimeout: Int32?
for media in item.message.media { for media in item.message.media {
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action { if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
isVideo = isVideoValue isVideo = isVideoValue
@ -123,11 +132,32 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
break break
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(_, duration, _) = action.action { } else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
isVideo = false isVideo = conferenceCall.flags.contains(.isVideo)
callDuration = duration callDuration = conferenceCall.duration
//TODO:localize //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 break
} }
} }
@ -211,7 +241,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom 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.width, { boundingWidth in
return (boundingSize, { [weak self] animation, _, _ in return (boundingSize, { [weak self] animation, _, _ in
@ -234,6 +266,22 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
if let buttonImage = buttonImage { if let buttonImage = buttonImage {
strongSelf.buttonNode.setImage(buttonImage, for: []) 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.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 { 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) { if self.buttonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore) return ChatMessageBubbleContentTapAction(content: .ignore)
} else if self.bounds.contains(point), let item = self.item { } else if self.bounds.contains(point), let item = self.item {

View File

@ -405,7 +405,7 @@ private final class JoinSubjectScreenComponent: Component {
isStream: false isStream: false
), ),
reference: .link(slug: groupCall.slug), reference: .link(slug: groupCall.slug),
mode: .joining beginWithVideo: false
) )
self.environment?.controller()?.dismiss() self.environment?.controller()?.dismiss()

View File

@ -14125,7 +14125,7 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa
createInviteLinkImpl = { [weak contactsController] in createInviteLinkImpl = { [weak contactsController] in
contactsController?.view.window?.endEditing(true) 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) parentController?.push(contactsController)

View File

@ -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() { public init() {
self.textNode = TextNode() self.textNode = TextNode()
} }
@ -301,6 +315,83 @@ public final class TextNodeWithEntities {
self.inlineStickerItemLayers.removeValue(forKey: key) 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 { public class ImmediateTextNodeWithEntities: TextNode {

View File

@ -398,6 +398,8 @@ final class AuthorizedApplicationContext {
if let action = media as? TelegramMediaAction { if let action = media as? TelegramMediaAction {
if case .messageAutoremoveTimeoutUpdated = action.action { if case .messageAutoremoveTimeoutUpdated = action.action {
return return
} else if case .conferenceCall = action.action {
return
} }
} }
} }

View File

@ -2890,14 +2890,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
break break
} }
} }
guard case let .conferenceCall(callId, duration, _) = action?.action else { guard case let .conferenceCall(conferenceCall) = action?.action else {
return return
} }
if duration != nil { if conferenceCall.duration != nil {
return 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() self.context.sharedContext.navigateToCurrentCall()
return return
} }
@ -2919,7 +2919,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
isStream: false isStream: false
), ),
reference: .message(id: message.id), reference: .message(id: message.id),
mode: .joining beginWithVideo: conferenceCall.flags.contains(.isVideo)
) )
}) })
}, longTap: { [weak self] action, params in }, longTap: { [weak self] action, params in

View File

@ -60,6 +60,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
} }
private let confirmation: (ContactListPeer) -> Signal<Bool, NoError> private let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
private let isPeerEnabled: (ContactListPeer) -> Bool
var dismissed: (() -> Void)? var dismissed: (() -> Void)?
var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in } var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in }
@ -107,6 +108,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.displayDeviceContacts = params.displayDeviceContacts self.displayDeviceContacts = params.displayDeviceContacts
self.displayCallIcons = params.displayCallIcons self.displayCallIcons = params.displayCallIcons
self.confirmation = params.confirmation self.confirmation = params.confirmation
self.isPeerEnabled = params.isPeerEnabled
self.multipleSelection = params.multipleSelection self.multipleSelection = params.multipleSelection
self.requirePhoneNumbers = params.requirePhoneNumbers self.requirePhoneNumbers = params.requirePhoneNumbers
self.allowChannelsInSearch = params.allowChannelsInSearch self.allowChannelsInSearch = params.allowChannelsInSearch
@ -218,7 +220,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
} }
override func loadDisplayNode() { 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._ready.set(self.contactsNode.contactListNode.ready)
self.contactsNode.navigationBar = self.navigationBar self.contactsNode.navigationBar = self.navigationBar

View File

@ -44,6 +44,8 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var cancelSearch: (() -> Void)? var cancelSearch: (() -> Void)?
var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)? var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)?
let isPeerEnabled: (ContactListPeer) -> Bool
var presentationData: PresentationData { var presentationData: PresentationData {
didSet { didSet {
self.presentationDataPromise.set(.single(self.presentationData)) self.presentationDataPromise.set(.single(self.presentationData))
@ -57,12 +59,13 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var searchContainerNode: ContactsSearchContainerNode? 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.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.displayDeviceContacts = displayDeviceContacts self.displayDeviceContacts = displayDeviceContacts
self.displayCallIcons = displayCallIcons self.displayCallIcons = displayCallIcons
self.allowChannelsInSearch = allowChannelsInSearch self.allowChannelsInSearch = allowChannelsInSearch
self.isPeerEnabled = isPeerEnabled
var excludeSelf = true var excludeSelf = true
@ -124,7 +127,9 @@ final class ContactSelectionControllerNode: ASDisplayNode {
} }
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? 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) contextActionImpl?(peer, node, gesture, nil)
} : nil, multipleSelection: multipleSelection) } : nil, multipleSelection: multipleSelection)

View File

@ -402,6 +402,15 @@ func openResolvedUrlImpl(
members: resolvedCallLink.members, members: resolvedCallLink.members,
totalMemberCount: resolvedCallLink.totalMemberCount 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): case let .localization(identifier):
dismissInput() dismissInput()
@ -788,6 +797,7 @@ func openResolvedUrlImpl(
} }
if let currentState = starsContext.currentState, currentState.balance >= StarsAmount(value: amount, nanos: 0) { if let currentState = starsContext.currentState, currentState.balance >= StarsAmount(value: amount, nanos: 0) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let controller = UndoOverlayController( let controller = UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,
content: .universal( content: .universal(

View File

@ -270,11 +270,13 @@ public final class OngoingGroupCallContext {
} }
public var kind: Kind public var kind: Kind
public var peerId: Int64
public var audioSsrc: UInt32 public var audioSsrc: UInt32
public var videoDescription: String? 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.kind = kind
self.peerId = peerId
self.audioSsrc = audioSsrc self.audioSsrc = audioSsrc
self.videoDescription = videoDescription self.videoDescription = videoDescription
} }
@ -575,6 +577,7 @@ public final class OngoingGroupCallContext {
} }
return OngoingGroupCallMediaChannelDescription( return OngoingGroupCallMediaChannelDescription(
type: mappedType, type: mappedType,
peerId: channel.peerId,
audioSsrc: channel.audioSsrc, audioSsrc: channel.audioSsrc,
videoDescription: channel.videoDescription videoDescription: channel.videoDescription
) )
@ -688,6 +691,7 @@ public final class OngoingGroupCallContext {
} }
return OngoingGroupCallMediaChannelDescription( return OngoingGroupCallMediaChannelDescription(
type: mappedType, type: mappedType,
peerId: channel.peerId,
audioSsrc: channel.audioSsrc, audioSsrc: channel.audioSsrc,
videoDescription: channel.videoDescription videoDescription: channel.videoDescription
) )

View File

@ -332,10 +332,12 @@ typedef NS_ENUM(int32_t, OngoingGroupCallMediaChannelType) {
@interface OngoingGroupCallMediaChannelDescription : NSObject @interface OngoingGroupCallMediaChannelDescription : NSObject
@property (nonatomic, readonly) OngoingGroupCallMediaChannelType type; @property (nonatomic, readonly) OngoingGroupCallMediaChannelType type;
@property (nonatomic, readonly) uint64_t peerId;
@property (nonatomic, readonly) uint32_t audioSsrc; @property (nonatomic, readonly) uint32_t audioSsrc;
@property (nonatomic, strong, readonly) NSString * _Nullable videoDescription; @property (nonatomic, strong, readonly) NSString * _Nullable videoDescription;
- (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type - (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type
peerId:(int64_t)peerId
audioSsrc:(uint32_t)audioSsrc audioSsrc:(uint32_t)audioSsrc
videoDescription:(NSString * _Nullable)videoDescription; videoDescription:(NSString * _Nullable)videoDescription;

View File

@ -2670,7 +2670,7 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
}, },
.createWrappedAudioDeviceModule = [audioDeviceModule, isActiveByDefault](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> { .createWrappedAudioDeviceModule = [audioDeviceModule, isActiveByDefault](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> {
if (audioDeviceModule) { if (audioDeviceModule) {
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault); auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault || true);
return result; return result;
} else { } else {
return nullptr; return nullptr;
@ -3029,11 +3029,13 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
@implementation OngoingGroupCallMediaChannelDescription @implementation OngoingGroupCallMediaChannelDescription
- (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type - (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type
audioSsrc:(uint32_t)audioSsrc peerId:(int64_t)peerId
videoDescription:(NSString * _Nullable)videoDescription { audioSsrc:(uint32_t)audioSsrc
videoDescription:(NSString * _Nullable)videoDescription {
self = [super init]; self = [super init];
if (self != nil) { if (self != nil) {
_type = type; _type = type;
_peerId = peerId;
_audioSsrc = audioSsrc; _audioSsrc = audioSsrc;
_videoDescription = videoDescription; _videoDescription = videoDescription;
} }

@ -1 +1 @@
Subproject commit 16cb0d29562576be221a7ac2b8bdd81fdb954bc4 Subproject commit a15014304d25193157ee809e8faceaca95dd8192