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

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

View File

@ -22,6 +22,7 @@ internal:
- export PATH=/opt/homebrew/opt/ruby/bin:$PATH
- export PATH=`gem environment gemdir`/bin:$PATH
- python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appcenter-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64
- python3 -u build-system/Make/DeployToFirebase.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/firebase-configurations/firebase-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
- python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
environment:
name: internal

View File

@ -11,25 +11,26 @@ def is_apple_silicon():
return False
def get_clean_env():
def get_clean_env(use_clean_env=True):
clean_env = os.environ.copy()
clean_env['PATH'] = '/usr/bin:/bin:/usr/sbin:/sbin'
if use_clean_env:
clean_env['PATH'] = '/usr/bin:/bin:/usr/sbin:/sbin'
return clean_env
def resolve_executable(program):
def resolve_executable(program, use_clean_env=True):
def is_executable(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
for path in get_clean_env()["PATH"].split(os.pathsep):
for path in get_clean_env(use_clean_env=use_clean_env)["PATH"].split(os.pathsep):
executable_file = os.path.join(path, program)
if is_executable(executable_file):
return executable_file
return None
def run_executable_with_output(path, arguments, decode=True, input=None, stderr_to_stdout=True, print_command=False, check_result=False):
executable_path = resolve_executable(path)
def run_executable_with_output(path, arguments, use_clean_env=True, decode=True, input=None, stderr_to_stdout=True, print_command=False, check_result=False):
executable_path = resolve_executable(path, use_clean_env=use_clean_env)
if executable_path is None:
raise Exception('Could not resolve {} to a valid executable file'.format(path))
@ -45,7 +46,7 @@ def run_executable_with_output(path, arguments, decode=True, input=None, stderr_
stdout=subprocess.PIPE,
stderr=stderr_assignment,
stdin=subprocess.PIPE,
env=get_clean_env()
env=get_clean_env(use_clean_env=use_clean_env)
)
if input is not None:
output_data, _ = process.communicate(input=input)

View File

@ -0,0 +1,81 @@
import os
import sys
import argparse
import json
import re
from BuildEnvironment import run_executable_with_output
def deploy_to_firebase(args):
if not os.path.exists(args.configuration):
print('{} does not exist'.format(args.configuration))
sys.exit(1)
if not os.path.exists(args.ipa):
print('{} does not exist'.format(args.ipa))
sys.exit(1)
if args.dsyms is not None and not os.path.exists(args.dsyms):
print('{} does not exist'.format(args.dsyms))
sys.exit(1)
with open(args.configuration) as file:
configuration_dict = json.load(file)
required_keys = [
'app_id',
'group',
]
for key in required_keys:
if key not in configuration_dict:
print('Configuration at {} does not contain {}'.format(args.configuration, key))
sys.exit(1)
firebase_arguments = [
'appdistribution:distribute',
'--app', configuration_dict['app_id'],
'--groups', configuration_dict['group'],
args.ipa
]
output = run_executable_with_output(
'firebase',
firebase_arguments,
use_clean_env=False,
check_result=True
)
sharing_link_match = re.search(r'Share this release with testers who have access: (https://\S+)', output)
if sharing_link_match:
print(f"Sharing link: {sharing_link_match.group(1)}")
else:
print("No sharing link found in the output.")
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='deploy-firebase')
parser.add_argument(
'--configuration',
required=True,
help='Path to configuration json.'
)
parser.add_argument(
'--ipa',
required=True,
help='Path to IPA.'
)
parser.add_argument(
'--dsyms',
required=False,
help='Path to DSYMs.zip.'
)
parser.add_argument(
'--debug',
action='store_true',
help='Enable debug output for firebase deploy.'
)
if len(sys.argv) < 2:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
deploy_to_firebase(args)

View File

@ -111,10 +111,27 @@ public final class ContactSelectionControllerParams {
public let requirePhoneNumbers: Bool
public let allowChannelsInSearch: Bool
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
public let isPeerEnabled: (ContactListPeer) -> Bool
public let openProfile: ((EnginePeer) -> Void)?
public let sendMessage: ((EnginePeer) -> Void)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: ContactSelectionControllerMode = .generic, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, allowChannelsInSearch: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) }, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) {
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
mode: ContactSelectionControllerMode = .generic,
autoDismiss: Bool = true,
title: @escaping (PresentationStrings) -> String,
options: Signal<[ContactListAdditionalOption], NoError> = .single([]),
displayDeviceContacts: Bool = false,
displayCallIcons: Bool = false,
multipleSelection: Bool = false,
requirePhoneNumbers: Bool = false,
allowChannelsInSearch: Bool = false,
confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) },
isPeerEnabled: @escaping (ContactListPeer) -> Bool = { _ in true },
openProfile: ((EnginePeer) -> Void)? = nil,
sendMessage: ((EnginePeer) -> Void)? = nil
) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.mode = mode
@ -127,6 +144,7 @@ public final class ContactSelectionControllerParams {
self.requirePhoneNumbers = requirePhoneNumbers
self.allowChannelsInSearch = allowChannelsInSearch
self.confirmation = confirmation
self.isPeerEnabled = isPeerEnabled
self.openProfile = openProfile
self.sendMessage = sendMessage
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -692,6 +692,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
var reallyHighlighted = self.isHighlighted
if let item = self.item, !item.enabled {
reallyHighlighted = false
}
let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0
if let item = self.item {
switch item.peer {
@ -1649,6 +1652,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
actionButtonNode.setImage(actionButton.image, for: .normal)
transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionButtonImage.size.width - offset, y: floor((nodeLayout.contentSize.height - actionButtonImage.size.height) / 2.0)), size: actionButtonImage.size))
actionButtonNode.isEnabled = item.enabled
actionButtonNode.alpha = item.enabled ? 1.0 : 0.4
offset += actionButtonImage.size.width + 12.0
}
}

View File

