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