mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
13669eee96
@ -22,6 +22,7 @@ internal:
|
||||
- export PATH=/opt/homebrew/opt/ruby/bin:$PATH
|
||||
- export PATH=`gem environment gemdir`/bin:$PATH
|
||||
- python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appcenter-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64
|
||||
- python3 -u build-system/Make/DeployToFirebase.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/firebase-configurations/firebase-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
|
||||
- python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
|
||||
environment:
|
||||
name: internal
|
||||
|
@ -11,25 +11,26 @@ def is_apple_silicon():
|
||||
return False
|
||||
|
||||
|
||||
def get_clean_env():
|
||||
def get_clean_env(use_clean_env=True):
|
||||
clean_env = os.environ.copy()
|
||||
clean_env['PATH'] = '/usr/bin:/bin:/usr/sbin:/sbin'
|
||||
if use_clean_env:
|
||||
clean_env['PATH'] = '/usr/bin:/bin:/usr/sbin:/sbin'
|
||||
return clean_env
|
||||
|
||||
|
||||
def resolve_executable(program):
|
||||
def resolve_executable(program, use_clean_env=True):
|
||||
def is_executable(fpath):
|
||||
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
||||
|
||||
for path in get_clean_env()["PATH"].split(os.pathsep):
|
||||
for path in get_clean_env(use_clean_env=use_clean_env)["PATH"].split(os.pathsep):
|
||||
executable_file = os.path.join(path, program)
|
||||
if is_executable(executable_file):
|
||||
return executable_file
|
||||
return None
|
||||
|
||||
|
||||
def run_executable_with_output(path, arguments, decode=True, input=None, stderr_to_stdout=True, print_command=False, check_result=False):
|
||||
executable_path = resolve_executable(path)
|
||||
def run_executable_with_output(path, arguments, use_clean_env=True, decode=True, input=None, stderr_to_stdout=True, print_command=False, check_result=False):
|
||||
executable_path = resolve_executable(path, use_clean_env=use_clean_env)
|
||||
if executable_path is None:
|
||||
raise Exception('Could not resolve {} to a valid executable file'.format(path))
|
||||
|
||||
@ -45,7 +46,7 @@ def run_executable_with_output(path, arguments, decode=True, input=None, stderr_
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=stderr_assignment,
|
||||
stdin=subprocess.PIPE,
|
||||
env=get_clean_env()
|
||||
env=get_clean_env(use_clean_env=use_clean_env)
|
||||
)
|
||||
if input is not None:
|
||||
output_data, _ = process.communicate(input=input)
|
||||
|
81
build-system/Make/DeployToFirebase.py
Normal file
81
build-system/Make/DeployToFirebase.py
Normal 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)
|
@ -111,10 +111,27 @@ public final class ContactSelectionControllerParams {
|
||||
public let requirePhoneNumbers: Bool
|
||||
public let allowChannelsInSearch: Bool
|
||||
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||
public let isPeerEnabled: (ContactListPeer) -> Bool
|
||||
public let openProfile: ((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.updatedPresentationData = updatedPresentationData
|
||||
self.mode = mode
|
||||
@ -127,6 +144,7 @@ public final class ContactSelectionControllerParams {
|
||||
self.requirePhoneNumbers = requirePhoneNumbers
|
||||
self.allowChannelsInSearch = allowChannelsInSearch
|
||||
self.confirmation = confirmation
|
||||
self.isPeerEnabled = isPeerEnabled
|
||||
self.openProfile = openProfile
|
||||
self.sendMessage = sendMessage
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ public protocol PresentationCall: AnyObject {
|
||||
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
||||
func debugInfo() -> Signal<(String, String), NoError>
|
||||
|
||||
func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable
|
||||
func upgradeToConference(invitePeers: [(id: EnginePeer.Id, isVideo: Bool)], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable
|
||||
|
||||
func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
|
||||
}
|
||||
@ -423,6 +423,7 @@ public protocol PresentationGroupCall: AnyObject {
|
||||
var internalId: CallSessionInternalId { get }
|
||||
var peerId: EnginePeer.Id? { get }
|
||||
var callId: Int64? { get }
|
||||
var currentReference: InternalGroupCallReference? { get }
|
||||
|
||||
var hasVideo: Bool { get }
|
||||
var hasScreencast: Bool { get }
|
||||
@ -484,7 +485,7 @@ public protocol PresentationGroupCall: AnyObject {
|
||||
|
||||
func updateTitle(_ title: String)
|
||||
|
||||
func invitePeer(_ peerId: EnginePeer.Id) -> Bool
|
||||
func invitePeer(_ peerId: EnginePeer.Id, isVideo: Bool) -> Bool
|
||||
func removedPeer(_ peerId: EnginePeer.Id)
|
||||
var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> { get }
|
||||
|
||||
@ -550,10 +551,6 @@ public enum PresentationCurrentCall: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum JoinConferenceCallMode {
|
||||
case joining
|
||||
}
|
||||
|
||||
public protocol PresentationCallManager: AnyObject {
|
||||
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
||||
var currentGroupCallSignal: Signal<VideoChatCall?, NoError> { get }
|
||||
@ -568,6 +565,6 @@ public protocol PresentationCallManager: AnyObject {
|
||||
accountContext: AccountContext,
|
||||
initialCall: EngineGroupCallDescription,
|
||||
reference: InternalGroupCallReference,
|
||||
mode: JoinConferenceCallMode
|
||||
beginWithVideo: Bool
|
||||
)
|
||||
}
|
||||
|
@ -297,18 +297,19 @@ public final class AvatarNode: ASDisplayNode {
|
||||
private struct Params: Equatable {
|
||||
let peerId: EnginePeer.Id?
|
||||
let resourceId: String?
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
let displayDimensions: CGSize
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
|
||||
init(
|
||||
peerId: EnginePeer.Id?,
|
||||
resourceId: String?,
|
||||
clipStyle: AvatarNodeClipStyle,
|
||||
displayDimensions: CGSize
|
||||
displayDimensions: CGSize,
|
||||
clipStyle: AvatarNodeClipStyle
|
||||
) {
|
||||
self.peerId = peerId
|
||||
self.resourceId = resourceId
|
||||
self.clipStyle = clipStyle
|
||||
self.displayDimensions = displayDimensions
|
||||
self.clipStyle = clipStyle
|
||||
}
|
||||
}
|
||||
|
||||
@ -663,8 +664,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
let params = Params(
|
||||
peerId: peer?.id,
|
||||
resourceId: smallProfileImage?.resource.id.stringRepresentation,
|
||||
clipStyle: clipStyle,
|
||||
displayDimensions: displayDimensions
|
||||
displayDimensions: displayDimensions,
|
||||
clipStyle: clipStyle
|
||||
)
|
||||
if self.params == params {
|
||||
return
|
||||
|
@ -138,16 +138,7 @@ class CallListCallItem: ListViewItem {
|
||||
|
||||
func selected(listView: ListView) {
|
||||
listView.clearHighlightAnimated(true)
|
||||
var isVideo = false
|
||||
for media in self.topMessage.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, _, _, isVideoValue) = action.action {
|
||||
isVideo = isVideoValue
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
self.interaction.call(self.topMessage.id.peerId, isVideo)
|
||||
self.interaction.call(self.topMessage)
|
||||
}
|
||||
|
||||
static func mergeType(item: CallListCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
|
||||
@ -262,15 +253,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||
guard let item = self?.layoutParams?.0 else {
|
||||
return false
|
||||
}
|
||||
var isVideo = false
|
||||
for media in item.topMessage.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, _, _, isVideoValue) = action.action {
|
||||
isVideo = isVideoValue
|
||||
}
|
||||
}
|
||||
}
|
||||
item.interaction.call(item.topMessage.id.peerId, isVideo)
|
||||
item.interaction.call(item.topMessage)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -390,6 +373,9 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||
var hadDuration = false
|
||||
var callDuration: Int32?
|
||||
|
||||
var isConference = false
|
||||
var conferenceIsDeclined = false
|
||||
|
||||
for message in item.messages {
|
||||
inner: for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
@ -411,6 +397,36 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||
} else {
|
||||
callDuration = nil
|
||||
}
|
||||
} else if case let .conferenceCall(conferenceCall) = action.action {
|
||||
isConference = true
|
||||
|
||||
isVideo = conferenceCall.flags.contains(.isVideo)
|
||||
if message.flags.contains(.Incoming) {
|
||||
hasIncoming = true
|
||||
//TODO:localize
|
||||
let missedTimeout: Int32
|
||||
#if DEBUG
|
||||
missedTimeout = 5
|
||||
#else
|
||||
missedTimeout = 30
|
||||
#endif
|
||||
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||
if conferenceCall.flags.contains(.isMissed) {
|
||||
titleColor = item.presentationData.theme.list.itemDestructiveColor
|
||||
conferenceIsDeclined = true
|
||||
} else if message.timestamp < currentTime - missedTimeout {
|
||||
titleColor = item.presentationData.theme.list.itemDestructiveColor
|
||||
hasMissed = true
|
||||
}
|
||||
} else {
|
||||
hasOutgoing = true
|
||||
}
|
||||
if callDuration == nil && !hadDuration {
|
||||
hadDuration = true
|
||||
callDuration = conferenceCall.duration
|
||||
} else {
|
||||
callDuration = nil
|
||||
}
|
||||
}
|
||||
break inner
|
||||
}
|
||||
@ -441,7 +457,18 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||
titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor)
|
||||
}
|
||||
|
||||
if hasMissed {
|
||||
if isConference {
|
||||
//TODO:localize
|
||||
if conferenceIsDeclined {
|
||||
statusAttributedString = NSAttributedString(string: "Declined Group Call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
} else if hasMissed {
|
||||
statusAttributedString = NSAttributedString(string: "Missed Group Call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
} else {
|
||||
statusAttributedString = NSAttributedString(string: "Group call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
|
||||
statusAccessibilityString = statusAttributedString?.string ?? ""
|
||||
} else if hasMissed {
|
||||
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallMissedShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
statusAccessibilityString = isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallMissed : item.presentationData.strings.Call_VoiceOver_VoiceCallMissed
|
||||
} else if hasIncoming && hasOutgoing {
|
||||
|
@ -92,6 +92,7 @@ public final class CallListController: TelegramBaseController {
|
||||
|
||||
private let createActionDisposable = MetaDisposable()
|
||||
private let clearDisposable = MetaDisposable()
|
||||
private var createConferenceCallDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, mode: CallListControllerMode) {
|
||||
self.context = context
|
||||
@ -163,6 +164,7 @@ public final class CallListController: TelegramBaseController {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
self.peerViewDisposable.dispose()
|
||||
self.clearDisposable.dispose()
|
||||
self.createConferenceCallDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
@ -210,11 +212,16 @@ public final class CallListController: TelegramBaseController {
|
||||
guard !self.presentAccountFrozenInfoIfNeeded() else {
|
||||
return
|
||||
}
|
||||
let _ = (self.context.engine.calls.createConferenceCall()
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] call in
|
||||
if self.createConferenceCallDisposable != nil {
|
||||
return
|
||||
}
|
||||
self.createConferenceCallDisposable = (self.context.engine.calls.createConferenceCall()
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] call in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.createConferenceCallDisposable?.dispose()
|
||||
self.createConferenceCallDisposable = nil
|
||||
|
||||
let openCall: () -> Void = { [weak self] in
|
||||
guard let self else {
|
||||
@ -231,38 +238,55 @@ public final class CallListController: TelegramBaseController {
|
||||
isStream: false
|
||||
),
|
||||
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
|
||||
mode: .joining
|
||||
beginWithVideo: false
|
||||
)
|
||||
}
|
||||
|
||||
let controller = InviteLinkInviteController(context: self.context, updatedPresentationData: nil, mode: .groupCall(link: call.link, isRecentlyCreated: true), parentNavigationController: self.navigationController as? NavigationController, completed: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let result {
|
||||
switch result {
|
||||
case .linkCopied:
|
||||
//TODO:localize
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: "View Call", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
openCall()
|
||||
}
|
||||
return false
|
||||
}), in: .window(.root))
|
||||
case .openCall:
|
||||
openCall()
|
||||
let controller = InviteLinkInviteController(
|
||||
context: self.context,
|
||||
updatedPresentationData: nil,
|
||||
mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(callId: call.callInfo.id, accessHash: call.callInfo.accessHash, isRecentlyCreated: true, canRevoke: true)),
|
||||
initialInvite: .link(link: call.link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil),
|
||||
parentNavigationController: self.navigationController as? NavigationController,
|
||||
completed: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let result {
|
||||
switch result {
|
||||
case .linkCopied:
|
||||
//TODO:localize
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: "View Call", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
openCall()
|
||||
}
|
||||
return false
|
||||
}), in: .window(.root))
|
||||
case .openCall:
|
||||
openCall()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
self.present(controller, in: .window(.root), with: nil)
|
||||
})
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = CallListControllerNode(controller: self, context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] peerId, isVideo in
|
||||
if let strongSelf = self {
|
||||
strongSelf.call(peerId, isVideo: isVideo)
|
||||
self.displayNode = CallListControllerNode(controller: self, context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] message in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, _, _, isVideo) = action.action {
|
||||
self.call(message.id.peerId, isVideo: isVideo)
|
||||
} else if case .conferenceCall = action.action {
|
||||
self.openGroupCall(message: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, joinGroupCall: { [weak self] peerId, activeCall in
|
||||
if let self {
|
||||
@ -573,6 +597,48 @@ public final class CallListController: TelegramBaseController {
|
||||
}))
|
||||
}
|
||||
|
||||
private func openGroupCall(message: EngineMessage) {
|
||||
var action: TelegramMediaAction?
|
||||
for media in message.media {
|
||||
if let media = media as? TelegramMediaAction {
|
||||
action = media
|
||||
break
|
||||
}
|
||||
}
|
||||
guard case let .conferenceCall(conferenceCall) = action?.action else {
|
||||
return
|
||||
}
|
||||
if conferenceCall.duration != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if let currentGroupCallController = self.context.sharedContext as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == conferenceCall.callId {
|
||||
self.context.sharedContext.navigateToCurrentCall()
|
||||
return
|
||||
}
|
||||
|
||||
let signal = self.context.engine.peers.joinCallInvitationInformation(messageId: message.id)
|
||||
let _ = (signal
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] resolvedCallLink in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.callManager?.joinConferenceCall(
|
||||
accountContext: self.context,
|
||||
initialCall: EngineGroupCallDescription(
|
||||
id: resolvedCallLink.id,
|
||||
accessHash: resolvedCallLink.accessHash,
|
||||
title: nil,
|
||||
scheduleTimestamp: nil,
|
||||
subscribedToScheduled: false,
|
||||
isStream: false
|
||||
),
|
||||
reference: .message(id: message.id),
|
||||
beginWithVideo: conferenceCall.flags.contains(.isVideo)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_StartNewCall, icon: { theme in
|
||||
|
@ -62,14 +62,14 @@ private extension EngineCallList.Item {
|
||||
|
||||
final class CallListNodeInteraction {
|
||||
let setMessageIdWithRevealedOptions: (EngineMessage.Id?, EngineMessage.Id?) -> Void
|
||||
let call: (EnginePeer.Id, Bool) -> Void
|
||||
let call: (EngineMessage) -> Void
|
||||
let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
|
||||
let delete: ([EngineMessage.Id]) -> Void
|
||||
let updateShowCallsTab: (Bool) -> Void
|
||||
let openGroupCall: (EnginePeer.Id) -> Void
|
||||
let createGroupCall: () -> Void
|
||||
|
||||
init(setMessageIdWithRevealedOptions: @escaping (EngineMessage.Id?, EngineMessage.Id?) -> Void, call: @escaping (EnginePeer.Id, Bool) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, delete: @escaping ([EngineMessage.Id]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void, openGroupCall: @escaping (EnginePeer.Id) -> Void, createGroupCall: @escaping () -> Void) {
|
||||
init(setMessageIdWithRevealedOptions: @escaping (EngineMessage.Id?, EngineMessage.Id?) -> Void, call: @escaping (EngineMessage) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, delete: @escaping ([EngineMessage.Id]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void, openGroupCall: @escaping (EnginePeer.Id) -> Void, createGroupCall: @escaping () -> Void) {
|
||||
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
|
||||
self.call = call
|
||||
self.openInfo = openInfo
|
||||
@ -222,7 +222,7 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
private let emptyButtonIconNode: ASImageNode
|
||||
private let emptyButtonTextNode: ImmediateTextNode
|
||||
|
||||
private let call: (EnginePeer.Id, Bool) -> Void
|
||||
private let call: (EngineMessage) -> Void
|
||||
private let joinGroupCall: (EnginePeer.Id, EngineGroupCallDescription) -> Void
|
||||
private let createGroupCall: () -> Void
|
||||
private let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
|
||||
@ -234,7 +234,7 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
|
||||
private var previousContentOffset: ListViewVisibleContentOffset?
|
||||
|
||||
init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EnginePeer.Id, Bool) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, createGroupCall: @escaping () -> Void) {
|
||||
init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EngineMessage) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, createGroupCall: @escaping () -> Void) {
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
@ -333,8 +333,8 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, call: { [weak self] peerId, isVideo in
|
||||
self?.call(peerId, isVideo)
|
||||
}, call: { [weak self] message in
|
||||
self?.call(message)
|
||||
}, openInfo: { [weak self] peerId, messages in
|
||||
self?.openInfo(peerId, messages)
|
||||
}, delete: { [weak self] messageIds in
|
||||
@ -519,10 +519,7 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
|
||||
let canCreateGroupCall = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App())
|
||||
|> map { configuration -> Bool in
|
||||
var isConferencePossible = false
|
||||
if context.sharedContext.immediateExperimentalUISettings.conferenceDebug {
|
||||
isConferencePossible = true
|
||||
}
|
||||
var isConferencePossible = true
|
||||
if let data = configuration.data, let value = data["ios_enable_conference"] as? Double {
|
||||
isConferencePossible = value != 0.0
|
||||
}
|
||||
|
@ -692,6 +692,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
||||
var reallyHighlighted = self.isHighlighted
|
||||
if let item = self.item, !item.enabled {
|
||||
reallyHighlighted = false
|
||||
}
|
||||
let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0
|
||||
if let item = self.item {
|
||||
switch item.peer {
|
||||
@ -1649,6 +1652,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
actionButtonNode.setImage(actionButton.image, for: .normal)
|
||||
transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionButtonImage.size.width - offset, y: floor((nodeLayout.contentSize.height - actionButtonImage.size.height) / 2.0)), size: actionButtonImage.size))
|
||||
|
||||
actionButtonNode.isEnabled = item.enabled
|
||||
actionButtonNode.alpha = item.enabled ? 1.0 : 0.4
|
||||
|
||||
offset += actionButtonImage.size.width + 12.0
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case enableReactionOverrides(Bool)
|
||||
case compressedEmojiCache(Bool)
|
||||
case storiesJpegExperiment(Bool)
|
||||
case conferenceDebug(Bool)
|
||||
case checkSerializedData(Bool)
|
||||
case enableQuickReactionSwitch(Bool)
|
||||
case disableReloginTokens(Bool)
|
||||
@ -134,7 +133,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.web.rawValue
|
||||
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .conferenceDebug, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation:
|
||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .logTranslationRecognition, .resetTranslationStates:
|
||||
return DebugControllerSection.translation.rawValue
|
||||
@ -243,8 +242,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 47
|
||||
case .disableReloginTokens:
|
||||
return 48
|
||||
case .conferenceDebug:
|
||||
return 49
|
||||
case .checkSerializedData:
|
||||
return 50
|
||||
case .enableQuickReactionSwitch:
|
||||
@ -1311,16 +1308,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .conferenceDebug(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Conference Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||
settings.conferenceDebug = value
|
||||
return PreferencesEntry(settings)
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .checkSerializedData(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Check Serialized Data", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
@ -1552,7 +1539,6 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
entries.append(.storiesJpegExperiment(experimentalSettings.storiesJpegExperiment))
|
||||
entries.append(.disableReloginTokens(experimentalSettings.disableReloginTokens))
|
||||
|
||||
entries.append(.conferenceDebug(experimentalSettings.conferenceDebug))
|
||||
entries.append(.checkSerializedData(experimentalSettings.checkSerializedData))
|
||||
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
|
||||
entries.append(.liveStreamV2(experimentalSettings.liveStreamV2))
|
||||
|
@ -364,7 +364,7 @@ public final class TextNodeLayout: NSObject {
|
||||
fileprivate let backgroundColor: UIColor?
|
||||
fileprivate let constrainedSize: CGSize
|
||||
fileprivate let explicitAlignment: NSTextAlignment
|
||||
fileprivate let resolvedAlignment: NSTextAlignment
|
||||
public let resolvedAlignment: NSTextAlignment
|
||||
fileprivate let verticalAlignment: TextVerticalAlignment
|
||||
fileprivate let lineSpacing: CGFloat
|
||||
fileprivate let cutout: TextNodeCutout?
|
||||
|
@ -27,13 +27,15 @@ class InviteLinkInviteInteraction {
|
||||
let copyLink: (ExportedInvitation) -> Void
|
||||
let shareLink: (ExportedInvitation) -> Void
|
||||
let manageLinks: () -> Void
|
||||
let openCallAction: () -> Void
|
||||
|
||||
init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void) {
|
||||
init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void, openCallAction: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.mainLinkContextAction = mainLinkContextAction
|
||||
self.copyLink = copyLink
|
||||
self.shareLink = shareLink
|
||||
self.manageLinks = manageLinks
|
||||
self.openCallAction = openCallAction
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +133,8 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
|
||||
}, contextAction: { node, gesture in
|
||||
interaction.mainLinkContextAction(invitation, node, gesture)
|
||||
}, viewAction: {
|
||||
}, openCallAction: {
|
||||
interaction.openCallAction()
|
||||
})
|
||||
case let .manage(text, standalone):
|
||||
return InviteLinkInviteManageItem(theme: presentationData.theme, text: text, standalone: standalone, action: {
|
||||
@ -150,14 +154,32 @@ private func preparedTransition(from fromEntries: [InviteLinkInviteEntry], to to
|
||||
return InviteLinkInviteTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading)
|
||||
}
|
||||
|
||||
private func getBackgroundColor(theme: PresentationTheme) -> UIColor {
|
||||
return theme.actionSheet.opaqueItemBackgroundColor
|
||||
}
|
||||
|
||||
public final class InviteLinkInviteController: ViewController {
|
||||
private var controllerNode: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
public struct GroupCall {
|
||||
public let callId: Int64
|
||||
public let accessHash: Int64
|
||||
public let isRecentlyCreated: Bool
|
||||
public let canRevoke: Bool
|
||||
|
||||
public init(callId: Int64, accessHash: Int64, isRecentlyCreated: Bool, canRevoke: Bool) {
|
||||
self.callId = callId
|
||||
self.accessHash = accessHash
|
||||
self.isRecentlyCreated = isRecentlyCreated
|
||||
self.canRevoke = canRevoke
|
||||
}
|
||||
}
|
||||
|
||||
case groupOrChannel(peerId: EnginePeer.Id)
|
||||
case groupCall(link: String, isRecentlyCreated: Bool)
|
||||
case groupCall(GroupCall)
|
||||
}
|
||||
|
||||
public enum CompletionResult {
|
||||
@ -169,6 +191,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
|
||||
private let context: AccountContext
|
||||
private let mode: Mode
|
||||
private let initialInvite: ExportedInvitation?
|
||||
private weak var parentNavigationController: NavigationController?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
@ -176,9 +199,10 @@ public final class InviteLinkInviteController: ViewController {
|
||||
|
||||
fileprivate let completed: ((CompletionResult?) -> Void)?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: Mode, parentNavigationController: NavigationController?, completed: ((CompletionResult?) -> Void)? = nil) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: Mode, initialInvite: ExportedInvitation?, parentNavigationController: NavigationController?, completed: ((CompletionResult?) -> Void)? = nil) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.initialInvite = initialInvite
|
||||
self.parentNavigationController = parentNavigationController
|
||||
self.completed = completed
|
||||
|
||||
@ -211,7 +235,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, presentationData: self.presentationData, mode: self.mode, controller: self)
|
||||
self.displayNode = Node(context: self.context, presentationData: self.presentationData, mode: self.mode, controller: self, initialInvite: self.initialInvite)
|
||||
}
|
||||
|
||||
private var didAppearOnce: Bool = false
|
||||
@ -292,7 +316,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
|
||||
private var revokeDisposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, mode: InviteLinkInviteController.Mode, controller: InviteLinkInviteController) {
|
||||
init(context: AccountContext, presentationData: PresentationData, mode: InviteLinkInviteController.Mode, controller: InviteLinkInviteController, initialInvite: ExportedInvitation?) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
|
||||
@ -315,7 +339,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
self.headerNode.clipsToBounds = false
|
||||
|
||||
self.headerBackgroundNode = ASDisplayNode()
|
||||
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.headerBackgroundNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
|
||||
self.headerBackgroundNode.cornerRadius = 16.0
|
||||
self.headerBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
|
||||
@ -334,7 +358,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
|
||||
self.historyBackgroundContentNode = ASDisplayNode()
|
||||
self.historyBackgroundContentNode.isLayerBacked = true
|
||||
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.historyBackgroundContentNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
|
||||
|
||||
self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode)
|
||||
|
||||
@ -350,7 +374,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
self.backgroundColor = nil
|
||||
self.isOpaque = false
|
||||
|
||||
let mainInvitePromise = ValuePromise<ExportedInvitation?>(nil)
|
||||
let mainInvitePromise = ValuePromise<ExportedInvitation?>(initialInvite)
|
||||
|
||||
self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in
|
||||
guard let self else {
|
||||
@ -359,7 +383,6 @@ public final class InviteLinkInviteController: ViewController {
|
||||
guard let node = node as? ContextReferenceContentNode else {
|
||||
return
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
|
||||
@ -377,17 +400,17 @@ public final class InviteLinkInviteController: ViewController {
|
||||
}
|
||||
})))
|
||||
|
||||
if case let .groupOrChannel(peerId) = self.mode {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let invite = invite {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let invite {
|
||||
if case let .groupOrChannel(peerId) = self.mode {
|
||||
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let strongSelf = self else {
|
||||
@ -400,12 +423,17 @@ public final class InviteLinkInviteController: ViewController {
|
||||
isGroup = true
|
||||
}
|
||||
let updatedPresentationData = (strongSelf.presentationData, strongSelf.presentationDataPromise.get())
|
||||
let controller = QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup))
|
||||
let controller = QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel))
|
||||
strongSelf.controller?.present(controller, in: .window(.root))
|
||||
})
|
||||
} else if case .groupCall = self.mode {
|
||||
let controller = QrCodeScreen(context: context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), subject: .invite(invite: invite, type: .channel))
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
}
|
||||
})))
|
||||
|
||||
}
|
||||
})))
|
||||
|
||||
if case let .groupOrChannel(peerId) = self.mode {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { [ weak self] _, f in
|
||||
@ -450,7 +478,45 @@ public final class InviteLinkInviteController: ViewController {
|
||||
self?.controller?.present(controller, in: .window(.root))
|
||||
})
|
||||
})))
|
||||
} else if case let .groupCall(groupCall) = self.mode, groupCall.canRevoke {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { [ weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = ActionSheetController(presentationData: presentationData)
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
//TODO:localize
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: "Revoke Link"),
|
||||
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { [weak self] in
|
||||
dismissAction()
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let inviteLink = invite?.link {
|
||||
let _ = (context.engine.calls.revokeConferenceInviteLink(reference: .id(id: groupCall.callId, accessHash: groupCall.accessHash), link: inviteLink) |> deliverOnMainQueue).start(next: { result in
|
||||
mainInvitePromise.set(.link(link: result.listenerLink, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil))
|
||||
})
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: presentationData.strings.InviteLink_InviteLinkRevoked), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
})))
|
||||
}
|
||||
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
@ -546,6 +612,12 @@ public final class InviteLinkInviteController: ViewController {
|
||||
strongSelf.controller?.parentNavigationController?.pushViewController(controller)
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
}, openCallAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.completed?(.openCall)
|
||||
self.controller?.dismiss()
|
||||
})
|
||||
|
||||
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
|
||||
@ -587,15 +659,16 @@ public final class InviteLinkInviteController: ViewController {
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
})
|
||||
case let .groupCall(link, isRecentlyCreated):
|
||||
//TODO:release
|
||||
let tempInfo: Signal<Void, NoError> = .single(Void()) |> delay(0.0, queue: .mainQueue())
|
||||
|
||||
case let .groupCall(groupCall):
|
||||
// A workaround to skip the first run of the event cycle
|
||||
let delayOfZero = Signal<Void, NoError>.single(()) |> delay(0.0, queue: .mainQueue())
|
||||
|
||||
self.disposable = (combineLatest(queue: .mainQueue(),
|
||||
self.presentationDataPromise.get(),
|
||||
tempInfo
|
||||
mainInvitePromise.get(),
|
||||
delayOfZero
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, _ in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, mainInvite, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -605,9 +678,9 @@ public final class InviteLinkInviteController: ViewController {
|
||||
let helpText: String = "Anyone on Telegram can join your call by following the link below."
|
||||
entries.append(.header(title: "Call Link", text: helpText))
|
||||
|
||||
let mainInvite: ExportedInvitation = .link(link: link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil)
|
||||
let mainInvite: ExportedInvitation = .link(link: mainInvite?.link ?? "", title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil)
|
||||
|
||||
entries.append(.mainLink(invitation: mainInvite, isCall: true, isRecentlyCreated: isRecentlyCreated))
|
||||
entries.append(.mainLink(invitation: mainInvite, isCall: true, isRecentlyCreated: groupCall.isRecentlyCreated))
|
||||
|
||||
let previousEntries = previousEntries.swap(entries)
|
||||
|
||||
@ -665,8 +738,8 @@ public final class InviteLinkInviteController: ViewController {
|
||||
self.presentationData = presentationData
|
||||
self.presentationDataPromise.set(.single(presentationData))
|
||||
|
||||
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.historyBackgroundContentNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
|
||||
self.headerBackgroundNode.backgroundColor = getBackgroundColor(theme: self.presentationData.theme)
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||
|
||||
self.doneButtonIconNode.image = generateCloseButtonImage(backgroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))!
|
||||
|
@ -229,6 +229,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
if let invite = invite {
|
||||
arguments.openLink(invite)
|
||||
}
|
||||
}, openCallAction: {
|
||||
})
|
||||
case let .mainLinkOtherInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: nil, style: .blocks, tag: nil)
|
||||
@ -529,7 +530,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
|
||||
} else {
|
||||
isGroup = true
|
||||
}
|
||||
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
|
||||
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
|
||||
})
|
||||
})))
|
||||
|
||||
@ -718,7 +719,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
|
||||
isGroup = true
|
||||
}
|
||||
Queue.mainQueue().after(0.2) {
|
||||
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
|
||||
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
|
||||
}
|
||||
})
|
||||
})))
|
||||
|
@ -271,6 +271,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
}, contextAction: invite.link?.hasSuffix("...") == true ? nil : { node, gesture in
|
||||
interaction.contextAction(invite, node, gesture)
|
||||
}, viewAction: {
|
||||
}, openCallAction: {
|
||||
})
|
||||
case let .subscriptionHeader(_, title):
|
||||
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
|
||||
@ -754,7 +755,7 @@ public final class InviteLinkViewController: ViewController {
|
||||
isGroup = true
|
||||
}
|
||||
let updatedPresentationData = (strongSelf.presentationData, parentController.presentationDataPromise.get())
|
||||
strongSelf.controller?.present(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), in: .window(.root))
|
||||
strongSelf.controller?.present(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), in: .window(.root))
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import Markdown
|
||||
import TextFormat
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import TextNodeWithEntities
|
||||
|
||||
private func actionButtonImage(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in
|
||||
@ -46,6 +47,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
let shareAction: (() -> Void)?
|
||||
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||
let viewAction: (() -> Void)?
|
||||
let openCallAction: (() -> Void)?
|
||||
public let tag: ItemListItemTag?
|
||||
|
||||
public init(
|
||||
@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
shareAction: (() -> Void)?,
|
||||
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?,
|
||||
viewAction: (() -> Void)?,
|
||||
openCallAction: (() -> Void)?,
|
||||
tag: ItemListItemTag? = nil
|
||||
) {
|
||||
self.context = context
|
||||
@ -83,6 +86,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
self.shareAction = shareAction
|
||||
self.contextAction = contextAction
|
||||
self.viewAction = viewAction
|
||||
self.openCallAction = openCallAction
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
@ -147,7 +151,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
private var shimmerNode: ShimmerEffectNode?
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
|
||||
private var justCreatedCallTextNode: TextNode?
|
||||
private var justCreatedCallTextNode: TextNodeWithEntities?
|
||||
private var justCreatedCallLeftSeparatorLayer: SimpleLayer?
|
||||
private var justCreatedCallRightSeparatorLayer: SimpleLayer?
|
||||
private var justCreatedCallSeparatorText: ComponentView<Empty>?
|
||||
@ -299,7 +303,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
public func asyncLayout() -> (_ item: ItemListPermanentInviteLinkItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeAddressLayout = TextNode.asyncLayout(self.addressNode)
|
||||
let makeInvitedPeersLayout = TextNode.asyncLayout(self.invitedPeersNode)
|
||||
let makeJustCreatedCallTextNodeLayout = TextNode.asyncLayout(self.justCreatedCallTextNode)
|
||||
let makeJustCreatedCallTextNodeLayout = TextNodeWithEntities.asyncLayout(self.justCreatedCallTextNode)
|
||||
|
||||
let currentItem = self.item
|
||||
let avatarsContext = self.avatarsContext
|
||||
@ -343,7 +347,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
let (invitedPeersLayout, invitedPeersApply) = makeInvitedPeersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var justCreatedCallTextNodeLayout: (TextNodeLayout, () -> TextNode?)?
|
||||
var justCreatedCallTextNodeLayout: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities?)?
|
||||
if item.isCall {
|
||||
let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor)
|
||||
|
||||
@ -571,17 +575,39 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
|
||||
|
||||
if let justCreatedCallTextNodeLayout {
|
||||
if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1() {
|
||||
if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1(TextNodeWithEntities.Arguments(
|
||||
context: item.context,
|
||||
cache: item.context.animationCache,
|
||||
renderer: item.context.animationRenderer,
|
||||
placeholderColor: .gray,
|
||||
attemptSynchronous: true
|
||||
)) {
|
||||
if strongSelf.justCreatedCallTextNode !== justCreatedCallTextNode {
|
||||
strongSelf.justCreatedCallTextNode?.removeFromSupernode()
|
||||
strongSelf.justCreatedCallTextNode?.textNode.removeFromSupernode()
|
||||
strongSelf.justCreatedCallTextNode = justCreatedCallTextNode
|
||||
|
||||
//justCreatedCallTextNode.highlig
|
||||
|
||||
strongSelf.addSubnode(justCreatedCallTextNode)
|
||||
strongSelf.addSubnode(justCreatedCallTextNode.textNode)
|
||||
}
|
||||
|
||||
justCreatedCallTextNode.linkHighlightColor = item.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.1)
|
||||
justCreatedCallTextNode.highlightAttributeAction = { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
justCreatedCallTextNode.tapAttributeAction = { [weak strongSelf] attributes, _ in
|
||||
guard let strongSelf else {
|
||||
return
|
||||
}
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
strongSelf.item?.openCallAction?()
|
||||
}
|
||||
}
|
||||
|
||||
let justCreatedCallTextNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - justCreatedCallTextNodeLayout.0.size.width) / 2.0), y: shareButtonNode.frame.maxY + justCreatedCallTextSpacing), size: CGSize(width: justCreatedCallTextNodeLayout.0.size.width, height: justCreatedCallTextNodeLayout.0.size.height))
|
||||
justCreatedCallTextNode.frame = justCreatedCallTextNodeFrame
|
||||
justCreatedCallTextNode.textNode.frame = justCreatedCallTextNodeFrame
|
||||
|
||||
let justCreatedCallSeparatorText: ComponentView<Empty>
|
||||
if let current = strongSelf.justCreatedCallSeparatorText {
|
||||
@ -636,7 +662,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
}
|
||||
} else if let justCreatedCallTextNode = strongSelf.justCreatedCallTextNode {
|
||||
strongSelf.justCreatedCallTextNode = nil
|
||||
justCreatedCallTextNode.removeFromSupernode()
|
||||
justCreatedCallTextNode.textNode.removeFromSupernode()
|
||||
|
||||
strongSelf.justCreatedCallLeftSeparatorLayer?.removeFromSuperlayer()
|
||||
strongSelf.justCreatedCallLeftSeparatorLayer = nil
|
||||
|
@ -655,7 +655,7 @@ public func channelMembersController(context: AccountContext, updatedPresentatio
|
||||
}, inviteViaLink: {
|
||||
if let controller = getControllerImpl?() {
|
||||
dismissInputImpl?()
|
||||
presentControllerImpl?(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: peerId), parentNavigationController: controller.navigationController as? NavigationController), nil)
|
||||
presentControllerImpl?(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: peerId), initialInvite: nil, parentNavigationController: controller.navigationController as? NavigationController), nil)
|
||||
}
|
||||
}, updateHideMembers: { value in
|
||||
let _ = context.engine.peers.updateChannelMembersHidden(peerId: peerId, value: value).start()
|
||||
|
@ -655,6 +655,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
|
||||
if let invite = invite {
|
||||
arguments.openLink(invite)
|
||||
}
|
||||
}, openCallAction: {
|
||||
})
|
||||
case let .editablePublicLink(theme, _, placeholder, currentText):
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), clearType: .always, tag: ChannelVisibilityEntryTag.publicLink, sectionId: self.section, textUpdated: { updatedText in
|
||||
@ -1608,7 +1609,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
|
||||
} else {
|
||||
isGroup = true
|
||||
}
|
||||
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
|
||||
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -35,9 +35,15 @@ private func shareQrCode(context: AccountContext, link: String, ecl: String, vie
|
||||
}
|
||||
|
||||
public final class QrCodeScreen: ViewController {
|
||||
public enum SubjectType {
|
||||
case group
|
||||
case channel
|
||||
case groupCall
|
||||
}
|
||||
|
||||
public enum Subject {
|
||||
case peer(peer: EnginePeer)
|
||||
case invite(invite: ExportedInvitation, isGroup: Bool)
|
||||
case invite(invite: ExportedInvitation, type: SubjectType)
|
||||
case chatFolder(slug: String)
|
||||
|
||||
var link: String {
|
||||
@ -239,9 +245,17 @@ public final class QrCodeScreen: ViewController {
|
||||
let title: String
|
||||
let text: String
|
||||
switch subject {
|
||||
case let .invite(_, isGroup):
|
||||
case let .invite(_, type):
|
||||
title = self.presentationData.strings.InviteLink_QRCode_Title
|
||||
text = isGroup ? self.presentationData.strings.InviteLink_QRCode_Info : self.presentationData.strings.InviteLink_QRCode_InfoChannel
|
||||
switch type {
|
||||
case .group:
|
||||
text = self.presentationData.strings.InviteLink_QRCode_Info
|
||||
case .channel:
|
||||
text = self.presentationData.strings.InviteLink_QRCode_InfoChannel
|
||||
case .groupCall:
|
||||
//TODO:localize
|
||||
text = "Everyone on Telegram can scan this code to join your group call."
|
||||
}
|
||||
case .chatFolder:
|
||||
title = self.presentationData.strings.InviteLink_QRCodeFolder_Title
|
||||
text = self.presentationData.strings.InviteLink_QRCodeFolder_Text
|
||||
|
@ -1050,7 +1050,7 @@ private enum StatsEntry: ItemListNodeEntry {
|
||||
arguments.copyBoostLink(link)
|
||||
}, shareAction: {
|
||||
arguments.shareBoostLink(link)
|
||||
}, contextAction: nil, viewAction: nil, tag: nil)
|
||||
}, contextAction: nil, viewAction: nil, openCallAction: nil, tag: nil)
|
||||
case let .boostersPlaceholder(_, text):
|
||||
return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks)
|
||||
case let .boostGifts(theme, title):
|
||||
|
@ -384,6 +384,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) }
|
||||
dict[-1945083841] = { return Api.InputGroupCall.parse_inputGroupCallInviteMessage($0) }
|
||||
dict[-33127873] = { return Api.InputGroupCall.parse_inputGroupCallSlug($0) }
|
||||
dict[-191267262] = { return Api.InputInvoice.parse_inputInvoiceBusinessBotTransferStars($0) }
|
||||
dict[887591921] = { return Api.InputInvoice.parse_inputInvoiceChatInviteSubscription($0) }
|
||||
dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) }
|
||||
dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) }
|
||||
|
@ -248,6 +248,7 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
indirect enum InputInvoice: TypeConstructorDescription {
|
||||
case inputInvoiceBusinessBotTransferStars(bot: Api.InputUser, stars: Int64)
|
||||
case inputInvoiceChatInviteSubscription(hash: String)
|
||||
case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32)
|
||||
case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption)
|
||||
@ -260,6 +261,13 @@ public extension Api {
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .inputInvoiceBusinessBotTransferStars(let bot, let stars):
|
||||
if boxed {
|
||||
buffer.appendInt32(-191267262)
|
||||
}
|
||||
bot.serialize(buffer, true)
|
||||
serializeInt64(stars, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .inputInvoiceChatInviteSubscription(let hash):
|
||||
if boxed {
|
||||
buffer.appendInt32(887591921)
|
||||
@ -329,6 +337,8 @@ public extension Api {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .inputInvoiceBusinessBotTransferStars(let bot, let stars):
|
||||
return ("inputInvoiceBusinessBotTransferStars", [("bot", bot as Any), ("stars", stars as Any)])
|
||||
case .inputInvoiceChatInviteSubscription(let hash):
|
||||
return ("inputInvoiceChatInviteSubscription", [("hash", hash as Any)])
|
||||
case .inputInvoiceMessage(let peer, let msgId):
|
||||
@ -350,6 +360,22 @@ public extension Api {
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_inputInvoiceBusinessBotTransferStars(_ reader: BufferReader) -> InputInvoice? {
|
||||
var _1: Api.InputUser?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.InputUser
|
||||
}
|
||||
var _2: Int64?
|
||||
_2 = reader.readInt64()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.InputInvoice.inputInvoiceBusinessBotTransferStars(bot: _1!, stars: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputInvoiceChatInviteSubscription(_ reader: BufferReader) -> InputInvoice? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
|
@ -10136,12 +10136,13 @@ public extension Api.functions.phone {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.phone {
|
||||
static func inviteConferenceCallParticipant(call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
static func inviteConferenceCallParticipant(flags: Int32, call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1050474478)
|
||||
buffer.appendInt32(-1124981115)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
call.serialize(buffer, true)
|
||||
userId.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "phone.inviteConferenceCallParticipant", parameters: [("call", String(describing: call)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
return (FunctionDescription(name: "phone.inviteConferenceCallParticipant", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
|
@ -486,45 +486,86 @@ public final class CallController: ViewController {
|
||||
var disablePeerIds: [EnginePeer.Id] = []
|
||||
disablePeerIds.append(self.call.context.account.peerId)
|
||||
disablePeerIds.append(self.call.peerId)
|
||||
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in
|
||||
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, shareLink: nil, completion: { [weak self] peers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = self.call.upgradeToConference(invitePeerIds: peerIds, completion: { _ in
|
||||
let _ = self.call.upgradeToConference(invitePeers: peers, completion: { _ in
|
||||
})
|
||||
})
|
||||
self.push(controller)
|
||||
}
|
||||
|
||||
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([EnginePeer.Id]) -> Void) -> ViewController {
|
||||
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], shareLink: (() -> Void)?, completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController {
|
||||
//TODO:localize
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
|
||||
var options: [ContactListAdditionalOption] = []
|
||||
var openShareLinkImpl: (() -> Void)?
|
||||
if shareLink != nil {
|
||||
//TODO:localize
|
||||
options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
|
||||
openShareLinkImpl?()
|
||||
}, clearHighlightAutomatically: false))
|
||||
}
|
||||
|
||||
let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
|
||||
context: context,
|
||||
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
|
||||
title: "Invite Members",
|
||||
mode: .peerSelection(searchChatList: true, searchGroups: false, searchChannels: false),
|
||||
mode: .generic,
|
||||
title: { strings in
|
||||
//TODO:localize
|
||||
return "Add Member"
|
||||
},
|
||||
options: .single(options),
|
||||
displayCallIcons: true,
|
||||
confirmation: { peer in
|
||||
switch peer {
|
||||
case let .peer(peer, _, _):
|
||||
let peer = EnginePeer(peer)
|
||||
guard case let .user(user) = peer else {
|
||||
return .single(false)
|
||||
}
|
||||
if disablePeerIds.contains(user.id) {
|
||||
return .single(false)
|
||||
}
|
||||
if user.botInfo != nil {
|
||||
return .single(false)
|
||||
}
|
||||
return .single(true)
|
||||
default:
|
||||
return .single(false)
|
||||
}
|
||||
},
|
||||
isPeerEnabled: { peer in
|
||||
guard case let .user(user) = peer else {
|
||||
switch peer {
|
||||
case let .peer(peer, _, _):
|
||||
let peer = EnginePeer(peer)
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
if disablePeerIds.contains(user.id) {
|
||||
return false
|
||||
}
|
||||
if user.botInfo != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if disablePeerIds.contains(user.id) {
|
||||
return false
|
||||
}
|
||||
if user.botInfo != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
))
|
||||
|
||||
openShareLinkImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
shareLink?()
|
||||
}
|
||||
|
||||
controller.navigationPresentation = .modal
|
||||
let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
|
||||
guard case let .result(peerIds, _) = result else {
|
||||
controller?.dismiss()
|
||||
return
|
||||
}
|
||||
if peerIds.isEmpty {
|
||||
guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else {
|
||||
controller?.dismiss()
|
||||
return
|
||||
}
|
||||
@ -533,15 +574,15 @@ public final class CallController: ViewController {
|
||||
controller?.dismiss()
|
||||
}
|
||||
|
||||
let invitePeerIds = peerIds.compactMap { item -> EnginePeer.Id? in
|
||||
if case let .peer(peerId) = item {
|
||||
return peerId
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
var isVideo = false
|
||||
switch result.1 {
|
||||
case .videoCall:
|
||||
isVideo = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
completion(invitePeerIds)
|
||||
completion([(peer.id, isVideo)])
|
||||
})
|
||||
|
||||
return controller
|
||||
|
@ -167,10 +167,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
self.conferenceAddParticipant?()
|
||||
}
|
||||
|
||||
var isConferencePossible = false
|
||||
if self.call.context.sharedContext.immediateExperimentalUISettings.conferenceDebug {
|
||||
isConferencePossible = true
|
||||
}
|
||||
var isConferencePossible = true
|
||||
if let data = self.call.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_enable_conference"] as? Double {
|
||||
isConferencePossible = value != 0.0
|
||||
}
|
||||
|
@ -234,6 +234,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
public let peerId: EnginePeer.Id
|
||||
public let isOutgoing: Bool
|
||||
private let incomingConferenceSource: EngineMessage.Id?
|
||||
private let conferenceStableId: Int64?
|
||||
public var isVideo: Bool
|
||||
public var isVideoPossible: Bool
|
||||
private let enableStunMarking: Bool
|
||||
@ -368,7 +369,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
return self.conferenceStatePromise.get()
|
||||
}
|
||||
|
||||
public private(set) var pendingInviteToConferencePeerIds: [EnginePeer.Id] = []
|
||||
public private(set) var pendingInviteToConferencePeerIds: [(id: EnginePeer.Id, isVideo: Bool)] = []
|
||||
|
||||
private var localVideoEndpointId: String?
|
||||
private var remoteVideoEndpointId: String?
|
||||
@ -423,6 +424,11 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.peerId = peerId
|
||||
self.isOutgoing = isOutgoing
|
||||
self.incomingConferenceSource = incomingConferenceSource
|
||||
if let _ = incomingConferenceSource {
|
||||
self.conferenceStableId = Int64.random(in: Int64.min ..< Int64.max)
|
||||
} else {
|
||||
self.conferenceStableId = nil
|
||||
}
|
||||
self.isVideo = initialState?.type == .video
|
||||
self.isVideoPossible = isVideoPossible
|
||||
self.enableStunMarking = enableStunMarking
|
||||
@ -445,19 +451,67 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
|
||||
var didReceiveAudioOutputs = false
|
||||
|
||||
var callSessionState: Signal<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 incomingConferenceSource = incomingConferenceSource {
|
||||
self.sessionStateDisposable = (context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Messages.Message(id: incomingConferenceSource)
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] message in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let state: CallSessionState
|
||||
if let message = message {
|
||||
var foundAction: TelegramMediaAction?
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
foundAction = action
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||
if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil {
|
||||
state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions())
|
||||
} else {
|
||||
state = .ringing
|
||||
}
|
||||
} else {
|
||||
state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions())
|
||||
}
|
||||
} else {
|
||||
state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions())
|
||||
}
|
||||
|
||||
self.updateSessionState(
|
||||
sessionState: CallSession(
|
||||
id: self.internalId,
|
||||
stableId: self.conferenceStableId,
|
||||
isOutgoing: false,
|
||||
type: self.isVideo ? .video : .audio,
|
||||
state: state,
|
||||
isVideoPossible: true
|
||||
),
|
||||
callContextState: nil,
|
||||
reception: nil,
|
||||
audioSessionControl: self.audioSessionControl
|
||||
)
|
||||
})
|
||||
} else {
|
||||
var callSessionState: Signal<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"] {
|
||||
self.sharedAudioContext = nil
|
||||
@ -933,15 +987,20 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
let keyPair: TelegramKeyPair? = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair()
|
||||
guard let keyPair, let groupCall else {
|
||||
self.updateSessionState(sessionState: CallSession(
|
||||
id: self.internalId,
|
||||
stableId: nil,
|
||||
isOutgoing: false,
|
||||
type: .audio,
|
||||
state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()),
|
||||
isVideoPossible: true
|
||||
),
|
||||
callContextState: nil, reception: nil, audioSessionControl: self.audioSessionControl)
|
||||
self.sessionStateDisposable?.dispose()
|
||||
self.updateSessionState(
|
||||
sessionState: CallSession(
|
||||
id: self.internalId,
|
||||
stableId: self.conferenceStableId,
|
||||
isOutgoing: false,
|
||||
type: self.isVideo ? .video : .audio,
|
||||
state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()),
|
||||
isVideoPossible: true
|
||||
),
|
||||
callContextState: nil,
|
||||
reception: nil,
|
||||
audioSessionControl: self.audioSessionControl
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@ -967,14 +1026,15 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
keyPair: keyPair,
|
||||
conferenceSourceId: self.internalId,
|
||||
isConference: true,
|
||||
beginWithVideo: false,
|
||||
sharedAudioContext: self.sharedAudioContext
|
||||
)
|
||||
self.conferenceCallImpl = conferenceCall
|
||||
conferenceCall.upgradedConferenceCall = self
|
||||
|
||||
conferenceCall.setConferenceInvitedPeers(self.pendingInviteToConferencePeerIds)
|
||||
for peerId in self.pendingInviteToConferencePeerIds {
|
||||
let _ = conferenceCall.invitePeer(peerId)
|
||||
for (peerId, isVideo) in self.pendingInviteToConferencePeerIds {
|
||||
let _ = conferenceCall.invitePeer(peerId, isVideo: isVideo)
|
||||
}
|
||||
|
||||
conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted)
|
||||
@ -1067,9 +1127,10 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sessionStateDisposable?.dispose()
|
||||
self.updateSessionState(sessionState: CallSession(
|
||||
id: self.internalId,
|
||||
stableId: nil,
|
||||
stableId: self.conferenceStableId,
|
||||
isOutgoing: false,
|
||||
type: .audio,
|
||||
state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()),
|
||||
@ -1341,11 +1402,12 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
if strongSelf.incomingConferenceSource != nil {
|
||||
strongSelf.conferenceStateValue = .preparing
|
||||
strongSelf.isAcceptingIncomingConference = true
|
||||
strongSelf.sessionStateDisposable?.dispose()
|
||||
strongSelf.updateSessionState(sessionState: CallSession(
|
||||
id: strongSelf.internalId,
|
||||
stableId: nil,
|
||||
stableId: strongSelf.conferenceStableId,
|
||||
isOutgoing: false,
|
||||
type: .audio,
|
||||
type: strongSelf.isVideo ? .video : .audio,
|
||||
state: .ringing,
|
||||
isVideoPossible: true
|
||||
),
|
||||
@ -1365,9 +1427,10 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
if strongSelf.incomingConferenceSource != nil {
|
||||
strongSelf.conferenceStateValue = .preparing
|
||||
strongSelf.isAcceptingIncomingConference = true
|
||||
strongSelf.sessionStateDisposable?.dispose()
|
||||
strongSelf.updateSessionState(sessionState: CallSession(
|
||||
id: strongSelf.internalId,
|
||||
stableId: nil,
|
||||
stableId: strongSelf.conferenceStableId,
|
||||
isOutgoing: false,
|
||||
type: .audio,
|
||||
state: .ringing,
|
||||
@ -1552,7 +1615,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.videoCapturer?.setIsVideoEnabled(!isPaused)
|
||||
}
|
||||
|
||||
public func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable {
|
||||
public func upgradeToConference(invitePeers: [(id: EnginePeer.Id, isVideo: Bool)], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable {
|
||||
if self.isMovedToConference {
|
||||
return EmptyDisposable
|
||||
}
|
||||
@ -1561,7 +1624,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
self.pendingInviteToConferencePeerIds = invitePeerIds
|
||||
self.pendingInviteToConferencePeerIds = invitePeers
|
||||
let index = self.upgradedToConferenceCompletions.add({ call in
|
||||
completion(call)
|
||||
})
|
||||
|
@ -850,6 +850,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
keyPair: nil,
|
||||
conferenceSourceId: nil,
|
||||
isConference: false,
|
||||
beginWithVideo: false,
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
call.schedule(timestamp: timestamp)
|
||||
@ -1076,6 +1077,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
keyPair: nil,
|
||||
conferenceSourceId: nil,
|
||||
isConference: false,
|
||||
beginWithVideo: false,
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
self.updateCurrentGroupCall(.group(call))
|
||||
@ -1085,16 +1087,13 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
accountContext: AccountContext,
|
||||
initialCall: EngineGroupCallDescription,
|
||||
reference: InternalGroupCallReference,
|
||||
mode: JoinConferenceCallMode
|
||||
beginWithVideo: Bool
|
||||
) {
|
||||
let keyPair: TelegramKeyPair
|
||||
switch mode {
|
||||
case .joining:
|
||||
guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
|
||||
return
|
||||
}
|
||||
keyPair = keyPairValue
|
||||
guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
|
||||
return
|
||||
}
|
||||
keyPair = keyPairValue
|
||||
|
||||
let call = PresentationGroupCallImpl(
|
||||
accountContext: accountContext,
|
||||
@ -1111,6 +1110,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
keyPair: keyPair,
|
||||
conferenceSourceId: nil,
|
||||
isConference: true,
|
||||
beginWithVideo: beginWithVideo,
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
self.updateCurrentGroupCall(.group(call))
|
||||
|
@ -161,7 +161,8 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
||||
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
|
||||
isVideoEnabled: state.isVideoEnabled,
|
||||
unmutedVideoLimit: state.unmutedVideoLimit,
|
||||
isStream: state.isStream
|
||||
isStream: state.isStream,
|
||||
isCreator: state.isCreator
|
||||
),
|
||||
topParticipants: topParticipants,
|
||||
participantCount: state.totalCount,
|
||||
@ -621,54 +622,98 @@ private final class PendingConferenceInvitationContext {
|
||||
case ringing
|
||||
}
|
||||
|
||||
private let callSessionManager: CallSessionManager
|
||||
private let engine: TelegramEngine
|
||||
private var requestDisposable: Disposable?
|
||||
private var stateDisposable: Disposable?
|
||||
private var internalId: CallSessionInternalId?
|
||||
private(set) var messageId: EngineMessage.Id?
|
||||
|
||||
private var hadMessage: Bool = false
|
||||
private var didNotifyEnded: Bool = false
|
||||
|
||||
init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, peerId: PeerId, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) {
|
||||
self.callSessionManager = callSessionManager
|
||||
|
||||
preconditionFailure()
|
||||
|
||||
/*self.requestDisposable = (callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: (groupCall, encryptionKey))
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] internalId in
|
||||
init(engine: TelegramEngine, reference: InternalGroupCallReference, peerId: PeerId, isVideo: Bool, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) {
|
||||
self.engine = engine
|
||||
self.requestDisposable = (engine.calls.inviteConferenceCallParticipant(reference: reference, peerId: peerId, isVideo: isVideo).startStrict(next: { [weak self] messageId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.internalId = internalId
|
||||
guard let messageId else {
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.messageId = messageId
|
||||
|
||||
self.stateDisposable = (self.callSessionManager.callState(internalId: internalId)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
|
||||
onStateUpdated(.ringing)
|
||||
|
||||
let timeout: Double = 30.0
|
||||
let timerSignal = Signal<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 {
|
||||
return
|
||||
}
|
||||
switch state.state {
|
||||
case let .requesting(ringing, _):
|
||||
if ringing {
|
||||
onStateUpdated(.ringing)
|
||||
if let message {
|
||||
self.hadMessage = true
|
||||
if message.timestamp + Int32(timeout) <= Int32(Date().timeIntervalSince1970) {
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(false)
|
||||
}
|
||||
} else {
|
||||
var isActive = false
|
||||
var isAccepted = false
|
||||
var foundAction: TelegramMediaAction?
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
foundAction = action
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||
if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil {
|
||||
} else {
|
||||
if conferenceCall.flags.contains(.isActive) {
|
||||
isAccepted = true
|
||||
} else {
|
||||
isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isActive {
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(isAccepted)
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .dropping(reason), let .terminated(_, reason, _):
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(reason == .ended(.switchedToConference))
|
||||
} else {
|
||||
if self.hadMessage || CFAbsoluteTimeGetCurrent() > startTime + 1.0 {
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(false)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
})*/
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
self.stateDisposable?.dispose()
|
||||
|
||||
if let internalId = self.internalId {
|
||||
self.callSessionManager.drop(internalId: internalId, reason: .hangUp, debugLog: .single(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -844,6 +889,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void)
|
||||
|
||||
private(set) var initialCall: (description: EngineGroupCallDescription, reference: InternalGroupCallReference)?
|
||||
public var currentReference: InternalGroupCallReference?
|
||||
public let internalId: CallSessionInternalId
|
||||
public let peerId: EnginePeer.Id?
|
||||
private let isChannel: Bool
|
||||
@ -1121,6 +1167,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private let sharedAudioContext: SharedCallAudioContext?
|
||||
|
||||
public let isConference: Bool
|
||||
private let beginWithVideo: Bool
|
||||
|
||||
private let conferenceSourceId: CallSessionInternalId?
|
||||
public var conferenceSource: CallSessionInternalId? {
|
||||
@ -1153,6 +1200,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
keyPair: TelegramKeyPair?,
|
||||
conferenceSourceId: CallSessionInternalId?,
|
||||
isConference: Bool,
|
||||
beginWithVideo: Bool,
|
||||
sharedAudioContext: SharedCallAudioContext?
|
||||
) {
|
||||
self.account = accountContext.account
|
||||
@ -1162,6 +1210,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.getDeviceAccessData = getDeviceAccessData
|
||||
|
||||
self.initialCall = initialCall
|
||||
self.currentReference = initialCall?.reference
|
||||
self.callId = initialCall?.description.id
|
||||
|
||||
self.internalId = internalId
|
||||
@ -1183,6 +1232,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.isStream = isStream
|
||||
self.conferenceSourceId = conferenceSourceId
|
||||
self.isConference = isConference
|
||||
self.beginWithVideo = beginWithVideo
|
||||
self.keyPair = keyPair
|
||||
|
||||
if let keyPair, let initialCall {
|
||||
@ -1490,6 +1540,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath)
|
||||
})*/
|
||||
|
||||
if beginWithVideo {
|
||||
self.requestVideo()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -1912,7 +1966,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
defaultParticipantsAreMuted: callInfo.defaultParticipantsAreMuted ?? state.defaultParticipantsAreMuted,
|
||||
isVideoEnabled: callInfo.isVideoEnabled,
|
||||
unmutedVideoLimit: callInfo.unmutedVideoLimit,
|
||||
isStream: callInfo.isStream
|
||||
isStream: callInfo.isStream,
|
||||
isCreator: callInfo.isCreator
|
||||
)), audioSessionControl: self.audioSessionControl)
|
||||
} else {
|
||||
self.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
|
||||
@ -1928,7 +1983,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
|
||||
isVideoEnabled: state.isVideoEnabled,
|
||||
unmutedVideoLimit: state.unmutedVideoLimit,
|
||||
isStream: callInfo.isStream
|
||||
isStream: callInfo.isStream,
|
||||
isCreator: callInfo.isCreator
|
||||
))))
|
||||
|
||||
self.summaryParticipantsState.set(.single(SummaryParticipantsState(
|
||||
@ -2250,6 +2306,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.currentReference = .id(id: joinCallResult.callInfo.id, accessHash: joinCallResult.callInfo.accessHash)
|
||||
|
||||
let clientParams = joinCallResult.jsonParams
|
||||
if let data = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] {
|
||||
if let video = dict["video"] as? [String: Any] {
|
||||
@ -2859,7 +2918,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
|
||||
isVideoEnabled: state.isVideoEnabled,
|
||||
unmutedVideoLimit: state.unmutedVideoLimit,
|
||||
isStream: callInfo.isStream
|
||||
isStream: callInfo.isStream,
|
||||
isCreator: callInfo.isCreator
|
||||
))))
|
||||
|
||||
self.summaryParticipantsState.set(.single(SummaryParticipantsState(
|
||||
@ -2937,6 +2997,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
result.append(OngoingGroupCallContext.MediaChannelDescription(
|
||||
kind: .audio,
|
||||
peerId: participant.peer.id.id._internalGetInt64Value(),
|
||||
audioSsrc: audioSsrc,
|
||||
videoDescription: nil
|
||||
))
|
||||
@ -2948,6 +3009,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
result.append(OngoingGroupCallContext.MediaChannelDescription(
|
||||
kind: .audio,
|
||||
peerId: participant.peer.id.id._internalGetInt64Value(),
|
||||
audioSsrc: screencastSsrc,
|
||||
videoDescription: nil
|
||||
))
|
||||
@ -3838,28 +3900,23 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}))
|
||||
}
|
||||
|
||||
public func invitePeer(_ peerId: PeerId) -> Bool {
|
||||
public func invitePeer(_ peerId: PeerId, isVideo: Bool) -> Bool {
|
||||
if self.isConference {
|
||||
guard let initialCall = self.initialCall else {
|
||||
return false
|
||||
}
|
||||
|
||||
//TODO:release
|
||||
let _ = self.accountContext.engine.calls.inviteConferenceCallParticipant(callId: initialCall.description.id, accessHash: initialCall.description.accessHash, peerId: peerId).start()
|
||||
return false
|
||||
/*guard let initialCall = self.initialCall else {
|
||||
return false
|
||||
}
|
||||
if conferenceInvitationContexts[peerId] != nil {
|
||||
if self.conferenceInvitationContexts[peerId] != nil {
|
||||
return false
|
||||
}
|
||||
var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)?
|
||||
var onEnded: ((Bool) -> Void)?
|
||||
var didEndAlready = false
|
||||
let invitationContext = PendingConferenceInvitationContext(
|
||||
callSessionManager: self.accountContext.account.callSessionManager,
|
||||
groupCall: GroupCallReference(id: initialCall.id, accessHash: initialCall.accessHash),
|
||||
engine: self.accountContext.engine,
|
||||
reference: initialCall.reference,
|
||||
peerId: peerId,
|
||||
isVideo: isVideo,
|
||||
onStateUpdated: { state in
|
||||
onStateUpdated?(state)
|
||||
},
|
||||
@ -3906,7 +3963,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
}
|
||||
|
||||
return false*/
|
||||
return false
|
||||
} else {
|
||||
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else {
|
||||
return false
|
||||
@ -3922,7 +3979,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
}
|
||||
|
||||
func setConferenceInvitedPeers(_ peerIds: [PeerId]) {
|
||||
func setConferenceInvitedPeers(_ invitedPeers: [(id: PeerId, isVideo: Bool)]) {
|
||||
//TODO:release
|
||||
/*self.invitedPeersValue = peerIds.map {
|
||||
PresentationGroupCallInvitedPeer(id: $0, state: .requesting)
|
||||
@ -3933,6 +3990,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
var updatedInvitedPeers = self.invitedPeersValue
|
||||
updatedInvitedPeers.removeAll(where: { $0.id == peerId})
|
||||
self.invitedPeersValue = updatedInvitedPeers
|
||||
|
||||
if let conferenceInvitationContext = self.conferenceInvitationContexts[peerId] {
|
||||
self.conferenceInvitationContexts.removeValue(forKey: peerId)
|
||||
if let messageId = conferenceInvitationContext.messageId {
|
||||
self.accountContext.engine.account.callSessionManager.dropOutgoingConferenceRequest(messageId: messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTitle(_ title: String) {
|
||||
|
@ -5,6 +5,179 @@ import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import TelegramPresentationData
|
||||
import CallsEmoji
|
||||
|
||||
private final class EmojiItemComponent: Component {
|
||||
let emoji: String?
|
||||
|
||||
init(emoji: String?) {
|
||||
self.emoji = emoji
|
||||
}
|
||||
|
||||
static func ==(lhs: EmojiItemComponent, rhs: EmojiItemComponent) -> Bool {
|
||||
if lhs.emoji != rhs.emoji {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let measureEmojiView = ComponentView<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 {
|
||||
let theme: PresentationTheme
|
||||
@ -119,7 +292,7 @@ final class VideoChatEncryptionKeyComponent: Component {
|
||||
let expandedButtonTopInset: CGFloat = 12.0
|
||||
let expandedButtonBottomInset: CGFloat = 13.0
|
||||
|
||||
let emojiItemSizes = (0 ..< component.emoji.count).map { i -> CGSize in
|
||||
let emojiItemSizes = (0 ..< 4).map { i -> CGSize in
|
||||
let emojiItem: ComponentView<Empty>
|
||||
if self.emojiItems.count > i {
|
||||
emojiItem = self.emojiItems[i]
|
||||
@ -128,9 +301,9 @@ final class VideoChatEncryptionKeyComponent: Component {
|
||||
self.emojiItems.append(emojiItem)
|
||||
}
|
||||
return emojiItem.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.emoji[i], font: Font.regular(40.0), textColor: .white))
|
||||
transition: transition,
|
||||
component: AnyComponent(EmojiItemComponent(
|
||||
emoji: i < component.emoji.count ? component.emoji[i] : nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||
|
@ -7,16 +7,24 @@ import TelegramPresentationData
|
||||
import BundleIconComponent
|
||||
|
||||
final class VideoChatListInviteComponent: Component {
|
||||
enum Icon {
|
||||
case addUser
|
||||
case link
|
||||
}
|
||||
|
||||
let title: String
|
||||
let icon: Icon
|
||||
let theme: PresentationTheme
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
title: String,
|
||||
icon: Icon,
|
||||
theme: PresentationTheme,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.theme = theme
|
||||
self.action = action
|
||||
}
|
||||
@ -25,6 +33,9 @@ final class VideoChatListInviteComponent: Component {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
@ -116,10 +127,17 @@ final class VideoChatListInviteComponent: Component {
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
}
|
||||
|
||||
let iconName: String
|
||||
switch component.icon {
|
||||
case .addUser:
|
||||
iconName = "Chat/Context Menu/AddUser"
|
||||
case .link:
|
||||
iconName = "Chat/Context Menu/Link"
|
||||
}
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Context Menu/AddUser",
|
||||
name: iconName,
|
||||
tintColor: component.theme.list.itemAccentColor
|
||||
)),
|
||||
environment: {},
|
||||
|
@ -39,23 +39,33 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
final class Participants: Equatable {
|
||||
enum InviteType {
|
||||
case invite
|
||||
enum InviteType: Equatable {
|
||||
case invite(isMultipleUsers: Bool)
|
||||
case shareLink
|
||||
}
|
||||
|
||||
struct InviteOption: Equatable {
|
||||
let id: Int
|
||||
let type: InviteType
|
||||
|
||||
init(id: Int, type: InviteType) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
}
|
||||
}
|
||||
|
||||
let myPeerId: EnginePeer.Id
|
||||
let participants: [GroupCallParticipantsContext.Participant]
|
||||
let totalCount: Int
|
||||
let loadMoreToken: String?
|
||||
let inviteType: InviteType?
|
||||
let inviteOptions: [InviteOption]
|
||||
|
||||
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteType: InviteType?) {
|
||||
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteOptions: [InviteOption]) {
|
||||
self.myPeerId = myPeerId
|
||||
self.participants = participants
|
||||
self.totalCount = totalCount
|
||||
self.loadMoreToken = loadMoreToken
|
||||
self.inviteType = inviteType
|
||||
self.inviteOptions = inviteOptions
|
||||
}
|
||||
|
||||
static func ==(lhs: Participants, rhs: Participants) -> Bool {
|
||||
@ -74,7 +84,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if lhs.loadMoreToken != rhs.loadMoreToken {
|
||||
return false
|
||||
}
|
||||
if lhs.inviteType != rhs.inviteType {
|
||||
if lhs.inviteOptions != rhs.inviteOptions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -142,7 +152,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void
|
||||
let updateIsMainParticipantPinned: (Bool) -> Void
|
||||
let updateIsExpandedUIHidden: (Bool) -> Void
|
||||
let openInviteMembers: () -> Void
|
||||
let openInviteMembers: (Participants.InviteType) -> Void
|
||||
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
|
||||
|
||||
init(
|
||||
@ -162,7 +172,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
|
||||
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
||||
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
|
||||
openInviteMembers: @escaping () -> Void,
|
||||
openInviteMembers: @escaping (Participants.InviteType) -> Void,
|
||||
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
|
||||
) {
|
||||
self.call = call
|
||||
@ -379,14 +389,14 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let sideInset: CGFloat
|
||||
let itemCount: Int
|
||||
let itemHeight: CGFloat
|
||||
let trailingItemHeight: CGFloat
|
||||
let trailingItemHeights: [CGFloat]
|
||||
|
||||
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeight: CGFloat) {
|
||||
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeights: [CGFloat]) {
|
||||
self.containerSize = containerSize
|
||||
self.sideInset = sideInset
|
||||
self.itemCount = itemCount
|
||||
self.itemHeight = itemHeight
|
||||
self.trailingItemHeight = trailingItemHeight
|
||||
self.trailingItemHeights = trailingItemHeights
|
||||
}
|
||||
|
||||
func frame(at index: Int) -> CGRect {
|
||||
@ -394,8 +404,15 @@ final class VideoChatParticipantsComponent: Component {
|
||||
return frame
|
||||
}
|
||||
|
||||
func trailingItemFrame() -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeight))
|
||||
func trailingItemFrame(index: Int) -> CGRect {
|
||||
if index < 0 || index >= self.trailingItemHeights.count {
|
||||
return CGRect()
|
||||
}
|
||||
var prefixHeight: CGFloat = 0.0
|
||||
for i in 0 ..< index {
|
||||
prefixHeight += self.trailingItemHeights[i]
|
||||
}
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight + prefixHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeights[index]))
|
||||
}
|
||||
|
||||
func contentHeight() -> CGFloat {
|
||||
@ -403,7 +420,9 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if self.itemCount != 0 {
|
||||
result = self.frame(at: self.itemCount - 1).maxY
|
||||
}
|
||||
result += self.trailingItemHeight
|
||||
for height in self.trailingItemHeights {
|
||||
result += height
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -439,7 +458,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let scrollClippingFrame: CGRect
|
||||
let separateVideoScrollClippingFrame: CGRect
|
||||
|
||||
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
|
||||
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeights: [CGFloat]) {
|
||||
self.containerSize = containerSize
|
||||
self.layout = layout
|
||||
self.isUIHidden = isUIHidden
|
||||
@ -465,7 +484,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil)
|
||||
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
|
||||
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeights: listTrailingItemHeights)
|
||||
self.spacing = 4.0
|
||||
|
||||
if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden {
|
||||
@ -568,8 +587,8 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func listTrailingItemFrame() -> CGRect {
|
||||
return self.list.trailingItemFrame()
|
||||
func listTrailingItemFrame(index: Int) -> CGRect {
|
||||
return self.list.trailingItemFrame(index: index)
|
||||
}
|
||||
}
|
||||
|
||||
@ -641,7 +660,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
private var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||
|
||||
private let measureListItemView = ComponentView<Empty>()
|
||||
private let inviteListItemView = ComponentView<Empty>()
|
||||
private var inviteListItemViews: [Int: ComponentView<Empty>] = [:]
|
||||
|
||||
private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
|
||||
private let gridItemViewContainer: UIView
|
||||
@ -1270,7 +1289,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
case .requesting:
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral)
|
||||
case .ringing:
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "ringing...", color: .neutral)
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "invited", color: .neutral)
|
||||
}
|
||||
|
||||
peerItemComponent = PeerListItemComponent(
|
||||
@ -1381,11 +1400,15 @@ final class VideoChatParticipantsComponent: Component {
|
||||
self.listItemViews.removeValue(forKey: itemId)
|
||||
}
|
||||
|
||||
do {
|
||||
var trailingItemIndex = 0
|
||||
for inviteOption in component.participants?.inviteOptions ?? [] {
|
||||
guard let itemView = self.inviteListItemViews[inviteOption.id] else {
|
||||
continue
|
||||
}
|
||||
var itemTransition = transition
|
||||
let itemView = self.inviteListItemView
|
||||
|
||||
let itemFrame = itemLayout.listTrailingItemFrame()
|
||||
let itemFrame = itemLayout.listTrailingItemFrame(index: trailingItemIndex)
|
||||
trailingItemIndex += 1
|
||||
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
@ -1395,6 +1418,17 @@ final class VideoChatParticipantsComponent: Component {
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
var removeInviteListItemIds: [Int] = []
|
||||
for (id, itemView) in self.inviteListItemViews {
|
||||
if let participants = component.participants, participants.inviteOptions.contains(where: { $0.id == id }) {
|
||||
} else {
|
||||
removeInviteListItemIds.append(id)
|
||||
itemView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
for id in removeInviteListItemIds {
|
||||
self.inviteListItemViews.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0)
|
||||
transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY))
|
||||
@ -1748,32 +1782,51 @@ final class VideoChatParticipantsComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
let inviteText: String
|
||||
if let participants = component.participants, let inviteType = participants.inviteType {
|
||||
switch inviteType {
|
||||
case .invite:
|
||||
inviteText = component.strings.VoiceChat_InviteMember
|
||||
var inviteListItemSizes: [CGSize] = []
|
||||
for (inviteOption) in component.participants?.inviteOptions ?? [] {
|
||||
let inviteText: String
|
||||
let iconType: VideoChatListInviteComponent.Icon
|
||||
switch inviteOption.type {
|
||||
case let .invite(isMultiple):
|
||||
//TODO:localize
|
||||
if isMultiple {
|
||||
inviteText = component.strings.VoiceChat_InviteMember
|
||||
} else {
|
||||
inviteText = "Add Member"
|
||||
}
|
||||
iconType = .addUser
|
||||
case .shareLink:
|
||||
inviteText = component.strings.VoiceChat_Share
|
||||
iconType = .link
|
||||
}
|
||||
} else {
|
||||
inviteText = component.strings.VoiceChat_InviteMember
|
||||
}
|
||||
let inviteListItemSize = self.inviteListItemView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(VideoChatListInviteComponent(
|
||||
title: inviteText,
|
||||
theme: component.theme,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
|
||||
let inviteListItemView: ComponentView<Empty>
|
||||
var inviteListItemTransition = transition
|
||||
if let current = self.inviteListItemViews[inviteOption.id] {
|
||||
inviteListItemView = current
|
||||
} else {
|
||||
inviteListItemView = ComponentView()
|
||||
self.inviteListItemViews[inviteOption.id] = inviteListItemView
|
||||
inviteListItemTransition = inviteListItemTransition.withAnimation(.none)
|
||||
}
|
||||
|
||||
inviteListItemSizes.append(inviteListItemView.update(
|
||||
transition: inviteListItemTransition,
|
||||
component: AnyComponent(VideoChatListInviteComponent(
|
||||
title: inviteText,
|
||||
icon: iconType,
|
||||
theme: component.theme,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.openInviteMembers(inviteOption.type)
|
||||
}
|
||||
component.openInviteMembers()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
))
|
||||
}
|
||||
|
||||
var gridParticipants: [VideoParticipant] = []
|
||||
var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||
@ -1824,7 +1877,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
gridItemCount: gridParticipants.count,
|
||||
listItemCount: listParticipants.count + component.invitedPeers.count,
|
||||
listItemHeight: measureListItemSize.height,
|
||||
listTrailingItemHeight: inviteListItemSize.height
|
||||
listTrailingItemHeights: inviteListItemSizes.map(\.height)
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
|
@ -25,6 +25,7 @@ import LegacyComponents
|
||||
import TooltipUI
|
||||
import BlurredBackgroundComponent
|
||||
import CallsEmoji
|
||||
import InviteLinksUI
|
||||
|
||||
extension VideoChatCall {
|
||||
var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> {
|
||||
@ -652,6 +653,51 @@ final class VideoChatScreenComponent: Component {
|
||||
guard case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
||||
if groupCall.isConference {
|
||||
guard let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
guard let currentReference = groupCall.currentReference, case let .id(callId, accessHash) = currentReference else {
|
||||
return
|
||||
}
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
var presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
let controller = InviteLinkInviteController(
|
||||
context: groupCall.accountContext,
|
||||
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
|
||||
mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(
|
||||
callId: callId,
|
||||
accessHash: accessHash,
|
||||
isRecentlyCreated: false,
|
||||
canRevoke: callState.canManageCall
|
||||
)),
|
||||
initialInvite: .link(link: inviteLinks.listenerLink, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: groupCall.accountContext.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil),
|
||||
parentNavigationController: navigationController,
|
||||
completed: { [weak self] result in
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
if let result {
|
||||
switch result {
|
||||
case .linkCopied:
|
||||
//TODO:localize
|
||||
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
return false
|
||||
}), in: .current)
|
||||
case .openCall:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
self.environment?.controller()?.present(controller, in: .window(.root), with: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let formatSendTitle: (String) -> String = { string in
|
||||
var string = string
|
||||
@ -705,7 +751,7 @@ final class VideoChatScreenComponent: Component {
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
||||
)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerList in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerList in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
@ -1051,7 +1097,7 @@ final class VideoChatScreenComponent: Component {
|
||||
|
||||
static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [InvitedPeer]), NoError> {
|
||||
let invitedPeers = conferenceSource.context.engine.data.subscribe(
|
||||
EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0) })
|
||||
EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0.id) })
|
||||
)
|
||||
|
||||
let accountPeerId = conferenceSource.context.account.peerId
|
||||
@ -1759,12 +1805,19 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
|
||||
if canInvite {
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite
|
||||
var inviteOptions: [VideoChatParticipantsComponent.Participants.InviteOption] = []
|
||||
if case let .group(groupCall) = self.currentCall, groupCall.isConference {
|
||||
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: .invite(isMultipleUsers: false)))
|
||||
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 1, type: .shareLink))
|
||||
} else {
|
||||
if canInvite {
|
||||
let inviteType: VideoChatParticipantsComponent.Participants.InviteType
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite(isMultipleUsers: false)
|
||||
}
|
||||
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: inviteType))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1773,7 +1826,7 @@ final class VideoChatScreenComponent: Component {
|
||||
participants: members.participants,
|
||||
totalCount: members.totalCount,
|
||||
loadMoreToken: members.loadMoreToken,
|
||||
inviteType: inviteType
|
||||
inviteOptions: inviteOptions
|
||||
)
|
||||
}
|
||||
|
||||
@ -2038,7 +2091,13 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
|
||||
var encryptionKeyFrame: CGRect?
|
||||
if let encryptionKeyEmoji = self.encryptionKeyEmoji {
|
||||
var isConference = false
|
||||
if case let .group(groupCall) = self.currentCall {
|
||||
isConference = groupCall.isConference
|
||||
} else if case .conferenceSource = self.currentCall {
|
||||
isConference = true
|
||||
}
|
||||
if isConference {
|
||||
navigationHeight -= 2.0
|
||||
let encryptionKey: ComponentView<Empty>
|
||||
var encryptionKeyTransition = transition
|
||||
@ -2055,7 +2114,7 @@ final class VideoChatScreenComponent: Component {
|
||||
component: AnyComponent(VideoChatEncryptionKeyComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
emoji: encryptionKeyEmoji,
|
||||
emoji: self.encryptionKeyEmoji ?? [],
|
||||
isExpanded: self.isEncryptionKeyExpanded,
|
||||
tapAction: { [weak self] in
|
||||
guard let self else {
|
||||
@ -2326,11 +2385,18 @@ final class VideoChatScreenComponent: Component {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
openInviteMembers: { [weak self] in
|
||||
openInviteMembers: { [weak self] type in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openInviteMembers()
|
||||
if case .shareLink = type {
|
||||
guard let inviteLinks = self.inviteLinks else {
|
||||
return
|
||||
}
|
||||
self.presentShare(inviteLinks)
|
||||
} else {
|
||||
self.openInviteMembers()
|
||||
}
|
||||
},
|
||||
visibleParticipantsUpdated: { [weak self] visibleParticipants in
|
||||
guard let self else {
|
||||
|
@ -16,31 +16,6 @@ extension VideoChatScreenComponent.View {
|
||||
return
|
||||
}
|
||||
|
||||
/*if groupCall.accountContext.sharedContext.immediateExperimentalUISettings.conferenceDebug {
|
||||
guard let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
var presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
let controller = InviteLinkInviteController(context: groupCall.accountContext, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), mode: .groupCall(link: "https://t.me/call/+abbfbffll123", isRecentlyCreated: false), parentNavigationController: navigationController, completed: { [weak self] result in
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
if let result {
|
||||
switch result {
|
||||
case .linkCopied:
|
||||
//TODO:localize
|
||||
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
return false
|
||||
}), in: .current)
|
||||
}
|
||||
}
|
||||
})
|
||||
self.environment?.controller()?.present(controller, in: .window(.root), with: nil)
|
||||
return
|
||||
}*/
|
||||
|
||||
if groupCall.isConference {
|
||||
var disablePeerIds: [EnginePeer.Id] = []
|
||||
disablePeerIds.append(groupCall.accountContext.account.peerId)
|
||||
@ -51,13 +26,21 @@ extension VideoChatScreenComponent.View {
|
||||
}
|
||||
}
|
||||
}
|
||||
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in
|
||||
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, shareLink: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let inviteLinks = self.inviteLinks else {
|
||||
return
|
||||
}
|
||||
self.presentShare(inviteLinks)
|
||||
}, completion: { [weak self] peerIds in
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
||||
for peerId in peerIds {
|
||||
let _ = groupCall.invitePeer(peerId)
|
||||
let _ = groupCall.invitePeer(peerId.id, isVideo: peerId.isVideo)
|
||||
}
|
||||
})
|
||||
self.environment?.controller()?.push(controller)
|
||||
@ -80,7 +63,7 @@ extension VideoChatScreenComponent.View {
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite
|
||||
inviteType = .invite(isMultipleUsers: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +129,8 @@ extension VideoChatScreenComponent.View {
|
||||
if let participant {
|
||||
dismissController?()
|
||||
|
||||
if groupCall.invitePeer(participant.peer.id) {
|
||||
//TODO:release
|
||||
if groupCall.invitePeer(participant.peer.id, isVideo: false) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
@ -258,7 +242,8 @@ extension VideoChatScreenComponent.View {
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if groupCall.invitePeer(peer.id) {
|
||||
//TODO:release
|
||||
if groupCall.invitePeer(peer.id, isVideo: false) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
@ -330,7 +315,8 @@ extension VideoChatScreenComponent.View {
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if groupCall.invitePeer(peer.id) {
|
||||
//TODO:release
|
||||
if groupCall.invitePeer(peer.id, isVideo: false) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
|
@ -1256,7 +1256,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
|
||||
if let participant = participant {
|
||||
dismissController?()
|
||||
|
||||
if strongSelf.call.invitePeer(participant.peer.id) {
|
||||
//TODO:release
|
||||
if strongSelf.call.invitePeer(participant.peer.id, isVideo: false) {
|
||||
let text: String
|
||||
if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
|
||||
@ -1364,7 +1365,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if strongSelf.call.invitePeer(peer.id) {
|
||||
//TODO:release
|
||||
if strongSelf.call.invitePeer(peer.id, isVideo: false) {
|
||||
let text: String
|
||||
if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
|
||||
@ -1432,7 +1434,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if strongSelf.call.invitePeer(peer.id) {
|
||||
//TODO:release
|
||||
if strongSelf.call.invitePeer(peer.id, isVideo: false) {
|
||||
let text: String
|
||||
if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
|
||||
|
@ -93,13 +93,18 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
|
||||
tags.insert(.webPage)
|
||||
} else if let action = attachment as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case let .phoneCall(_, discardReason, _, _):
|
||||
globalTags.insert(.Calls)
|
||||
if incoming, let discardReason = discardReason, case .missed = discardReason {
|
||||
globalTags.insert(.MissedCalls)
|
||||
}
|
||||
default:
|
||||
break
|
||||
case let .phoneCall(_, discardReason, _, _):
|
||||
globalTags.insert(.Calls)
|
||||
if incoming, let discardReason = discardReason, case .missed = discardReason {
|
||||
globalTags.insert(.MissedCalls)
|
||||
}
|
||||
case let .conferenceCall(conferenceCall):
|
||||
globalTags.insert(.Calls)
|
||||
if incoming, conferenceCall.flags.contains(.isMissed) {
|
||||
globalTags.insert(.MissedCalls)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else if let location = attachment as? TelegramMediaMap, location.liveBroadcastingTimeout != nil {
|
||||
tags.insert(.liveLocation)
|
||||
@ -118,9 +123,6 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
|
||||
}
|
||||
}
|
||||
|
||||
if !incoming {
|
||||
assert(true)
|
||||
}
|
||||
return (tags, globalTags)
|
||||
}
|
||||
|
||||
|
@ -201,12 +201,28 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
|
||||
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))
|
||||
case let .messageActionPaidMessagesPrice(stars):
|
||||
return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars))
|
||||
case let .messageActionConferenceCall(_, callId, duration, otherParticipants):
|
||||
return TelegramMediaAction(action: .conferenceCall(
|
||||
case let .messageActionConferenceCall(flags, callId, duration, otherParticipants):
|
||||
let isMissed = (flags & (1 << 0)) != 0
|
||||
let isActive = (flags & (1 << 1)) != 0
|
||||
let isVideo = (flags & (1 << 4)) != 0
|
||||
|
||||
var mappedFlags = TelegramMediaActionType.ConferenceCall.Flags()
|
||||
if isMissed {
|
||||
mappedFlags.insert(.isMissed)
|
||||
}
|
||||
if isActive {
|
||||
mappedFlags.insert(.isActive)
|
||||
}
|
||||
if isVideo {
|
||||
mappedFlags.insert(.isVideo)
|
||||
}
|
||||
|
||||
return TelegramMediaAction(action: .conferenceCall(TelegramMediaActionType.ConferenceCall(
|
||||
callId: callId,
|
||||
duration: duration,
|
||||
flags: mappedFlags,
|
||||
otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? []
|
||||
))
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,7 +382,7 @@ private final class CallSessionContext {
|
||||
private final class IncomingConferenceInvitationContext {
|
||||
enum State: Equatable {
|
||||
case pending
|
||||
case ringing(callId: Int64, otherParticipants: [EnginePeer])
|
||||
case ringing(callId: Int64, isVideo: Bool, otherParticipants: [EnginePeer])
|
||||
case stopped
|
||||
}
|
||||
|
||||
@ -420,11 +420,11 @@ private final class IncomingConferenceInvitationContext {
|
||||
}
|
||||
}
|
||||
|
||||
if let action = foundAction, case let .conferenceCall(callId, duration, otherParticipants) = action.action {
|
||||
if duration != nil {
|
||||
if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||
if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil {
|
||||
state = .stopped
|
||||
} else {
|
||||
state = .ringing(callId: callId, otherParticipants: otherParticipants.compactMap { id -> EnginePeer? in
|
||||
state = .ringing(callId: conferenceCall.callId, isVideo: conferenceCall.flags.contains(.isVideo), otherParticipants: conferenceCall.otherParticipants.compactMap { id -> EnginePeer? in
|
||||
return message.peers[id].flatMap(EnginePeer.init)
|
||||
})
|
||||
}
|
||||
@ -639,11 +639,11 @@ private final class CallSessionManagerContext {
|
||||
}
|
||||
}
|
||||
for (id, context) in self.incomingConferenceInvitationContexts {
|
||||
if case let .ringing(_, otherParticipants) = context.state {
|
||||
if case let .ringing(_, isVideo, otherParticipants) = context.state {
|
||||
ringingContexts.append(CallSessionRingingState(
|
||||
id: context.internalId,
|
||||
peerId: id.peerId,
|
||||
isVideo: false,
|
||||
isVideo: isVideo,
|
||||
isVideoPossible: true,
|
||||
conferenceSource: id,
|
||||
otherParticipants: otherParticipants
|
||||
@ -718,6 +718,23 @@ private final class CallSessionManagerContext {
|
||||
}
|
||||
}
|
||||
|
||||
func dropOutgoingConferenceRequest(messageId: MessageId) {
|
||||
let addUpdates = self.addUpdates
|
||||
let rejectSignal = self.network.request(Api.functions.phone.declineConferenceCallInvite(msgId: messageId.id))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<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>) {
|
||||
for (id, context) in self.incomingConferenceInvitationContexts {
|
||||
if context.internalId == internalId {
|
||||
@ -1383,6 +1400,12 @@ public final class CallSessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
public func dropOutgoingConferenceRequest(messageId: MessageId) {
|
||||
self.withContext { context in
|
||||
context.dropOutgoingConferenceRequest(messageId: messageId)
|
||||
}
|
||||
}
|
||||
|
||||
func drop(stableId: CallSessionStableId, reason: DropCallReason) {
|
||||
self.withContext { context in
|
||||
context.drop(stableId: stableId, reason: reason)
|
||||
|
@ -86,6 +86,31 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConferenceCall: Equatable {
|
||||
public struct Flags: OptionSet {
|
||||
public var rawValue: Int32
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let isVideo = Flags(rawValue: 1 << 0)
|
||||
public static let isActive = Flags(rawValue: 1 << 1)
|
||||
public static let isMissed = Flags(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
public let callId: Int64
|
||||
public let duration: Int32?
|
||||
public let flags: Flags
|
||||
public let otherParticipants: [PeerId]
|
||||
|
||||
public init(callId: Int64, duration: Int32?, flags: Flags, otherParticipants: [PeerId]) {
|
||||
self.callId = callId
|
||||
self.duration = duration
|
||||
self.flags = flags
|
||||
self.otherParticipants = otherParticipants
|
||||
}
|
||||
}
|
||||
|
||||
case unknown
|
||||
case groupCreated(title: String)
|
||||
case addedMembers(peerIds: [PeerId])
|
||||
@ -134,7 +159,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?)
|
||||
case paidMessagesRefunded(count: Int32, stars: Int64)
|
||||
case paidMessagesPriceEdited(stars: Int64)
|
||||
case conferenceCall(callId: Int64, duration: Int32?, otherParticipants: [PeerId])
|
||||
case conferenceCall(ConferenceCall)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
|
||||
@ -264,7 +289,12 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
case 47:
|
||||
self = .paidMessagesPriceEdited(stars: decoder.decodeInt64ForKey("stars", orElse: 0))
|
||||
case 48:
|
||||
self = .conferenceCall(callId: decoder.decodeInt64ForKey("cid", orElse: 0), duration: decoder.decodeOptionalInt32ForKey("dur"), otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init))
|
||||
self = .conferenceCall(ConferenceCall(
|
||||
callId: decoder.decodeInt64ForKey("cid", orElse: 0),
|
||||
duration: decoder.decodeOptionalInt32ForKey("dur"),
|
||||
flags: ConferenceCall.Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)),
|
||||
otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init)
|
||||
))
|
||||
default:
|
||||
self = .unknown
|
||||
}
|
||||
@ -642,15 +672,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
case let .paidMessagesPriceEdited(stars):
|
||||
encoder.encodeInt32(47, forKey: "_rawValue")
|
||||
encoder.encodeInt64(stars, forKey: "stars")
|
||||
case let .conferenceCall(callId, duration, otherParticipants):
|
||||
case let .conferenceCall(conferenceCall):
|
||||
encoder.encodeInt32(48, forKey: "_rawValue")
|
||||
encoder.encodeInt64(callId, forKey: "cid")
|
||||
if let duration {
|
||||
encoder.encodeInt64(conferenceCall.callId, forKey: "cid")
|
||||
if let duration = conferenceCall.duration {
|
||||
encoder.encodeInt32(duration, forKey: "dur")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "dur")
|
||||
}
|
||||
encoder.encodeInt64Array(otherParticipants.map({ $0.toInt64() }), forKey: "part")
|
||||
encoder.encodeInt32(conferenceCall.flags.rawValue, forKey: "flags")
|
||||
encoder.encodeInt64Array(conferenceCall.otherParticipants.map({ $0.toInt64() }), forKey: "part")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,6 +96,7 @@ public struct GroupCallInfo: Equatable {
|
||||
public var isVideoEnabled: Bool
|
||||
public var unmutedVideoLimit: Int
|
||||
public var isStream: Bool
|
||||
public var isCreator: Bool
|
||||
|
||||
public init(
|
||||
id: Int64,
|
||||
@ -110,7 +111,8 @@ public struct GroupCallInfo: Equatable {
|
||||
defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?,
|
||||
isVideoEnabled: Bool,
|
||||
unmutedVideoLimit: Int,
|
||||
isStream: Bool
|
||||
isStream: Bool,
|
||||
isCreator: Bool
|
||||
) {
|
||||
self.id = id
|
||||
self.accessHash = accessHash
|
||||
@ -125,6 +127,7 @@ public struct GroupCallInfo: Equatable {
|
||||
self.isVideoEnabled = isVideoEnabled
|
||||
self.unmutedVideoLimit = unmutedVideoLimit
|
||||
self.isStream = isStream
|
||||
self.isCreator = isCreator
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,7 +153,8 @@ extension GroupCallInfo {
|
||||
defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: (flags & (1 << 1)) != 0, canChange: (flags & (1 << 2)) != 0),
|
||||
isVideoEnabled: (flags & (1 << 9)) != 0,
|
||||
unmutedVideoLimit: Int(unmutedVideoLimit),
|
||||
isStream: (flags & (1 << 12)) != 0
|
||||
isStream: (flags & (1 << 12)) != 0,
|
||||
isCreator: (flags & (1 << 15)) != 0
|
||||
)
|
||||
case .groupCallDiscarded:
|
||||
return nil
|
||||
@ -454,17 +458,17 @@ public enum GetGroupCallParticipantsError {
|
||||
func _internal_getGroupCallParticipants(account: Account, reference: InternalGroupCallReference, offset: String, ssrcs: [UInt32], limit: Int32, sortAscending: Bool?) -> Signal<GroupCallParticipantsContext.State, GetGroupCallParticipantsError> {
|
||||
let accountPeerId = account.peerId
|
||||
|
||||
let sortAscendingValue: Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int, Bool), GetGroupCallParticipantsError>
|
||||
let sortAscendingValue: Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int, Bool, Bool), GetGroupCallParticipantsError>
|
||||
|
||||
sortAscendingValue = _internal_getCurrentGroupCall(account: account, reference: reference)
|
||||
|> mapError { _ -> GetGroupCallParticipantsError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int, Bool), GetGroupCallParticipantsError> in
|
||||
|> mapToSignal { result -> Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int, Bool, Bool), GetGroupCallParticipantsError> in
|
||||
guard let result = result else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
return .single((sortAscending ?? result.info.sortAscending, result.info.scheduleTimestamp, result.info.subscribedToScheduled, result.info.defaultParticipantsAreMuted, result.info.isVideoEnabled, result.info.unmutedVideoLimit, result.info.isStream))
|
||||
return .single((sortAscending ?? result.info.sortAscending, result.info.scheduleTimestamp, result.info.subscribedToScheduled, result.info.defaultParticipantsAreMuted, result.info.isVideoEnabled, result.info.unmutedVideoLimit, result.info.isStream, result.info.isCreator))
|
||||
}
|
||||
|
||||
return combineLatest(
|
||||
@ -481,7 +485,7 @@ func _internal_getGroupCallParticipants(account: Account, reference: InternalGro
|
||||
let version: Int32
|
||||
let nextParticipantsFetchOffset: String?
|
||||
|
||||
let (sortAscendingValue, scheduleTimestamp, subscribedToScheduled, defaultParticipantsAreMuted, isVideoEnabled, unmutedVideoLimit, isStream) = sortAscendingAndScheduleTimestamp
|
||||
let (sortAscendingValue, scheduleTimestamp, subscribedToScheduled, defaultParticipantsAreMuted, isVideoEnabled, unmutedVideoLimit, isStream, isCreator) = sortAscendingAndScheduleTimestamp
|
||||
|
||||
switch result {
|
||||
case let .groupParticipants(count, participants, nextOffset, chats, users, apiVersion):
|
||||
@ -506,7 +510,7 @@ func _internal_getGroupCallParticipants(account: Account, reference: InternalGro
|
||||
participants: parsedParticipants,
|
||||
nextParticipantsFetchOffset: nextParticipantsFetchOffset,
|
||||
adminIds: Set(),
|
||||
isCreator: false,
|
||||
isCreator: isCreator,
|
||||
defaultParticipantsAreMuted: defaultParticipantsAreMuted ?? GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false),
|
||||
sortAscending: sortAscendingValue,
|
||||
recordingStartTimestamp: nil,
|
||||
@ -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 transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
|
||||
|> mapToSignal { inputPeer -> Signal<MessageId?, NoError> in
|
||||
guard let inputPeer else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(call: .inputGroupCall(id: callId, accessHash: accessHash), userId: inputPeer))
|
||||
var flags: Int32 = 0
|
||||
if isVideo {
|
||||
flags |= 1 << 0
|
||||
}
|
||||
return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: reference.apiInputGroupCall, userId: inputPeer))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||
|> mapToSignal { result -> Signal<MessageId?, NoError> in
|
||||
if let result {
|
||||
account.stateManager.addUpdates(result)
|
||||
if let message = result.messageIds.first {
|
||||
return .single(message)
|
||||
}
|
||||
}
|
||||
return .complete()
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2952,6 +2963,29 @@ func _internal_createConferenceCall(postbox: Postbox, network: Network, accountP
|
||||
}
|
||||
}
|
||||
|
||||
public enum RevokeConferenceInviteLinkError {
|
||||
case generic
|
||||
}
|
||||
|
||||
func _internal_revokeConferenceInviteLink(account: Account, reference: InternalGroupCallReference, link: String) -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> {
|
||||
return account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 1, call: reference.apiInputGroupCall, joinMuted: .boolFalse))
|
||||
|> mapError { _ -> RevokeConferenceInviteLinkError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> in
|
||||
account.stateManager.addUpdates(result)
|
||||
|
||||
return _internal_groupCallInviteLinks(account: account, reference: reference, isConference: true)
|
||||
|> castError(RevokeConferenceInviteLinkError.self)
|
||||
|> mapToSignal { result -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> in
|
||||
guard let result = result else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
return .single(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConfirmAddConferenceParticipantError {
|
||||
case generic
|
||||
}
|
||||
|
@ -96,6 +96,10 @@ public extension TelegramEngine {
|
||||
public func createConferenceCall() -> Signal<EngineCreatedGroupCall, CreateConferenceCallError> {
|
||||
return _internal_createConferenceCall(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId)
|
||||
}
|
||||
|
||||
public func revokeConferenceInviteLink(reference: InternalGroupCallReference, link: String) -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> {
|
||||
return _internal_revokeConferenceInviteLink(account: self.account, reference: reference, link: link)
|
||||
}
|
||||
|
||||
public func pollConferenceCallBlockchain(reference: InternalGroupCallReference, subChainId: Int, offset: Int, limit: Int) -> Signal<(blocks: [Data], nextOffset: Int)?, NoError> {
|
||||
return _internal_pollConferenceCallBlockchain(network: self.account.network, reference: reference, subChainId: subChainId, offset: offset, limit: limit)
|
||||
@ -105,8 +109,8 @@ public extension TelegramEngine {
|
||||
return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block)
|
||||
}
|
||||
|
||||
public func inviteConferenceCallParticipant(callId: Int64, accessHash: Int64, peerId: EnginePeer.Id) -> Signal<Never, NoError> {
|
||||
return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId)
|
||||
public func inviteConferenceCallParticipant(reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<EngineMessage.Id?, NoError> {
|
||||
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> {
|
||||
|
@ -10,6 +10,7 @@ swift_library(
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
|
@ -9,6 +9,7 @@ import AppBundle
|
||||
import ChatMessageBubbleContentNode
|
||||
import ChatMessageItemCommon
|
||||
import ChatMessageDateAndStatusNode
|
||||
import SwiftSignalKit
|
||||
|
||||
private let titleFont: UIFont = Font.medium(16.0)
|
||||
private let labelFont: UIFont = Font.regular(13.0)
|
||||
@ -25,6 +26,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let iconNode: ASImageNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
|
||||
private var activeConferenceUpdateTimer: SwiftSignalKit.Timer?
|
||||
|
||||
required public init() {
|
||||
self.titleNode = TextNode()
|
||||
self.labelNode = TextNode()
|
||||
@ -57,6 +60,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.activeConferenceUpdateTimer?.invalidate()
|
||||
}
|
||||
|
||||
override public func accessibilityActivate() -> Bool {
|
||||
self.callButtonPressed()
|
||||
return true
|
||||
@ -90,6 +97,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var callDuration: Int32?
|
||||
var callSuccessful = true
|
||||
var isVideo = false
|
||||
var hasCallButton = true
|
||||
var updateConferenceTimerEndTimeout: Int32?
|
||||
for media in item.message.media {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
|
||||
isVideo = isVideoValue
|
||||
@ -123,11 +132,32 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
break
|
||||
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(_, duration, _) = action.action {
|
||||
isVideo = false
|
||||
callDuration = duration
|
||||
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||
isVideo = conferenceCall.flags.contains(.isVideo)
|
||||
callDuration = conferenceCall.duration
|
||||
//TODO:localize
|
||||
titleString = "Group Call"
|
||||
let missedTimeout: Int32
|
||||
#if DEBUG
|
||||
missedTimeout = 5
|
||||
#else
|
||||
missedTimeout = 30
|
||||
#endif
|
||||
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||
if conferenceCall.flags.contains(.isMissed) {
|
||||
titleString = "Declined Group Call"
|
||||
} else if item.message.timestamp < currentTime - missedTimeout {
|
||||
titleString = "Missed Group Call"
|
||||
} else if conferenceCall.duration != nil {
|
||||
titleString = "Cancelled Group Call"
|
||||
hasCallButton = true
|
||||
} else {
|
||||
if incoming {
|
||||
titleString = "Incoming Group Call"
|
||||
} else {
|
||||
titleString = "Outgoing Group Call"
|
||||
}
|
||||
updateConferenceTimerEndTimeout = (item.message.timestamp + missedTimeout) - currentTime
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -211,7 +241,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
||||
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
||||
|
||||
boundingSize.width += 54.0
|
||||
if hasCallButton {
|
||||
boundingSize.width += 54.0
|
||||
}
|
||||
|
||||
return (boundingSize.width, { boundingWidth in
|
||||
return (boundingSize, { [weak self] animation, _, _ in
|
||||
@ -234,6 +266,22 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let buttonImage = buttonImage {
|
||||
strongSelf.buttonNode.setImage(buttonImage, for: [])
|
||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size)
|
||||
strongSelf.buttonNode.isHidden = !hasCallButton
|
||||
}
|
||||
|
||||
if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer {
|
||||
activeConferenceUpdateTimer.invalidate()
|
||||
strongSelf.activeConferenceUpdateTimer = nil
|
||||
}
|
||||
if let updateConferenceTimerEndTimeout, updateConferenceTimerEndTimeout >= 0 {
|
||||
strongSelf.activeConferenceUpdateTimer?.invalidate()
|
||||
strongSelf.activeConferenceUpdateTimer = SwiftSignalKit.Timer(timeout: Double(updateConferenceTimerEndTimeout) + 0.5, repeat: false, completion: { [weak strongSelf] in
|
||||
guard let strongSelf else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestInlineUpdate?()
|
||||
}, queue: .mainQueue())
|
||||
strongSelf.activeConferenceUpdateTimer?.start()
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -270,6 +318,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
if self.buttonNode.isHidden {
|
||||
return ChatMessageBubbleContentTapAction(content: .none)
|
||||
}
|
||||
|
||||
if self.buttonNode.frame.contains(point) {
|
||||
return ChatMessageBubbleContentTapAction(content: .ignore)
|
||||
} else if self.bounds.contains(point), let item = self.item {
|
||||
|
@ -405,7 +405,7 @@ private final class JoinSubjectScreenComponent: Component {
|
||||
isStream: false
|
||||
),
|
||||
reference: .link(slug: groupCall.slug),
|
||||
mode: .joining
|
||||
beginWithVideo: false
|
||||
)
|
||||
|
||||
self.environment?.controller()?.dismiss()
|
||||
|
@ -14125,7 +14125,7 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa
|
||||
|
||||
createInviteLinkImpl = { [weak contactsController] in
|
||||
contactsController?.view.window?.endEditing(true)
|
||||
contactsController?.present(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: groupPeer.id), parentNavigationController: contactsController?.navigationController as? NavigationController), in: .window(.root))
|
||||
contactsController?.present(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: groupPeer.id), initialInvite: nil, parentNavigationController: contactsController?.navigationController as? NavigationController), in: .window(.root))
|
||||
}
|
||||
|
||||
parentController?.push(contactsController)
|
||||
|
@ -136,6 +136,20 @@ public final class TextNodeWithEntities {
|
||||
}
|
||||
}
|
||||
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
public var linkHighlightColor: UIColor?
|
||||
public var linkHighlightInset: UIEdgeInsets = .zero
|
||||
|
||||
public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
|
||||
public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
|
||||
public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? {
|
||||
didSet {
|
||||
self.updateInteractiveActions()
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.textNode = TextNode()
|
||||
}
|
||||
@ -301,6 +315,83 @@ public final class TextNodeWithEntities {
|
||||
self.inlineStickerItemLayers.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateInteractiveActions() {
|
||||
if self.highlightAttributeAction != nil {
|
||||
if self.tapRecognizer == nil {
|
||||
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
|
||||
tapRecognizer.highlight = { [weak self] point in
|
||||
if let strongSelf = self, let cachedLayout = strongSelf.textNode.cachedLayout {
|
||||
var rects: [CGRect]?
|
||||
if let point = point {
|
||||
if let (index, attributes) = strongSelf.textNode.attributesAtPoint(CGPoint(x: point.x, y: point.y)) {
|
||||
if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) {
|
||||
let initialRects = strongSelf.textNode.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index)
|
||||
if let initialRects = initialRects, case .center = cachedLayout.resolvedAlignment {
|
||||
var mappedRects: [CGRect] = []
|
||||
for i in 0 ..< initialRects.count {
|
||||
let lineRect = initialRects[i].0
|
||||
var itemRect = initialRects[i].1
|
||||
itemRect.origin.x = floor((strongSelf.textNode.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x
|
||||
mappedRects.append(itemRect)
|
||||
}
|
||||
rects = mappedRects
|
||||
} else {
|
||||
rects = strongSelf.textNode.attributeRects(name: selectedAttribute.rawValue, at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if var rects, !rects.isEmpty {
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = strongSelf.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear)
|
||||
strongSelf.linkHighlightingNode = linkHighlightingNode
|
||||
strongSelf.textNode.addSubnode(linkHighlightingNode)
|
||||
}
|
||||
linkHighlightingNode.frame = strongSelf.textNode.bounds
|
||||
rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset)
|
||||
linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) })
|
||||
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
|
||||
strongSelf.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
self.textNode.view.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
} else if let tapRecognizer = self.tapRecognizer {
|
||||
self.tapRecognizer = nil
|
||||
self.textNode.view.removeGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
|
||||
self.tapAttributeAction?(attributes, index)
|
||||
}
|
||||
case .longTap:
|
||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
|
||||
self.longTapAttributeAction?(attributes, index)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ImmediateTextNodeWithEntities: TextNode {
|
||||
|
@ -398,6 +398,8 @@ final class AuthorizedApplicationContext {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case .messageAutoremoveTimeoutUpdated = action.action {
|
||||
return
|
||||
} else if case .conferenceCall = action.action {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2890,14 +2890,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
break
|
||||
}
|
||||
}
|
||||
guard case let .conferenceCall(callId, duration, _) = action?.action else {
|
||||
guard case let .conferenceCall(conferenceCall) = action?.action else {
|
||||
return
|
||||
}
|
||||
if duration != nil {
|
||||
if conferenceCall.duration != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if let currentGroupCallController = self.context.sharedContext as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == callId {
|
||||
if let currentGroupCallController = self.context.sharedContext as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == conferenceCall.callId {
|
||||
self.context.sharedContext.navigateToCurrentCall()
|
||||
return
|
||||
}
|
||||
@ -2919,7 +2919,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
isStream: false
|
||||
),
|
||||
reference: .message(id: message.id),
|
||||
mode: .joining
|
||||
beginWithVideo: conferenceCall.flags.contains(.isVideo)
|
||||
)
|
||||
})
|
||||
}, longTap: { [weak self] action, params in
|
||||
|
@ -60,6 +60,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
||||
}
|
||||
|
||||
private let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||
private let isPeerEnabled: (ContactListPeer) -> Bool
|
||||
var dismissed: (() -> Void)?
|
||||
|
||||
var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in }
|
||||
@ -107,6 +108,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
||||
self.displayDeviceContacts = params.displayDeviceContacts
|
||||
self.displayCallIcons = params.displayCallIcons
|
||||
self.confirmation = params.confirmation
|
||||
self.isPeerEnabled = params.isPeerEnabled
|
||||
self.multipleSelection = params.multipleSelection
|
||||
self.requirePhoneNumbers = params.requirePhoneNumbers
|
||||
self.allowChannelsInSearch = params.allowChannelsInSearch
|
||||
@ -218,7 +220,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch)
|
||||
self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch, isPeerEnabled: self.isPeerEnabled)
|
||||
self._ready.set(self.contactsNode.contactListNode.ready)
|
||||
|
||||
self.contactsNode.navigationBar = self.navigationBar
|
||||
|
@ -44,6 +44,8 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
||||
var cancelSearch: (() -> Void)?
|
||||
var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)?
|
||||
|
||||
let isPeerEnabled: (ContactListPeer) -> Bool
|
||||
|
||||
var presentationData: PresentationData {
|
||||
didSet {
|
||||
self.presentationDataPromise.set(.single(self.presentationData))
|
||||
@ -57,12 +59,13 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
||||
|
||||
var searchContainerNode: ContactsSearchContainerNode?
|
||||
|
||||
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool) {
|
||||
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool, isPeerEnabled: @escaping (ContactListPeer) -> Bool) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.displayDeviceContacts = displayDeviceContacts
|
||||
self.displayCallIcons = displayCallIcons
|
||||
self.allowChannelsInSearch = allowChannelsInSearch
|
||||
self.isPeerEnabled = isPeerEnabled
|
||||
|
||||
var excludeSelf = true
|
||||
|
||||
@ -124,7 +127,9 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
|
||||
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, isPeerEnabled: { peer in
|
||||
return isPeerEnabled(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil))
|
||||
}, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
|
||||
contextActionImpl?(peer, node, gesture, nil)
|
||||
} : nil, multipleSelection: multipleSelection)
|
||||
|
||||
|
@ -402,6 +402,15 @@ func openResolvedUrlImpl(
|
||||
members: resolvedCallLink.members,
|
||||
totalMemberCount: resolvedCallLink.totalMemberCount
|
||||
))))
|
||||
}, error: { _ in
|
||||
var elevatedLayout = true
|
||||
if case .chat = urlContext {
|
||||
elevatedLayout = false
|
||||
}
|
||||
//TODO:localize
|
||||
present(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: "This link is no longer active"), elevatedLayout: elevatedLayout, animateInAsReplacement: false, action: { _ in
|
||||
return true
|
||||
}), nil)
|
||||
})
|
||||
case let .localization(identifier):
|
||||
dismissInput()
|
||||
@ -788,6 +797,7 @@ func openResolvedUrlImpl(
|
||||
}
|
||||
if let currentState = starsContext.currentState, currentState.balance >= StarsAmount(value: amount, nanos: 0) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
//TODO:localize
|
||||
let controller = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universal(
|
||||
|
@ -270,11 +270,13 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
|
||||
public var kind: Kind
|
||||
public var peerId: Int64
|
||||
public var audioSsrc: UInt32
|
||||
public var videoDescription: String?
|
||||
|
||||
public init(kind: Kind, audioSsrc: UInt32, videoDescription: String?) {
|
||||
public init(kind: Kind, peerId: Int64, audioSsrc: UInt32, videoDescription: String?) {
|
||||
self.kind = kind
|
||||
self.peerId = peerId
|
||||
self.audioSsrc = audioSsrc
|
||||
self.videoDescription = videoDescription
|
||||
}
|
||||
@ -575,6 +577,7 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
return OngoingGroupCallMediaChannelDescription(
|
||||
type: mappedType,
|
||||
peerId: channel.peerId,
|
||||
audioSsrc: channel.audioSsrc,
|
||||
videoDescription: channel.videoDescription
|
||||
)
|
||||
@ -688,6 +691,7 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
return OngoingGroupCallMediaChannelDescription(
|
||||
type: mappedType,
|
||||
peerId: channel.peerId,
|
||||
audioSsrc: channel.audioSsrc,
|
||||
videoDescription: channel.videoDescription
|
||||
)
|
||||
|
@ -332,10 +332,12 @@ typedef NS_ENUM(int32_t, OngoingGroupCallMediaChannelType) {
|
||||
@interface OngoingGroupCallMediaChannelDescription : NSObject
|
||||
|
||||
@property (nonatomic, readonly) OngoingGroupCallMediaChannelType type;
|
||||
@property (nonatomic, readonly) uint64_t peerId;
|
||||
@property (nonatomic, readonly) uint32_t audioSsrc;
|
||||
@property (nonatomic, strong, readonly) NSString * _Nullable videoDescription;
|
||||
|
||||
- (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type
|
||||
peerId:(int64_t)peerId
|
||||
audioSsrc:(uint32_t)audioSsrc
|
||||
videoDescription:(NSString * _Nullable)videoDescription;
|
||||
|
||||
|
@ -2670,7 +2670,7 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
|
||||
},
|
||||
.createWrappedAudioDeviceModule = [audioDeviceModule, isActiveByDefault](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> {
|
||||
if (audioDeviceModule) {
|
||||
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault);
|
||||
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault || true);
|
||||
return result;
|
||||
} else {
|
||||
return nullptr;
|
||||
@ -3029,11 +3029,13 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
|
||||
@implementation OngoingGroupCallMediaChannelDescription
|
||||
|
||||
- (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type
|
||||
audioSsrc:(uint32_t)audioSsrc
|
||||
videoDescription:(NSString * _Nullable)videoDescription {
|
||||
peerId:(int64_t)peerId
|
||||
audioSsrc:(uint32_t)audioSsrc
|
||||
videoDescription:(NSString * _Nullable)videoDescription {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_type = type;
|
||||
_peerId = peerId;
|
||||
_audioSsrc = audioSsrc;
|
||||
_videoDescription = videoDescription;
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 16cb0d29562576be221a7ac2b8bdd81fdb954bc4
|
||||
Subproject commit a15014304d25193157ee809e8faceaca95dd8192
|
Loading…
x
Reference in New Issue
Block a user