@ -100,7 +100,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case enableReactionOverrides(Bool)
case compressedEmojiCache(Bool)
case storiesJpegExperiment(Bool)
case conferenceDebug(Bool)
case checkSerializedData(Bool)
case enableQuickReactionSwitch(Bool)
case disableReloginTokens(Bool)
@ -134,7 +133,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .conferenceDebug, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation:
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation:
return DebugControllerSection.experiments.rawValue
case .logTranslationRecognition, .resetTranslationStates:
return DebugControllerSection.translation.rawValue
@ -243,8 +242,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 47
case .disableReloginTokens:
return 48
case .conferenceDebug:
return 49
case .checkSerializedData:
return 50
case .enableQuickReactionSwitch:
@ -1311,16 +1308,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
})
}).start()
})
case let .conferenceDebug(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Conference Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
settings.conferenceDebug = value
return PreferencesEntry(settings)
})
}).start()
})
case let .checkSerializedData(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Check Serialized Data", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
@ -1552,7 +1539,6 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.storiesJpegExperiment(experimentalSettings.storiesJpegExperiment))
entries.append(.disableReloginTokens(experimentalSettings.disableReloginTokens))
entries.append(.conferenceDebug(experimentalSettings.conferenceDebug))
entries.append(.checkSerializedData(experimentalSettings.checkSerializedData))
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
entries.append(.liveStreamV2(experimentalSettings.liveStreamV2))

View File

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

View File

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

View File

@ -229,6 +229,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
if let invite = invite {
arguments.openLink(invite)
}
}, openCallAction: {
})
case let .mainLinkOtherInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: nil, style: .blocks, tag: nil)
@ -529,7 +530,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
} else {
isGroup = true
}
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
})
})))
@ -718,7 +719,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
isGroup = true
}
Queue.mainQueue().after(0.2) {
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
}
})
})))

View File

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

View File

@ -14,6 +14,7 @@ import Markdown
import TextFormat
import ComponentFlow
import MultilineTextComponent
import TextNodeWithEntities
private func actionButtonImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in
@ -46,6 +47,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let shareAction: (() -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
let viewAction: (() -> Void)?
let openCallAction: (() -> Void)?
public let tag: ItemListItemTag?
public init(
@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
shareAction: (() -> Void)?,
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?,
viewAction: (() -> Void)?,
openCallAction: (() -> Void)?,
tag: ItemListItemTag? = nil
) {
self.context = context
@ -83,6 +86,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.shareAction = shareAction
self.contextAction = contextAction
self.viewAction = viewAction
self.openCallAction = openCallAction
self.tag = tag
}
@ -147,7 +151,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
private var shimmerNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var justCreatedCallTextNode: TextNode?
private var justCreatedCallTextNode: TextNodeWithEntities?
private var justCreatedCallLeftSeparatorLayer: SimpleLayer?
private var justCreatedCallRightSeparatorLayer: SimpleLayer?
private var justCreatedCallSeparatorText: ComponentView<Empty>?
@ -299,7 +303,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
public func asyncLayout() -> (_ item: ItemListPermanentInviteLinkItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeAddressLayout = TextNode.asyncLayout(self.addressNode)
let makeInvitedPeersLayout = TextNode.asyncLayout(self.invitedPeersNode)
let makeJustCreatedCallTextNodeLayout = TextNode.asyncLayout(self.justCreatedCallTextNode)
let makeJustCreatedCallTextNodeLayout = TextNodeWithEntities.asyncLayout(self.justCreatedCallTextNode)
let currentItem = self.item
let avatarsContext = self.avatarsContext
@ -343,7 +347,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
let (invitedPeersLayout, invitedPeersApply) = makeInvitedPeersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var justCreatedCallTextNodeLayout: (TextNodeLayout, () -> TextNode?)?
var justCreatedCallTextNodeLayout: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities?)?
if item.isCall {
let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor)
@ -571,17 +575,39 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
if let justCreatedCallTextNodeLayout {
if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1() {
if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: .gray,
attemptSynchronous: true
)) {
if strongSelf.justCreatedCallTextNode !== justCreatedCallTextNode {
strongSelf.justCreatedCallTextNode?.removeFromSupernode()
strongSelf.justCreatedCallTextNode?.textNode.removeFromSupernode()
strongSelf.justCreatedCallTextNode = justCreatedCallTextNode
//justCreatedCallTextNode.highlig
strongSelf.addSubnode(justCreatedCallTextNode)
strongSelf.addSubnode(justCreatedCallTextNode.textNode)
}
justCreatedCallTextNode.linkHighlightColor = item.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.1)
justCreatedCallTextNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
}
justCreatedCallTextNode.tapAttributeAction = { [weak strongSelf] attributes, _ in
guard let strongSelf else {
return
}
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
strongSelf.item?.openCallAction?()
}
}
let justCreatedCallTextNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - justCreatedCallTextNodeLayout.0.size.width) / 2.0), y: shareButtonNode.frame.maxY + justCreatedCallTextSpacing), size: CGSize(width: justCreatedCallTextNodeLayout.0.size.width, height: justCreatedCallTextNodeLayout.0.size.height))
justCreatedCallTextNode.frame = justCreatedCallTextNodeFrame
justCreatedCallTextNode.textNode.frame = justCreatedCallTextNodeFrame
let justCreatedCallSeparatorText: ComponentView<Empty>
if let current = strongSelf.justCreatedCallSeparatorText {
@ -636,7 +662,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
}
} else if let justCreatedCallTextNode = strongSelf.justCreatedCallTextNode {
strongSelf.justCreatedCallTextNode = nil
justCreatedCallTextNode.removeFromSupernode()
justCreatedCallTextNode.textNode.removeFromSupernode()
strongSelf.justCreatedCallLeftSeparatorLayer?.removeFromSuperlayer()
strongSelf.justCreatedCallLeftSeparatorLayer = nil

View File

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

View File

@ -655,6 +655,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
if let invite = invite {
arguments.openLink(invite)
}
}, openCallAction: {
})
case let .editablePublicLink(theme, _, placeholder, currentText):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), clearType: .always, tag: ChannelVisibilityEntryTag.publicLink, sectionId: self.section, textUpdated: { updatedText in
@ -1608,7 +1609,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
} else {
isGroup = true
}
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
})
}
})

View File

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

View File

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

View File

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

View File

@ -248,6 +248,7 @@ public extension Api {
}
public extension Api {
indirect enum InputInvoice: TypeConstructorDescription {
case inputInvoiceBusinessBotTransferStars(bot: Api.InputUser, stars: Int64)
case inputInvoiceChatInviteSubscription(hash: String)
case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32)
case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption)
@ -260,6 +261,13 @@ public extension Api {
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .inputInvoiceBusinessBotTransferStars(let bot, let stars):
if boxed {
buffer.appendInt32(-191267262)
}
bot.serialize(buffer, true)
serializeInt64(stars, buffer: buffer, boxed: false)
break
case .inputInvoiceChatInviteSubscription(let hash):
if boxed {
buffer.appendInt32(887591921)
@ -329,6 +337,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .inputInvoiceBusinessBotTransferStars(let bot, let stars):
return ("inputInvoiceBusinessBotTransferStars", [("bot", bot as Any), ("stars", stars as Any)])
case .inputInvoiceChatInviteSubscription(let hash):
return ("inputInvoiceChatInviteSubscription", [("hash", hash as Any)])
case .inputInvoiceMessage(let peer, let msgId):
@ -350,6 +360,22 @@ public extension Api {
}
}
public static func parse_inputInvoiceBusinessBotTransferStars(_ reader: BufferReader) -> InputInvoice? {
var _1: Api.InputUser?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.InputUser
}
var _2: Int64?
_2 = reader.readInt64()
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.InputInvoice.inputInvoiceBusinessBotTransferStars(bot: _1!, stars: _2!)
}
else {
return nil
}
}
public static func parse_inputInvoiceChatInviteSubscription(_ reader: BufferReader) -> InputInvoice? {
var _1: String?
_1 = parseString(reader)

View File

@ -10136,12 +10136,13 @@ public extension Api.functions.phone {
}
}
public extension Api.functions.phone {
static func inviteConferenceCallParticipant(call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
static func inviteConferenceCallParticipant(flags: Int32, call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(1050474478)
buffer.appendInt32(-1124981115)
serializeInt32(flags, buffer: buffer, boxed: false)
call.serialize(buffer, true)
userId.serialize(buffer, true)
return (FunctionDescription(name: "phone.inviteConferenceCallParticipant", parameters: [("call", String(describing: call)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
return (FunctionDescription(name: "phone.inviteConferenceCallParticipant", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {

View File

@ -486,45 +486,86 @@ public final class CallController: ViewController {
var disablePeerIds: [EnginePeer.Id] = []
disablePeerIds.append(self.call.context.account.peerId)
disablePeerIds.append(self.call.peerId)
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, shareLink: nil, completion: { [weak self] peers in
guard let self else {
return
}
let _ = self.call.upgradeToConference(invitePeerIds: peerIds, completion: { _ in
let _ = self.call.upgradeToConference(invitePeers: peers, completion: { _ in
})
})
self.push(controller)
}
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([EnginePeer.Id]) -> Void) -> ViewController {
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], shareLink: (() -> Void)?, completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController {
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
var options: [ContactListAdditionalOption] = []
var openShareLinkImpl: (() -> Void)?
if shareLink != nil {
//TODO:localize
options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
openShareLinkImpl?()
}, clearHighlightAutomatically: false))
}
let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
context: context,
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
title: "Invite Members",
mode: .peerSelection(searchChatList: true, searchGroups: false, searchChannels: false),
mode: .generic,
title: { strings in
//TODO:localize
return "Add Member"
},
options: .single(options),
displayCallIcons: true,
confirmation: { peer in
switch peer {
case let .peer(peer, _, _):
let peer = EnginePeer(peer)
guard case let .user(user) = peer else {
return .single(false)
}
if disablePeerIds.contains(user.id) {
return .single(false)
}
if user.botInfo != nil {
return .single(false)
}
return .single(true)
default:
return .single(false)
}
},
isPeerEnabled: { peer in
guard case let .user(user) = peer else {
switch peer {
case let .peer(peer, _, _):
let peer = EnginePeer(peer)
guard case let .user(user) = peer else {
return false
}
if disablePeerIds.contains(user.id) {
return false
}
if user.botInfo != nil {
return false
}
return true
default:
return false
}
if disablePeerIds.contains(user.id) {
return false
}
if user.botInfo != nil {
return false
}
return true
}
))
openShareLinkImpl = { [weak controller] in
controller?.dismiss()
shareLink?()
}
controller.navigationPresentation = .modal
let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
guard case let .result(peerIds, _) = result else {
controller?.dismiss()
return
}
if peerIds.isEmpty {
guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else {
controller?.dismiss()
return
}
@ -533,15 +574,15 @@ public final class CallController: ViewController {
controller?.dismiss()
}
let invitePeerIds = peerIds.compactMap { item -> EnginePeer.Id? in
if case let .peer(peerId) = item {
return peerId
} else {
return nil
}
var isVideo = false
switch result.1 {
case .videoCall:
isVideo = true
default:
break
}
completion(invitePeerIds)
completion([(peer.id, isVideo)])
})
return controller

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,179 @@ import ComponentFlow
import MultilineTextComponent
import BalancedTextComponent
import TelegramPresentationData
import CallsEmoji
private final class EmojiItemComponent: Component {
let emoji: String?
init(emoji: String?) {
self.emoji = emoji
}
static func ==(lhs: EmojiItemComponent, rhs: EmojiItemComponent) -> Bool {
if lhs.emoji != rhs.emoji {
return false
}
return true
}
final class View: UIView {
private let measureEmojiView = ComponentView<Empty>()
private var pendingContainerView: UIView?
private var pendingEmojiViews: [ComponentView<Empty>] = []
private var emojiView: ComponentView<Empty>?
private var component: EmojiItemComponent?
private weak var state: EmptyComponentState?
private var pendingEmojiValues: [String]?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func update(component: EmojiItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = self.measureEmojiView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "👍", font: Font.regular(40.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
)
let borderEmoji = 2
let numEmoji = borderEmoji * 2 + 3
if let emoji = component.emoji {
let emojiView: ComponentView<Empty>
var emojiViewTransition = transition
if let current = self.emojiView {
emojiView = current
} else {
emojiViewTransition = .immediate
emojiView = ComponentView()
self.emojiView = emojiView
}
let emojiSize = emojiView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: emoji, font: Font.regular(40.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
)
let emojiFrame = CGRect(origin: CGPoint(x: floor((size.width - emojiSize.width) * 0.5), y: floor((size.height - emojiSize.height) * 0.5)), size: emojiSize)
if let emojiComponentView = emojiView.view {
if emojiComponentView.superview == nil {
self.addSubview(emojiComponentView)
}
emojiViewTransition.setFrame(view: emojiComponentView, frame: emojiFrame)
}
self.pendingEmojiValues = nil
} else {
if let emojiView = self.emojiView {
self.emojiView = nil
emojiView.view?.removeFromSuperview()
}
if self.pendingEmojiValues?.count != numEmoji {
var pendingEmojiValuesValue: [String] = []
for _ in 0 ..< numEmoji - borderEmoji - 1 {
pendingEmojiValuesValue.append(randomCallsEmoji() ?? "👍")
}
for i in 0 ..< borderEmoji + 1 {
pendingEmojiValuesValue.append(pendingEmojiValuesValue[i])
}
self.pendingEmojiValues = pendingEmojiValuesValue
}
}
if let pendingEmojiValues, pendingEmojiValues.count == numEmoji {
let pendingContainerView: UIView
if let current = self.pendingContainerView {
pendingContainerView = current
} else {
pendingContainerView = UIView()
self.pendingContainerView = pendingContainerView
}
for i in 0 ..< numEmoji {
let pendingEmojiView: ComponentView<Empty>
if self.pendingEmojiViews.count > i {
pendingEmojiView = self.pendingEmojiViews[i]
} else {
pendingEmojiView = ComponentView()
self.pendingEmojiViews.append(pendingEmojiView)
}
let pendingEmojiViewSize = pendingEmojiView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: pendingEmojiValues[i], font: Font.regular(40.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
)
if let pendingEmojiComponentView = pendingEmojiView.view {
if pendingEmojiComponentView.superview == nil {
pendingContainerView.addSubview(pendingEmojiComponentView)
}
pendingEmojiComponentView.frame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(i) * size.height), size: pendingEmojiViewSize)
}
}
pendingContainerView.frame = CGRect(origin: CGPoint(), size: size)
if pendingContainerView.superview == nil {
self.addSubview(pendingContainerView)
let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y")
//animation.duration = 4.2
animation.duration = 0.2
animation.fromValue = -CGFloat(numEmoji - borderEmoji) * size.height
animation.toValue = CGFloat(borderEmoji - 3) * size.height
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.autoreverses = false
animation.repeatCount = .infinity
pendingContainerView.layer.add(animation, forKey: "offsetCycle")
}
} else if let pendingContainerView = self.pendingContainerView {
self.pendingContainerView = nil
pendingContainerView.removeFromSuperview()
for emojiView in self.pendingEmojiViews {
emojiView.view?.removeFromSuperview()
}
self.pendingEmojiViews.removeAll()
}
//self.layer.borderColor = UIColor.red.cgColor
//self.layer.borderWidth = 4.0
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class VideoChatEncryptionKeyComponent: Component {
let theme: PresentationTheme
@ -119,7 +292,7 @@ final class VideoChatEncryptionKeyComponent: Component {
let expandedButtonTopInset: CGFloat = 12.0
let expandedButtonBottomInset: CGFloat = 13.0
let emojiItemSizes = (0 ..< component.emoji.count).map { i -> CGSize in
let emojiItemSizes = (0 ..< 4).map { i -> CGSize in
let emojiItem: ComponentView<Empty>
if self.emojiItems.count > i {
emojiItem = self.emojiItems[i]
@ -128,9 +301,9 @@ final class VideoChatEncryptionKeyComponent: Component {
self.emojiItems.append(emojiItem)
}
return emojiItem.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.emoji[i], font: Font.regular(40.0), textColor: .white))
transition: transition,
component: AnyComponent(EmojiItemComponent(
emoji: i < component.emoji.count ? component.emoji[i] : nil
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)

View File

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

View File

@ -39,23 +39,33 @@ final class VideoChatParticipantsComponent: Component {
}
final class Participants: Equatable {
enum InviteType {
case invite
enum InviteType: Equatable {
case invite(isMultipleUsers: Bool)
case shareLink
}
struct InviteOption: Equatable {
let id: Int
let type: InviteType
init(id: Int, type: InviteType) {
self.id = id
self.type = type
}
}
let myPeerId: EnginePeer.Id
let participants: [GroupCallParticipantsContext.Participant]
let totalCount: Int
let loadMoreToken: String?
let inviteType: InviteType?
let inviteOptions: [InviteOption]
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteType: InviteType?) {
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteOptions: [InviteOption]) {
self.myPeerId = myPeerId
self.participants = participants
self.totalCount = totalCount
self.loadMoreToken = loadMoreToken
self.inviteType = inviteType
self.inviteOptions = inviteOptions
}
static func ==(lhs: Participants, rhs: Participants) -> Bool {
@ -74,7 +84,7 @@ final class VideoChatParticipantsComponent: Component {
if lhs.loadMoreToken != rhs.loadMoreToken {
return false
}
if lhs.inviteType != rhs.inviteType {
if lhs.inviteOptions != rhs.inviteOptions {
return false
}
return true
@ -142,7 +152,7 @@ final class VideoChatParticipantsComponent: Component {
let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void
let updateIsMainParticipantPinned: (Bool) -> Void
let updateIsExpandedUIHidden: (Bool) -> Void
let openInviteMembers: () -> Void
let openInviteMembers: (Participants.InviteType) -> Void
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
init(
@ -162,7 +172,7 @@ final class VideoChatParticipantsComponent: Component {
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
openInviteMembers: @escaping () -> Void,
openInviteMembers: @escaping (Participants.InviteType) -> Void,
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
) {
self.call = call
@ -379,14 +389,14 @@ final class VideoChatParticipantsComponent: Component {
let sideInset: CGFloat
let itemCount: Int
let itemHeight: CGFloat
let trailingItemHeight: CGFloat
let trailingItemHeights: [CGFloat]
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeight: CGFloat) {
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeights: [CGFloat]) {
self.containerSize = containerSize
self.sideInset = sideInset
self.itemCount = itemCount
self.itemHeight = itemHeight
self.trailingItemHeight = trailingItemHeight
self.trailingItemHeights = trailingItemHeights
}
func frame(at index: Int) -> CGRect {
@ -394,8 +404,15 @@ final class VideoChatParticipantsComponent: Component {
return frame
}
func trailingItemFrame() -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeight))
func trailingItemFrame(index: Int) -> CGRect {
if index < 0 || index >= self.trailingItemHeights.count {
return CGRect()
}
var prefixHeight: CGFloat = 0.0
for i in 0 ..< index {
prefixHeight += self.trailingItemHeights[i]
}
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight + prefixHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeights[index]))
}
func contentHeight() -> CGFloat {
@ -403,7 +420,9 @@ final class VideoChatParticipantsComponent: Component {
if self.itemCount != 0 {
result = self.frame(at: self.itemCount - 1).maxY
}
result += self.trailingItemHeight
for height in self.trailingItemHeights {
result += height
}
return result
}
@ -439,7 +458,7 @@ final class VideoChatParticipantsComponent: Component {
let scrollClippingFrame: CGRect
let separateVideoScrollClippingFrame: CGRect
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeights: [CGFloat]) {
self.containerSize = containerSize
self.layout = layout
self.isUIHidden = isUIHidden
@ -465,7 +484,7 @@ final class VideoChatParticipantsComponent: Component {
}
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil)
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeights: listTrailingItemHeights)
self.spacing = 4.0
if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden {
@ -568,8 +587,8 @@ final class VideoChatParticipantsComponent: Component {
}
}
func listTrailingItemFrame() -> CGRect {
return self.list.trailingItemFrame()
func listTrailingItemFrame(index: Int) -> CGRect {
return self.list.trailingItemFrame(index: index)
}
}
@ -641,7 +660,7 @@ final class VideoChatParticipantsComponent: Component {
private var listParticipants: [GroupCallParticipantsContext.Participant] = []
private let measureListItemView = ComponentView<Empty>()
private let inviteListItemView = ComponentView<Empty>()
private var inviteListItemViews: [Int: ComponentView<Empty>] = [:]
private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
private let gridItemViewContainer: UIView
@ -1270,7 +1289,7 @@ final class VideoChatParticipantsComponent: Component {
case .requesting:
subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral)
case .ringing:
subtitle = PeerListItemComponent.Subtitle(text: "ringing...", color: .neutral)
subtitle = PeerListItemComponent.Subtitle(text: "invited", color: .neutral)
}
peerItemComponent = PeerListItemComponent(
@ -1381,11 +1400,15 @@ final class VideoChatParticipantsComponent: Component {
self.listItemViews.removeValue(forKey: itemId)
}
do {
var trailingItemIndex = 0
for inviteOption in component.participants?.inviteOptions ?? [] {
guard let itemView = self.inviteListItemViews[inviteOption.id] else {
continue
}
var itemTransition = transition
let itemView = self.inviteListItemView
let itemFrame = itemLayout.listTrailingItemFrame()
let itemFrame = itemLayout.listTrailingItemFrame(index: trailingItemIndex)
trailingItemIndex += 1
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
@ -1395,6 +1418,17 @@ final class VideoChatParticipantsComponent: Component {
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
var removeInviteListItemIds: [Int] = []
for (id, itemView) in self.inviteListItemViews {
if let participants = component.participants, participants.inviteOptions.contains(where: { $0.id == id }) {
} else {
removeInviteListItemIds.append(id)
itemView.view?.removeFromSuperview()
}
}
for id in removeInviteListItemIds {
self.inviteListItemViews.removeValue(forKey: id)
}
transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0)
transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY))
@ -1748,32 +1782,51 @@ final class VideoChatParticipantsComponent: Component {
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let inviteText: String
if let participants = component.participants, let inviteType = participants.inviteType {
switch inviteType {
case .invite:
inviteText = component.strings.VoiceChat_InviteMember
var inviteListItemSizes: [CGSize] = []
for (inviteOption) in component.participants?.inviteOptions ?? [] {
let inviteText: String
let iconType: VideoChatListInviteComponent.Icon
switch inviteOption.type {
case let .invite(isMultiple):
//TODO:localize
if isMultiple {
inviteText = component.strings.VoiceChat_InviteMember
} else {
inviteText = "Add Member"
}
iconType = .addUser
case .shareLink:
inviteText = component.strings.VoiceChat_Share
iconType = .link
}
} else {
inviteText = component.strings.VoiceChat_InviteMember
}
let inviteListItemSize = self.inviteListItemView.update(
transition: transition,
component: AnyComponent(VideoChatListInviteComponent(
title: inviteText,
theme: component.theme,
action: { [weak self] in
guard let self, let component = self.component else {
return
let inviteListItemView: ComponentView<Empty>
var inviteListItemTransition = transition
if let current = self.inviteListItemViews[inviteOption.id] {
inviteListItemView = current
} else {
inviteListItemView = ComponentView()
self.inviteListItemViews[inviteOption.id] = inviteListItemView
inviteListItemTransition = inviteListItemTransition.withAnimation(.none)
}
inviteListItemSizes.append(inviteListItemView.update(
transition: inviteListItemTransition,
component: AnyComponent(VideoChatListInviteComponent(
title: inviteText,
icon: iconType,
theme: component.theme,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.openInviteMembers(inviteOption.type)
}
component.openInviteMembers()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
))
}
var gridParticipants: [VideoParticipant] = []
var listParticipants: [GroupCallParticipantsContext.Participant] = []
@ -1824,7 +1877,7 @@ final class VideoChatParticipantsComponent: Component {
gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count + component.invitedPeers.count,
listItemHeight: measureListItemSize.height,
listTrailingItemHeight: inviteListItemSize.height
listTrailingItemHeights: inviteListItemSizes.map(\.height)
)
self.itemLayout = itemLayout

View File

@ -25,6 +25,7 @@ import LegacyComponents
import TooltipUI
import BlurredBackgroundComponent
import CallsEmoji
import InviteLinksUI
extension VideoChatCall {
var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> {
@ -652,6 +653,51 @@ final class VideoChatScreenComponent: Component {
guard case let .group(groupCall) = self.currentCall else {
return
}
if groupCall.isConference {
guard let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
return
}
guard let currentReference = groupCall.currentReference, case let .id(callId, accessHash) = currentReference else {
return
}
guard let callState = self.callState else {
return
}
var presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
let controller = InviteLinkInviteController(
context: groupCall.accountContext,
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(
callId: callId,
accessHash: accessHash,
isRecentlyCreated: false,
canRevoke: callState.canManageCall
)),
initialInvite: .link(link: inviteLinks.listenerLink, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: groupCall.accountContext.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil),
parentNavigationController: navigationController,
completed: { [weak self] result in
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
if let result {
switch result {
case .linkCopied:
//TODO:localize
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
return false
}), in: .current)
case .openCall:
break
}
}
}
)
self.environment?.controller()?.present(controller, in: .window(.root), with: nil)
return
}
let formatSendTitle: (String) -> String = { string in
var string = string
@ -705,7 +751,7 @@ final class VideoChatScreenComponent: Component {
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
)
)
|> deliverOnMainQueue).start(next: { [weak self] peerList in
|> deliverOnMainQueue).start(next: { [weak self] peerList in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
@ -1051,7 +1097,7 @@ final class VideoChatScreenComponent: Component {
static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [InvitedPeer]), NoError> {
let invitedPeers = conferenceSource.context.engine.data.subscribe(
EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0) })
EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0.id) })
)
let accountPeerId = conferenceSource.context.account.peerId
@ -1759,12 +1805,19 @@ final class VideoChatScreenComponent: Component {
}
}
}
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
if canInvite {
if inviteIsLink {
inviteType = .shareLink
} else {
inviteType = .invite
var inviteOptions: [VideoChatParticipantsComponent.Participants.InviteOption] = []
if case let .group(groupCall) = self.currentCall, groupCall.isConference {
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: .invite(isMultipleUsers: false)))
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 1, type: .shareLink))
} else {
if canInvite {
let inviteType: VideoChatParticipantsComponent.Participants.InviteType
if inviteIsLink {
inviteType = .shareLink
} else {
inviteType = .invite(isMultipleUsers: false)
}
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: inviteType))
}
}
@ -1773,7 +1826,7 @@ final class VideoChatScreenComponent: Component {
participants: members.participants,
totalCount: members.totalCount,
loadMoreToken: members.loadMoreToken,
inviteType: inviteType
inviteOptions: inviteOptions
)
}
@ -2038,7 +2091,13 @@ final class VideoChatScreenComponent: Component {
}
var encryptionKeyFrame: CGRect?
if let encryptionKeyEmoji = self.encryptionKeyEmoji {
var isConference = false
if case let .group(groupCall) = self.currentCall {
isConference = groupCall.isConference
} else if case .conferenceSource = self.currentCall {
isConference = true
}
if isConference {
navigationHeight -= 2.0
let encryptionKey: ComponentView<Empty>
var encryptionKeyTransition = transition
@ -2055,7 +2114,7 @@ final class VideoChatScreenComponent: Component {
component: AnyComponent(VideoChatEncryptionKeyComponent(
theme: environment.theme,
strings: environment.strings,
emoji: encryptionKeyEmoji,
emoji: self.encryptionKeyEmoji ?? [],
isExpanded: self.isEncryptionKeyExpanded,
tapAction: { [weak self] in
guard let self else {
@ -2326,11 +2385,18 @@ final class VideoChatScreenComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4))
}
},
openInviteMembers: { [weak self] in
openInviteMembers: { [weak self] type in
guard let self else {
return
}
self.openInviteMembers()
if case .shareLink = type {
guard let inviteLinks = self.inviteLinks else {
return
}
self.presentShare(inviteLinks)
} else {
self.openInviteMembers()
}
},
visibleParticipantsUpdated: { [weak self] visibleParticipants in
guard let self else {

View File

@ -16,31 +16,6 @@ extension VideoChatScreenComponent.View {
return
}
/*if groupCall.accountContext.sharedContext.immediateExperimentalUISettings.conferenceDebug {
guard let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
return
}
var presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
let controller = InviteLinkInviteController(context: groupCall.accountContext, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), mode: .groupCall(link: "https://t.me/call/+abbfbffll123", isRecentlyCreated: false), parentNavigationController: navigationController, completed: { [weak self] result in
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
if let result {
switch result {
case .linkCopied:
//TODO:localize
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
return false
}), in: .current)
}
}
})
self.environment?.controller()?.present(controller, in: .window(.root), with: nil)
return
}*/
if groupCall.isConference {
var disablePeerIds: [EnginePeer.Id] = []
disablePeerIds.append(groupCall.accountContext.account.peerId)
@ -51,13 +26,21 @@ extension VideoChatScreenComponent.View {
}
}
}
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, shareLink: { [weak self] in
guard let self else {
return
}
guard let inviteLinks = self.inviteLinks else {
return
}
self.presentShare(inviteLinks)
}, completion: { [weak self] peerIds in
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
for peerId in peerIds {
let _ = groupCall.invitePeer(peerId)
let _ = groupCall.invitePeer(peerId.id, isVideo: peerId.isVideo)
}
})
self.environment?.controller()?.push(controller)
@ -80,7 +63,7 @@ extension VideoChatScreenComponent.View {
if inviteIsLink {
inviteType = .shareLink
} else {
inviteType = .invite
inviteType = .invite(isMultipleUsers: true)
}
}
@ -146,7 +129,8 @@ extension VideoChatScreenComponent.View {
if let participant {
dismissController?()
if groupCall.invitePeer(participant.peer.id) {
//TODO:release
if groupCall.invitePeer(participant.peer.id, isVideo: false) {
let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
@ -258,7 +242,8 @@ extension VideoChatScreenComponent.View {
}
dismissController?()
if groupCall.invitePeer(peer.id) {
//TODO:release
if groupCall.invitePeer(peer.id, isVideo: false) {
let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
@ -330,7 +315,8 @@ extension VideoChatScreenComponent.View {
}
dismissController?()
if groupCall.invitePeer(peer.id) {
//TODO:release
if groupCall.invitePeer(peer.id, isVideo: false) {
let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string

View File

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

View File

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

View File

@ -201,12 +201,28 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))
case let .messageActionPaidMessagesPrice(stars):
return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars))
case let .messageActionConferenceCall(_, callId, duration, otherParticipants):
return TelegramMediaAction(action: .conferenceCall(
case let .messageActionConferenceCall(flags, callId, duration, otherParticipants):
let isMissed = (flags & (1 << 0)) != 0
let isActive = (flags & (1 << 1)) != 0
let isVideo = (flags & (1 << 4)) != 0
var mappedFlags = TelegramMediaActionType.ConferenceCall.Flags()
if isMissed {
mappedFlags.insert(.isMissed)
}
if isActive {
mappedFlags.insert(.isActive)
}
if isVideo {
mappedFlags.insert(.isVideo)
}
return TelegramMediaAction(action: .conferenceCall(TelegramMediaActionType.ConferenceCall(
callId: callId,
duration: duration,
flags: mappedFlags,
otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? []
))
)))
}
}

View File

@ -382,7 +382,7 @@ private final class CallSessionContext {
private final class IncomingConferenceInvitationContext {
enum State: Equatable {
case pending
case ringing(callId: Int64, otherParticipants: [EnginePeer])
case ringing(callId: Int64, isVideo: Bool, otherParticipants: [EnginePeer])
case stopped
}
@ -420,11 +420,11 @@ private final class IncomingConferenceInvitationContext {
}
}
if let action = foundAction, case let .conferenceCall(callId, duration, otherParticipants) = action.action {
if duration != nil {
if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action {
if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil {
state = .stopped
} else {
state = .ringing(callId: callId, otherParticipants: otherParticipants.compactMap { id -> EnginePeer? in
state = .ringing(callId: conferenceCall.callId, isVideo: conferenceCall.flags.contains(.isVideo), otherParticipants: conferenceCall.otherParticipants.compactMap { id -> EnginePeer? in
return message.peers[id].flatMap(EnginePeer.init)
})
}
@ -639,11 +639,11 @@ private final class CallSessionManagerContext {
}
}
for (id, context) in self.incomingConferenceInvitationContexts {
if case let .ringing(_, otherParticipants) = context.state {
if case let .ringing(_, isVideo, otherParticipants) = context.state {
ringingContexts.append(CallSessionRingingState(
id: context.internalId,
peerId: id.peerId,
isVideo: false,
isVideo: isVideo,
isVideoPossible: true,
conferenceSource: id,
otherParticipants: otherParticipants
@ -718,6 +718,23 @@ private final class CallSessionManagerContext {
}
}
func dropOutgoingConferenceRequest(messageId: MessageId) {
let addUpdates = self.addUpdates
let rejectSignal = self.network.request(Api.functions.phone.declineConferenceCallInvite(msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates {
addUpdates(updates)
}
return .complete()
}
self.rejectConferenceInvitationDisposables.add(rejectSignal.startStrict())
}
func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal<String?, NoError>) {
for (id, context) in self.incomingConferenceInvitationContexts {
if context.internalId == internalId {
@ -1383,6 +1400,12 @@ public final class CallSessionManager {
}
}
public func dropOutgoingConferenceRequest(messageId: MessageId) {
self.withContext { context in
context.dropOutgoingConferenceRequest(messageId: messageId)
}
}
func drop(stableId: CallSessionStableId, reason: DropCallReason) {
self.withContext { context in
context.drop(stableId: stableId, reason: reason)

View File

@ -86,6 +86,31 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
}
}
public struct ConferenceCall: Equatable {
public struct Flags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let isVideo = Flags(rawValue: 1 << 0)
public static let isActive = Flags(rawValue: 1 << 1)
public static let isMissed = Flags(rawValue: 1 << 2)
}
public let callId: Int64
public let duration: Int32?
public let flags: Flags
public let otherParticipants: [PeerId]
public init(callId: Int64, duration: Int32?, flags: Flags, otherParticipants: [PeerId]) {
self.callId = callId
self.duration = duration
self.flags = flags
self.otherParticipants = otherParticipants
}
}
case unknown
case groupCreated(title: String)
case addedMembers(peerIds: [PeerId])
@ -134,7 +159,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?)
case paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64)
case conferenceCall(callId: Int64, duration: Int32?, otherParticipants: [PeerId])
case conferenceCall(ConferenceCall)
public init(decoder: PostboxDecoder) {
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
@ -264,7 +289,12 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 47:
self = .paidMessagesPriceEdited(stars: decoder.decodeInt64ForKey("stars", orElse: 0))
case 48:
self = .conferenceCall(callId: decoder.decodeInt64ForKey("cid", orElse: 0), duration: decoder.decodeOptionalInt32ForKey("dur"), otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init))
self = .conferenceCall(ConferenceCall(
callId: decoder.decodeInt64ForKey("cid", orElse: 0),
duration: decoder.decodeOptionalInt32ForKey("dur"),
flags: ConferenceCall.Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)),
otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init)
))
default:
self = .unknown
}
@ -642,15 +672,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case let .paidMessagesPriceEdited(stars):
encoder.encodeInt32(47, forKey: "_rawValue")
encoder.encodeInt64(stars, forKey: "stars")
case let .conferenceCall(callId, duration, otherParticipants):
case let .conferenceCall(conferenceCall):
encoder.encodeInt32(48, forKey: "_rawValue")
encoder.encodeInt64(callId, forKey: "cid")
if let duration {
encoder.encodeInt64(conferenceCall.callId, forKey: "cid")
if let duration = conferenceCall.duration {
encoder.encodeInt32(duration, forKey: "dur")
} else {
encoder.encodeNil(forKey: "dur")
}
encoder.encodeInt64Array(otherParticipants.map({ $0.toInt64() }), forKey: "part")
encoder.encodeInt32(conferenceCall.flags.rawValue, forKey: "flags")
encoder.encodeInt64Array(conferenceCall.otherParticipants.map({ $0.toInt64() }), forKey: "part")
}
}

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import AppBundle
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageDateAndStatusNode
import SwiftSignalKit
private let titleFont: UIFont = Font.medium(16.0)
private let labelFont: UIFont = Font.regular(13.0)
@ -25,6 +26,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
private let iconNode: ASImageNode
private let buttonNode: HighlightableButtonNode
private var activeConferenceUpdateTimer: SwiftSignalKit.Timer?
required public init() {
self.titleNode = TextNode()
self.labelNode = TextNode()
@ -57,6 +60,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside)
}
deinit {
self.activeConferenceUpdateTimer?.invalidate()
}
override public func accessibilityActivate() -> Bool {
self.callButtonPressed()
return true
@ -90,6 +97,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
var callDuration: Int32?
var callSuccessful = true
var isVideo = false
var hasCallButton = true
var updateConferenceTimerEndTimeout: Int32?
for media in item.message.media {
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
isVideo = isVideoValue
@ -123,11 +132,32 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
}
}
break
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(_, duration, _) = action.action {
isVideo = false
callDuration = duration
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
isVideo = conferenceCall.flags.contains(.isVideo)
callDuration = conferenceCall.duration
//TODO:localize
titleString = "Group Call"
let missedTimeout: Int32
#if DEBUG
missedTimeout = 5
#else
missedTimeout = 30
#endif
let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) {
titleString = "Declined Group Call"
} else if item.message.timestamp < currentTime - missedTimeout {
titleString = "Missed Group Call"
} else if conferenceCall.duration != nil {
titleString = "Cancelled Group Call"
hasCallButton = true
} else {
if incoming {
titleString = "Incoming Group Call"
} else {
titleString = "Outgoing Group Call"
}
updateConferenceTimerEndTimeout = (item.message.timestamp + missedTimeout) - currentTime
}
break
}
}
@ -211,7 +241,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
boundingSize.width += 54.0
if hasCallButton {
boundingSize.width += 54.0
}
return (boundingSize.width, { boundingWidth in
return (boundingSize, { [weak self] animation, _, _ in
@ -234,6 +266,22 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
if let buttonImage = buttonImage {
strongSelf.buttonNode.setImage(buttonImage, for: [])
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size)
strongSelf.buttonNode.isHidden = !hasCallButton
}
if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer {
activeConferenceUpdateTimer.invalidate()
strongSelf.activeConferenceUpdateTimer = nil
}
if let updateConferenceTimerEndTimeout, updateConferenceTimerEndTimeout >= 0 {
strongSelf.activeConferenceUpdateTimer?.invalidate()
strongSelf.activeConferenceUpdateTimer = SwiftSignalKit.Timer(timeout: Double(updateConferenceTimerEndTimeout) + 0.5, repeat: false, completion: { [weak strongSelf] in
guard let strongSelf else {
return
}
strongSelf.requestInlineUpdate?()
}, queue: .mainQueue())
strongSelf.activeConferenceUpdateTimer?.start()
}
}
})
@ -270,6 +318,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.isHidden {
return ChatMessageBubbleContentTapAction(content: .none)
}
if self.buttonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
} else if self.bounds.contains(point), let item = self.item {

View File

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

View File

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

View File

@ -136,6 +136,20 @@ public final class TextNodeWithEntities {
}
}
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
private var linkHighlightingNode: LinkHighlightingNode?
public var linkHighlightColor: UIColor?
public var linkHighlightInset: UIEdgeInsets = .zero
public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? {
didSet {
self.updateInteractiveActions()
}
}
public init() {
self.textNode = TextNode()
}
@ -301,6 +315,83 @@ public final class TextNodeWithEntities {
self.inlineStickerItemLayers.removeValue(forKey: key)
}
}
private func updateInteractiveActions() {
if self.highlightAttributeAction != nil {
if self.tapRecognizer == nil {
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
tapRecognizer.highlight = { [weak self] point in
if let strongSelf = self, let cachedLayout = strongSelf.textNode.cachedLayout {
var rects: [CGRect]?
if let point = point {
if let (index, attributes) = strongSelf.textNode.attributesAtPoint(CGPoint(x: point.x, y: point.y)) {
if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) {
let initialRects = strongSelf.textNode.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index)
if let initialRects = initialRects, case .center = cachedLayout.resolvedAlignment {
var mappedRects: [CGRect] = []
for i in 0 ..< initialRects.count {
let lineRect = initialRects[i].0
var itemRect = initialRects[i].1
itemRect.origin.x = floor((strongSelf.textNode.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x
mappedRects.append(itemRect)
}
rects = mappedRects
} else {
rects = strongSelf.textNode.attributeRects(name: selectedAttribute.rawValue, at: index)
}
}
}
}
if var rects, !rects.isEmpty {
let linkHighlightingNode: LinkHighlightingNode
if let current = strongSelf.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear)
strongSelf.linkHighlightingNode = linkHighlightingNode
strongSelf.textNode.addSubnode(linkHighlightingNode)
}
linkHighlightingNode.frame = strongSelf.textNode.bounds
rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset)
linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) })
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
strongSelf.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
self.textNode.view.addGestureRecognizer(tapRecognizer)
}
} else if let tapRecognizer = self.tapRecognizer {
self.tapRecognizer = nil
self.textNode.view.removeGestureRecognizer(tapRecognizer)
}
}
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.tapAttributeAction?(attributes, index)
}
case .longTap:
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.longTapAttributeAction?(attributes, index)
}
default:
break
}
}
default:
break
}
}
}
public class ImmediateTextNodeWithEntities: TextNode {

View File

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

View File

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

View File

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

View File

@ -44,6 +44,8 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var cancelSearch: (() -> Void)?
var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)?
let isPeerEnabled: (ContactListPeer) -> Bool
var presentationData: PresentationData {
didSet {
self.presentationDataPromise.set(.single(self.presentationData))
@ -57,12 +59,13 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var searchContainerNode: ContactsSearchContainerNode?
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool) {
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool, isPeerEnabled: @escaping (ContactListPeer) -> Bool) {
self.context = context
self.presentationData = presentationData
self.displayDeviceContacts = displayDeviceContacts
self.displayCallIcons = displayCallIcons
self.allowChannelsInSearch = allowChannelsInSearch
self.isPeerEnabled = isPeerEnabled
var excludeSelf = true
@ -124,7 +127,9 @@ final class ContactSelectionControllerNode: ASDisplayNode {
}
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, isPeerEnabled: { peer in
return isPeerEnabled(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil))
}, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
contextActionImpl?(peer, node, gesture, nil)
} : nil, multipleSelection: multipleSelection)

View File

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

View File

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

View File

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

View File

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

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