[WIP] Conference

This commit is contained in:
Isaac 2025-03-19 18:55:24 +01:00
parent 40b19cfef2
commit 17dd059f13
33 changed files with 2034 additions and 283 deletions

@ -168,6 +168,7 @@ private final class EmbeddedBroadcastUploadImpl: BroadcastUploadImpl {
onMutedSpeechActivityDetected: { _ in },
encryptionKey: nil,
isConference: false,
audioIsActiveByDefault: true,
isStream: false,
sharedAudioDevice: nil
)

@ -301,6 +301,7 @@ public enum ResolvedUrl {
case instantView(TelegramMediaWebpage, String?)
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?)
case join(String)
case joinCall(String)
case localization(String)
case confirmationCode(Int)
case cancelAccountReset(phone: String, hash: String)
@ -920,6 +921,61 @@ public enum JoinAffiliateProgramScreenMode {
case active(Active)
}
public enum JoinSubjectScreenMode {
public final class Group {
public enum VerificationStatus {
case fake
case scam
case verified
}
public let link: String
public let isGroup: Bool
public let isPublic: Bool
public let isRequest: Bool
public let verificationStatus: VerificationStatus?
public let image: TelegramMediaImageRepresentation?
public let title: String
public let about: String?
public let memberCount: Int32
public let members: [EnginePeer]
public init(link: String, isGroup: Bool, isPublic: Bool, isRequest: Bool, verificationStatus: VerificationStatus?, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, members: [EnginePeer]) {
self.link = link
self.isGroup = isGroup
self.isPublic = isPublic
self.isRequest = isRequest
self.verificationStatus = verificationStatus
self.image = image
self.title = title
self.about = about
self.memberCount = memberCount
self.members = members
}
}
public final class GroupCall {
public let inviter: EnginePeer?
public let members: [EnginePeer]
public let totalMemberCount: Int
public init(inviter: EnginePeer?, members: [EnginePeer], totalMemberCount: Int) {
self.inviter = inviter
self.members = members
self.totalMemberCount = totalMemberCount
}
}
case group(Group)
case groupCall(GroupCall)
}
public enum OldChannelsControllerIntent {
case join
case create
case upgrade
}
public protocol SharedAccountContext: AnyObject {
var sharedContainerPath: String { get }
var basePath: String { get }
@ -1126,9 +1182,12 @@ public protocol SharedAccountContext: AnyObject {
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError>
func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController
func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController
func makeJoinSubjectScreen(context: AccountContext, mode: JoinSubjectScreenMode) -> ViewController
func makeOldChannelsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void) -> ViewController
func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController
func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController

@ -30,6 +30,9 @@ swift_library(
"//submodules/TelegramBaseController:TelegramBaseController",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ItemListPeerActionItem",
"//submodules/InviteLinksUI",
"//submodules/UndoUI",
],
visibility = [
"//visibility:public",

@ -13,6 +13,8 @@ import AppBundle
import LocalizedPeerData
import ContextUI
import TelegramBaseController
import InviteLinksUI
import UndoUI
public enum CallListControllerMode {
case tab
@ -201,7 +203,28 @@ public final class CallListController: TelegramBaseController {
if self.isNodeLoaded {
self.controllerNode.updateThemeAndStrings(presentationData: self.presentationData)
}
}
private func createGroupCall() {
let controller = InviteLinkInviteController(context: self.context, updatedPresentationData: nil, mode: .groupCall(link: "https://t.me/call/+abbfbffll123", 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 {
//TODO:release
}
return false
}), in: .window(.root))
}
}
})
self.present(controller, in: .window(.root), with: nil)
}
override public func loadDisplayNode() {
@ -275,6 +298,10 @@ public final class CallListController: TelegramBaseController {
}
}
}
}, createGroupCall: { [weak self] in
if let strongSelf = self {
strongSelf.createGroupCall()
}
})
if case .navigation = self.mode {

@ -14,6 +14,7 @@ import ChatListSearchItemHeader
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AppBundle
import ItemListPeerActionItem
private struct CallListNodeListViewTransition {
let callListView: CallListNodeView
@ -66,14 +67,16 @@ final class CallListNodeInteraction {
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) {
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) {
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
self.call = call
self.openInfo = openInfo
self.delete = delete
self.updateShowCallsTab = updateShowCallsTab
self.openGroupCall = openGroupCall
self.createGroupCall = createGroupCall
}
}
@ -122,6 +125,12 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item
}), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .createGroupCall:
//TODO:localize
let item = ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "New Call Link", hasSeparator: false, sectionId: 1, noInsets: true, editing: false, action: {
nodeInteraction.createGroupCall()
})
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
case let .groupCall(peer, _, isActive):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader, _):
@ -141,6 +150,12 @@ private func mappedUpdateEntries(context: AccountContext, presentationData: Item
}), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .createGroupCall:
//TODO:localize
let item = ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "New Call Link", sectionId: 1, noInsets: true, editing: false, action: {
nodeInteraction.createGroupCall()
})
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
case let .groupCall(peer, _, isActive):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader, _):
@ -209,9 +224,9 @@ final class CallListControllerNode: ASDisplayNode {
private let call: (EnginePeer.Id, Bool) -> Void
private let joinGroupCall: (EnginePeer.Id, EngineGroupCallDescription) -> Void
private let createGroupCall: () -> Void
private let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
private let emptyStateUpdated: (Bool) -> Void
private let emptyStatePromise = Promise<Bool>()
private let emptyStateDisposable = MetaDisposable()
@ -219,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) {
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) {
self.controller = controller
self.context = context
self.mode = mode
@ -228,7 +243,7 @@ final class CallListControllerNode: ASDisplayNode {
self.joinGroupCall = joinGroupCall
self.openInfo = openInfo
self.emptyStateUpdated = emptyStateUpdated
self.createGroupCall = createGroupCall
self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true, editing: false, messageIdWithRevealedOptions: nil)
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
@ -432,6 +447,11 @@ final class CallListControllerNode: ASDisplayNode {
strongSelf.joinGroupCall(peerId, activeCall)
}
}))
}, createGroupCall: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.createGroupCall()
})
let viewProcessingQueue = self.viewProcessingQueue
@ -496,18 +516,31 @@ final class CallListControllerNode: ASDisplayNode {
})
}
|> distinctUntilChanged
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
}
if let data = configuration.data, let value = data["ios_enable_conference"] as? Double {
isConferencePossible = value != 0.0
}
return isConferencePossible
}
let callListNodeViewTransition = combineLatest(
callListViewUpdate,
self.statePromise.get(),
groupCalls,
showCallsTab,
currentGroupCallPeerId
currentGroupCallPeerId,
canCreateGroupCall
)
|> mapToQueue { (updateAndType, state, groupCalls, showCallsTab, currentGroupCallPeerId) -> Signal<CallListNodeListViewTransition, NoError> in
|> mapToQueue { (updateAndType, state, groupCalls, showCallsTab, currentGroupCallPeerId, canCreateGroupCall) -> Signal<CallListNodeListViewTransition, NoError> in
let (update, type) = updateAndType
let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(view: update.view, groupCalls: groupCalls, state: state, showSettings: showSettings, showCallsTab: showCallsTab, isRecentCalls: type == .all, currentGroupCallPeerId: currentGroupCallPeerId), presentationData: state.presentationData)
let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(view: update.view, canCreateGroupCall: canCreateGroupCall, groupCalls: groupCalls, state: state, showSettings: showSettings, showCallsTab: showCallsTab, isRecentCalls: type == .all, currentGroupCallPeerId: currentGroupCallPeerId), presentationData: state.presentationData)
let previous = previousView.swap(processedView)
let previousType = previousType.swap(type)

@ -25,6 +25,7 @@ enum CallListNodeEntry: Comparable, Identifiable {
enum SortIndex: Comparable {
case displayTab
case displayTabInfo
case createGroupCall
case groupCall(EnginePeer.Id, String)
case message(EngineMessage.Index)
case hole(EngineMessage.Index)
@ -40,10 +41,17 @@ enum CallListNodeEntry: Comparable, Identifiable {
default:
return false
}
case let .groupCall(lhsPeerId, lhsTitle):
case .createGroupCall:
switch rhs {
case .displayTab, .displayTabInfo:
return false
default:
return true
}
case let .groupCall(lhsPeerId, lhsTitle):
switch rhs {
case .displayTab, .displayTabInfo, .createGroupCall:
return false
case let .groupCall(rhsPeerId, rhsTitle):
if lhsTitle == rhsTitle {
return lhsPeerId < rhsPeerId
@ -55,7 +63,7 @@ enum CallListNodeEntry: Comparable, Identifiable {
}
case let .hole(lhsIndex):
switch rhs {
case .displayTab, .displayTabInfo, .groupCall:
case .displayTab, .displayTabInfo, .groupCall, .createGroupCall:
return false
case let .hole(rhsIndex):
return lhsIndex < rhsIndex
@ -64,7 +72,7 @@ enum CallListNodeEntry: Comparable, Identifiable {
}
case let .message(lhsIndex):
switch rhs {
case .displayTab, .displayTabInfo, .groupCall:
case .displayTab, .displayTabInfo, .groupCall, .createGroupCall:
return false
case let .hole(rhsIndex):
return lhsIndex < rhsIndex
@ -78,6 +86,7 @@ enum CallListNodeEntry: Comparable, Identifiable {
case displayTab(PresentationTheme, String, Bool)
case displayTabInfo(PresentationTheme, String)
case createGroupCall
case groupCall(peer: EnginePeer, editing: Bool, isActive: Bool)
case messageEntry(topMessage: EngineMessage, messages: [EngineMessage], theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, editing: Bool, hasActiveRevealControls: Bool, displayHeader: Bool, missed: Bool)
case holeEntry(index: EngineMessage.Index, theme: PresentationTheme)
@ -88,6 +97,8 @@ enum CallListNodeEntry: Comparable, Identifiable {
return .displayTab
case .displayTabInfo:
return .displayTabInfo
case .createGroupCall:
return .createGroupCall
case let .groupCall(peer, _, _):
return .groupCall(peer.id, peer.compactDisplayTitle)
case let .messageEntry(message, _, _, _, _, _, _, _, _):
@ -103,6 +114,8 @@ enum CallListNodeEntry: Comparable, Identifiable {
return .setting(0)
case .displayTabInfo:
return .setting(1)
case .createGroupCall:
return .setting(2)
case let .groupCall(peer, _, _):
return .groupCall(peer.id)
case let .messageEntry(message, _, _, _, _, _, _, _, _):
@ -118,82 +131,88 @@ enum CallListNodeEntry: Comparable, Identifiable {
static func ==(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool {
switch lhs {
case let .displayTab(lhsTheme, lhsText, lhsValue):
if case let .displayTab(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
case let .displayTab(lhsTheme, lhsText, lhsValue):
if case let .displayTab(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .displayTabInfo(lhsTheme, lhsText):
if case let .displayTabInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case .createGroupCall:
if case .createGroupCall = rhs {
return true
} else {
return false
}
case let .groupCall(lhsPeer, lhsEditing, lhsIsActive):
if case let .groupCall(rhsPeer, rhsEditing, rhsIsActive) = rhs {
if lhsPeer != rhsPeer {
return false
}
case let .displayTabInfo(lhsTheme, lhsText):
if case let .displayTabInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
if lhsEditing != rhsEditing {
return false
}
case let .groupCall(lhsPeer, lhsEditing, lhsIsActive):
if case let .groupCall(rhsPeer, rhsEditing, rhsIsActive) = rhs {
if lhsPeer != rhsPeer {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsIsActive != rhsIsActive {
return false
}
return true
} else {
if lhsIsActive != rhsIsActive {
return false
}
case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsEditing, lhsHasRevealControls, lhsDisplayHeader, lhsMissed):
if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsEditing, rhsHasRevealControls, rhsDisplayHeader, rhsMissed) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsMissed != rhsMissed {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsHasRevealControls != rhsHasRevealControls {
return false
}
if lhsDisplayHeader != rhsDisplayHeader {
return false
}
if !areMessagesEqual(lhsMessage, rhsMessage) {
return false
}
if lhsMessages.count != rhsMessages.count {
return false
}
for i in 0 ..< lhsMessages.count {
if !areMessagesEqual(lhsMessages[i], rhsMessages[i]) {
return false
}
}
return true
} else {
return true
} else {
return false
}
case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsEditing, lhsHasRevealControls, lhsDisplayHeader, lhsMissed):
if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsEditing, rhsHasRevealControls, rhsDisplayHeader, rhsMissed) = rhs {
if lhsTheme !== rhsTheme {
return false
}
case let .holeEntry(lhsIndex, lhsTheme):
if case let .holeEntry(rhsIndex, rhsTheme) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme {
return true
} else {
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsMissed != rhsMissed {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsHasRevealControls != rhsHasRevealControls {
return false
}
if lhsDisplayHeader != rhsDisplayHeader {
return false
}
if !areMessagesEqual(lhsMessage, rhsMessage) {
return false
}
if lhsMessages.count != rhsMessages.count {
return false
}
for i in 0 ..< lhsMessages.count {
if !areMessagesEqual(lhsMessages[i], rhsMessages[i]) {
return false
}
}
return true
} else {
return false
}
case let .holeEntry(lhsIndex, lhsTheme):
if case let .holeEntry(rhsIndex, rhsTheme) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme {
return true
} else {
return false
}
}
}
}
func callListNodeEntriesForView(view: EngineCallList, groupCalls: [EnginePeer], state: CallListNodeState, showSettings: Bool, showCallsTab: Bool, isRecentCalls: Bool, currentGroupCallPeerId: EnginePeer.Id?) -> [CallListNodeEntry] {
func callListNodeEntriesForView(view: EngineCallList, canCreateGroupCall: Bool, groupCalls: [EnginePeer], state: CallListNodeState, showSettings: Bool, showCallsTab: Bool, isRecentCalls: Bool, currentGroupCallPeerId: EnginePeer.Id?) -> [CallListNodeEntry] {
var result: [CallListNodeEntry] = []
for entry in view.items {
switch entry {
@ -217,6 +236,10 @@ func callListNodeEntriesForView(view: EngineCallList, groupCalls: [EnginePeer],
result.append(.groupCall(peer: peer, editing: state.editing, isActive: currentGroupCallPeerId == peer.id))
}
}
if canCreateGroupCall {
result.append(.createGroupCall)
}
if showSettings {
result.append(.displayTabInfo(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIconDescription))

@ -82,7 +82,19 @@ private enum Knob {
case right
}
private final class InternalGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
return true
}
}
private final class RecognizedTextSelectionGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
private let internalDelegate = InternalGestureRecognizerDelegate()
private var longTapTimer: Timer?
private var movingKnob: (Knob, CGPoint, CGPoint)?
private var currentLocation: CGPoint?
@ -96,7 +108,7 @@ private final class RecognizedTextSelectionGestureRecognizer: UIGestureRecognize
override init(target: Any?, action: Selector?) {
super.init(target: nil, action: nil)
self.delegate = self
self.delegate = self.internalDelegate
}
override public func reset() {
@ -179,15 +191,6 @@ private final class RecognizedTextSelectionGestureRecognizer: UIGestureRecognize
self.state = .ended
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return true
}
@available(iOS 9.0, *)
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
return true
}
}
public final class RecognizedTextSelectionNodeView: UIView {

@ -60,6 +60,8 @@ swift_library(
"//submodules/PromptUI",
"//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",

@ -51,9 +51,9 @@ private enum InviteLinkInviteEntryId: Hashable {
}
private enum InviteLinkInviteEntry: Comparable, Identifiable {
case header(PresentationTheme, String, String)
case mainLink(PresentationTheme, ExportedInvitation?)
case manage(PresentationTheme, String, Bool)
case header(title: String, text: String)
case mainLink(invitation: ExportedInvitation?, isCall: Bool, isRecentlyCreated: Bool)
case manage(text: String, standalone: Bool)
var stableId: InviteLinkInviteEntryId {
switch self {
@ -68,20 +68,20 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsTitle, lhsText):
if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
case let .header(lhsTitle, lhsText):
if case let .header(rhsTitle, rhsText) = rhs, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
case let .mainLink(lhsTheme, lhsInvitation):
if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
case let .mainLink(lhsInvitation, lhsIsCall, lhsIsRecentlyCreated):
if case let .mainLink(rhsInvitation, rhsIsCall, rhsIsRecentlyCreated) = rhs, lhsInvitation == rhsInvitation, lhsIsCall == rhsIsCall, lhsIsRecentlyCreated == rhsIsRecentlyCreated {
return true
} else {
return false
}
case let .manage(lhsTheme, lhsText, lhsStandalone):
if case let .manage(rhsTheme, rhsText, rhsStandalone) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStandalone == rhsStandalone {
case let .manage(lhsText, lhsStandalone):
if case let .manage(rhsText, rhsStandalone) = rhs, lhsText == rhsText, lhsStandalone == rhsStandalone {
return true
} else {
return false
@ -117,23 +117,23 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem {
switch self {
case let .header(theme, title, text):
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
case let .mainLink(_, invite):
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: {
if let invite = invite {
case let .header(title, text):
return InviteLinkInviteHeaderItem(theme: presentationData.theme, title: title, text: text)
case let .mainLink(invitation, isCall, isRecentlyCreated):
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invitation, count: 0, peers: [], displayButton: true, separateButtons: isCall, displayImporters: false, isCall: isRecentlyCreated, buttonColor: nil, sectionId: 0, style: .plain, copyAction: {
if let invite = invitation {
interaction.copyLink(invite)
}
}, shareAction: {
if let invite = invite {
if let invite = invitation {
interaction.shareLink(invite)
}
}, contextAction: { node, gesture in
interaction.mainLinkContextAction(invite, node, gesture)
interaction.mainLinkContextAction(invitation, node, gesture)
}, viewAction: {
})
case let .manage(theme, text, standalone):
return InviteLinkInviteManageItem(theme: theme, text: text, standalone: standalone, action: {
case let .manage(text, standalone):
return InviteLinkInviteManageItem(theme: presentationData.theme, text: text, standalone: standalone, action: {
interaction.manageLinks()
})
}
@ -155,19 +155,31 @@ public final class InviteLinkInviteController: ViewController {
return self.displayNode as! Node
}
public enum Mode {
case groupOrChannel(peerId: EnginePeer.Id)
case groupCall(link: String, isRecentlyCreated: Bool)
}
public enum CompletionResult {
case linkCopied
}
private var animatedIn = false
private let context: AccountContext
private let peerId: EnginePeer.Id
private let mode: Mode
private weak var parentNavigationController: NavigationController?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
fileprivate let completed: ((CompletionResult?) -> Void)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, parentNavigationController: NavigationController?) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: Mode, parentNavigationController: NavigationController?, completed: ((CompletionResult?) -> Void)? = nil) {
self.context = context
self.peerId = peerId
self.mode = mode
self.parentNavigationController = parentNavigationController
self.completed = completed
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
@ -198,7 +210,7 @@ public final class InviteLinkInviteController: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context, presentationData: self.presentationData, peerId: self.peerId, controller: self)
self.displayNode = Node(context: self.context, presentationData: self.presentationData, mode: self.mode, controller: self)
}
private var didAppearOnce: Bool = false
@ -251,8 +263,8 @@ public final class InviteLinkInviteController: ViewController {
private weak var controller: InviteLinkInviteController?
private let context: AccountContext
private let peerId: EnginePeer.Id
private let invitesContext: PeerExportedInvitationsContext
private let mode: InviteLinkInviteController.Mode
private let groupOrChannelInvitesContext: PeerExportedInvitationsContext?
private var interaction: InviteLinkInviteInteraction?
@ -267,6 +279,7 @@ public final class InviteLinkInviteController: ViewController {
private let headerBackgroundNode: ASDisplayNode
private let titleNode: ImmediateTextNode
private let doneButton: HighlightableButtonNode
private let doneButtonIconNode: ASImageNode
private let historyBackgroundNode: ASDisplayNode
private let historyBackgroundContentNode: ASDisplayNode
private var floatingHeaderOffset: CGFloat?
@ -278,15 +291,19 @@ public final class InviteLinkInviteController: ViewController {
private var revokeDisposable = MetaDisposable()
init(context: AccountContext, presentationData: PresentationData, peerId: EnginePeer.Id, controller: InviteLinkInviteController) {
init(context: AccountContext, presentationData: PresentationData, mode: InviteLinkInviteController.Mode, controller: InviteLinkInviteController) {
self.context = context
self.peerId = peerId
self.mode = mode
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.controller = controller
self.invitesContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: false)
if case let .groupOrChannel(peerId) = mode {
self.groupOrChannelInvitesContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: false)
} else {
self.groupOrChannelInvitesContext = nil
}
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
@ -294,11 +311,12 @@ public final class InviteLinkInviteController: ViewController {
self.contentNode = ASDisplayNode()
self.headerNode = ASDisplayNode()
self.headerNode.clipsToBounds = true
self.headerNode.clipsToBounds = false
self.headerBackgroundNode = ASDisplayNode()
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.cornerRadius = 16.0
self.headerBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
@ -306,7 +324,9 @@ public final class InviteLinkInviteController: ViewController {
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.doneButton = HighlightableButtonNode()
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.doneButtonIconNode = ASImageNode()
self.doneButtonIconNode.image = generateCloseButtonImage(backgroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))!
self.historyBackgroundNode = ASDisplayNode()
self.historyBackgroundNode.isLayerBacked = true
@ -332,6 +352,9 @@ public final class InviteLinkInviteController: ViewController {
let mainInvitePromise = ValuePromise<ExportedInvitation?>(nil)
self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in
guard let self else {
return
}
guard let node = node as? ContextReferenceContentNode else {
return
}
@ -340,7 +363,7 @@ public final class InviteLinkInviteController: ViewController {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
if let invite = invite {
@ -353,80 +376,99 @@ public final class InviteLinkInviteController: ViewController {
}
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
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 {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
} else {
isGroup = true
}
let updatedPresentationData = (strongSelf.presentationData, strongSelf.presentationDataPromise.get())
let controller = QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup))
strongSelf.controller?.present(controller, in: .window(.root))
})
}
})))
if let invite = invite {
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 _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
} else {
isGroup = true
}
let updatedPresentationData = (strongSelf.presentationData, strongSelf.presentationDataPromise.get())
let controller = QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup))
strongSelf.controller?.present(controller, in: .window(.root))
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: isGroup ? presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text : presentationData.strings.ChannelInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
if let inviteLink = invite?.link {
let _ = (context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: inviteLink) |> deliverOnMainQueue).start(next: { result in
if let result = result, case let .replace(_, invite) = result {
mainInvitePromise.set(invite)
}
})
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))
})
}
})))
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: { _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
} else {
isGroup = true
}
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: isGroup ? presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text : presentationData.strings.ChannelInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
if let inviteLink = invite?.link {
let _ = (context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: inviteLink) |> deliverOnMainQueue).start(next: { result in
if let result = result, case let .replace(_, invite) = result {
mainInvitePromise.set(invite)
}
})
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)
self?.controller?.presentInGlobalOverlay(contextController)
self.controller?.presentInGlobalOverlay(contextController)
}, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link
self?.controller?.dismissAllTooltips()
guard let self else {
return
}
self.controller?.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
if let completed = self.controller?.completed {
self.controller?.dismiss()
completed(.linkCopied)
} else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
}, shareLink: { [weak self] invite in
guard let strongSelf = self, let inviteLink = invite.link else {
return
@ -496,49 +538,82 @@ public final class InviteLinkInviteController: ViewController {
guard let strongSelf = self else {
return
}
let updatedPresentationData = (strongSelf.presentationData, strongSelf.presentationDataPromise.get())
let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil)
strongSelf.controller?.parentNavigationController?.pushViewController(controller)
strongSelf.controller?.dismiss()
if case let .groupOrChannel(peerId) = strongSelf.mode {
let updatedPresentationData = (strongSelf.presentationData, strongSelf.presentationDataPromise.get())
let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil)
strongSelf.controller?.parentNavigationController?.pushViewController(controller)
strongSelf.controller?.dismiss()
}
})
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
let peerView = context.account.postbox.peerView(id: peerId)
let invites: Signal<PeerExportedInvitationsState, NoError> = .single(PeerExportedInvitationsState())
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, mainInvitePromise.get(), invites)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view, interactiveMainInvite, invites in
if let strongSelf = self {
switch mode {
case let .groupOrChannel(peerId):
let peerView = context.account.postbox.peerView(id: peerId)
let invites: Signal<PeerExportedInvitationsState, NoError> = .single(PeerExportedInvitationsState())
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, mainInvitePromise.get(), invites)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view, interactiveMainInvite, invites in
if let strongSelf = self {
var entries: [InviteLinkInviteEntry] = []
let helpText: String
if let peer = peerViewMainPeer(view) as? TelegramChannel, case .broadcast = peer.info {
helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelpChannel
} else {
helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelp
}
entries.append(.header(title: presentationData.strings.InviteLink_InviteLink, text: helpText))
let mainInvite: ExportedInvitation?
if let invite = interactiveMainInvite {
mainInvite = invite
} else if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation {
mainInvite = invite
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
mainInvite = invite
} else {
mainInvite = nil
}
entries.append(.mainLink(invitation: mainInvite, isCall: false, isRecentlyCreated: false))
entries.append(.manage(text: presentationData.strings.InviteLink_Manage, standalone: true))
let previousEntries = previousEntries.swap(entries)
let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: false, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction!)
strongSelf.enqueueTransition(transition)
}
})
case let .groupCall(link, isRecentlyCreated):
//TODO:release
let tempInfo: Signal<Void, NoError> = .single(Void()) |> delay(0.0, queue: .mainQueue())
self.disposable = (combineLatest(queue: .mainQueue(),
self.presentationDataPromise.get(),
tempInfo
)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, _ in
guard let self else {
return
}
var entries: [InviteLinkInviteEntry] = []
let helpText: String
if let peer = peerViewMainPeer(view) as? TelegramChannel, case .broadcast = peer.info {
helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelpChannel
} else {
helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelp
}
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_InviteLink, helpText))
//TODO:localize
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?
if let invite = interactiveMainInvite {
mainInvite = invite
} else if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation {
mainInvite = invite
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
mainInvite = invite
} else {
mainInvite = nil
}
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)
entries.append(.mainLink(presentationData.theme, mainInvite))
entries.append(.manage(presentationData.theme, presentationData.strings.InviteLink_Manage, true))
entries.append(.mainLink(invitation: mainInvite, isCall: true, isRecentlyCreated: isRecentlyCreated))
let previousEntries = previousEntries.swap(entries)
let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: false, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction!)
strongSelf.enqueueTransition(transition)
}
})
let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: false, account: context.account, presentationData: presentationData, interaction: self.interaction!)
self.enqueueTransition(transition)
})
}
self.listNode.preloadPages = true
self.listNode.stackFromBottom = true
@ -556,6 +631,7 @@ public final class InviteLinkInviteController: ViewController {
self.headerNode.addSubnode(self.headerBackgroundNode)
self.headerNode.addSubnode(self.doneButton)
self.doneButton.addSubnode(self.doneButtonIconNode)
self.doneButton.addTarget(self, action: #selector(self.doneButtonPressed), forControlEvents: .touchUpInside)
}
@ -591,7 +667,8 @@ public final class InviteLinkInviteController: ViewController {
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.doneButtonIconNode.image = generateCloseButtonImage(backgroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))!
}
private func enqueueTransition(_ transition: InviteLinkInviteTransaction) {
@ -658,7 +735,10 @@ public final class InviteLinkInviteController: ViewController {
insets.bottom = layout.intrinsicInsets.bottom
let headerHeight: CGFloat = 54.0
let visibleItemsHeight: CGFloat = 409.0
var visibleItemsHeight: CGFloat = 409.0
if case .groupCall = self.mode {
visibleItemsHeight += 80.0
}
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
@ -673,20 +753,26 @@ public final class InviteLinkInviteController: ViewController {
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize))
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 68.0))
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 36.0))
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width, height: headerHeight))
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let doneSize = self.doneButton.measure(CGSize(width: layout.size.width, height: headerHeight))
let doneFrame = CGRect(origin: CGPoint(x: layout.size.width - doneSize.width - 16.0, y: 18.0), size: doneSize)
transition.updateFrame(node: self.doneButton, frame: doneFrame)
if let image = self.doneButtonIconNode.image {
let doneSize = CGSize(width: 62.0, height: 56.0)
let doneFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - doneSize.width, y: 13.0), size: doneSize)
transition.updateFrame(node: self.doneButton, frame: doneFrame)
transition.updateFrame(node: self.doneButtonIconNode, frame: CGRect(origin: CGPoint(x: floor((doneFrame.width - image.size.width) / 2.0), y: floor((doneFrame.height - image.size.height) / 2.0)), size: image.size))
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let result = result, result === self.doneButton.view.hitTest(self.view.convert(point, to: self.doneButton.view), with: event) {
return self.doneButton.view
}
if result === self.headerNode.view {
return self.view
}
@ -807,7 +893,7 @@ public final class InviteLinkInviteController: ViewController {
// transition.updateAlpha(node: self.headerNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height))
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY - 10.0), size: CGSize(width: validLayout.size.width, height: validLayout.size.height))
let previousBackgroundFrame = self.historyBackgroundNode.frame
@ -822,3 +908,23 @@ public final class InviteLinkInviteController: ViewController {
}
}
}
private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(foregroundColor.cgColor)
context.beginPath()
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()
})
}

@ -59,8 +59,8 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
}
}
private let titleFont = Font.medium(23.0)
private let textFont = Font.regular(13.0)
private let titleFont = Font.bold(24.0)
private let textFont = Font.regular(15.0)
class InviteLinkInviteHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
@ -102,8 +102,8 @@ class InviteLinkInviteHeaderItemNode: ListViewItemNode {
return { item, params, neighbors in
let leftInset: CGFloat = 40.0 + params.leftInset
let topInset: CGFloat = 98.0
let spacing: CGFloat = 8.0
let bottomInset: CGFloat = 24.0
let spacing: CGFloat = 10.0
let bottomInset: CGFloat = 13.0
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
@ -113,7 +113,7 @@ class InviteLinkInviteHeaderItemNode: ListViewItemNode {
let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.freeTextColor)
let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + spacing + textLayout.size.height + bottomInset)
@ -131,14 +131,14 @@ class InviteLinkInviteHeaderItemNode: ListViewItemNode {
}
let iconSize = CGSize(width: 92.0, height: 92.0)
strongSelf.iconBackgroundNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.iconNode.frame = strongSelf.iconBackgroundNode.frame
strongSelf.iconBackgroundNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -18.0), size: iconSize)
strongSelf.iconNode.frame = strongSelf.iconBackgroundNode.frame.insetBy(dx: 8.0, dy: 8.0)
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 10.0), size: titleLayout.size)
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: topInset + 8.0 + titleLayout.size.height + spacing), size: textLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: topInset + 10.0 + titleLayout.size.height + spacing), size: textLayout.size)
}
})
}

@ -538,7 +538,7 @@ public final class InviteLinkViewController: ViewController {
self.headerNode.clipsToBounds = true
self.headerBackgroundNode = ASDisplayNode()
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.headerBackgroundNode.cornerRadius = 16.0
self.titleNode = ImmediateTextNode()
@ -1025,8 +1025,8 @@ public final class InviteLinkViewController: 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 = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: titleFont, textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: subtitleFont, textColor: self.presentationData.theme.list.itemSecondaryTextColor)

@ -10,6 +10,10 @@ import SolidRoundedButtonNode
import AnimatedAvatarSetNode
import ShimmerEffect
import TelegramCore
import Markdown
import TextFormat
import ComponentFlow
import MultilineTextComponent
private func actionButtonImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in
@ -34,6 +38,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let displayButton: Bool
let separateButtons: Bool
let displayImporters: Bool
let isCall: Bool
let buttonColor: UIColor?
public let sectionId: ItemListSectionId
let style: ItemListStyle
@ -52,6 +57,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
displayButton: Bool,
separateButtons: Bool = false,
displayImporters: Bool,
isCall: Bool = false,
buttonColor: UIColor?,
sectionId: ItemListSectionId,
style: ItemListStyle,
@ -69,6 +75,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.displayButton = displayButton
self.separateButtons = separateButtons
self.displayImporters = displayImporters
self.isCall = isCall
self.buttonColor = buttonColor
self.sectionId = sectionId
self.style = style
@ -140,6 +147,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
private var shimmerNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var justCreatedCallTextNode: TextNode?
private var justCreatedCallLeftSeparatorLayer: SimpleLayer?
private var justCreatedCallRightSeparatorLayer: SimpleLayer?
private var justCreatedCallSeparatorText: ComponentView<Empty>?
private let activateArea: AccessibilityAreaNode
private var item: ItemListPermanentInviteLinkItem?
@ -287,6 +299,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 currentItem = self.item
let avatarsContext = self.avatarsContext
@ -330,14 +343,56 @@ 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?)?
if item.isCall {
let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = item.presentationData.theme.list.itemPrimaryTextColor
let accentColor = item.presentationData.theme.list.itemAccentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
//TODO:localize
let justCreatedCallTextAttributedString = parseMarkdownIntoAttributedString("Be the first to join the call and add people from there. [Open Call >]()", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
if let range = justCreatedCallTextAttributedString.string.range(of: ">"), let chevronImage {
justCreatedCallTextAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: justCreatedCallTextAttributedString.string))
}
justCreatedCallTextNodeLayout = makeJustCreatedCallTextNodeLayout(TextNodeLayoutArguments(
attributedString: justCreatedCallTextAttributedString,
backgroundColor: nil,
maximumNumberOfLines: 0,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
alignment: .center,
lineSpacing: 0.28,
cutout: nil,
insets: UIEdgeInsets()
))
}
let avatarsContent = avatarsContext.update(peers: item.peers, animated: false)
let verticalInset: CGFloat = 16.0
let fieldHeight: CGFloat = 52.0
let fieldSpacing: CGFloat = 16.0
let buttonHeight: CGFloat = 50.0
let justCreatedCallSeparatorSpacing: CGFloat = 16.0
let justCreatedCallTextSpacing: CGFloat = 45.0
var height = verticalInset * 2.0 + fieldHeight + fieldSpacing + buttonHeight + 54.0
if let justCreatedCallTextNodeLayout {
height += justCreatedCallTextSpacing - 2.0
height += justCreatedCallTextNodeLayout.0.size.height
}
switch item.style {
case .plain:
@ -514,6 +569,81 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
if let justCreatedCallTextNodeLayout {
if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1() {
if strongSelf.justCreatedCallTextNode !== justCreatedCallTextNode {
strongSelf.justCreatedCallTextNode?.removeFromSupernode()
strongSelf.justCreatedCallTextNode = justCreatedCallTextNode
strongSelf.addSubnode(justCreatedCallTextNode)
}
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
let justCreatedCallSeparatorText: ComponentView<Empty>
if let current = strongSelf.justCreatedCallSeparatorText {
justCreatedCallSeparatorText = current
} else {
justCreatedCallSeparatorText = ComponentView()
strongSelf.justCreatedCallSeparatorText = justCreatedCallSeparatorText
}
let justCreatedCallLeftSeparatorLayer: SimpleLayer
if let current = strongSelf.justCreatedCallLeftSeparatorLayer {
justCreatedCallLeftSeparatorLayer = current
} else {
justCreatedCallLeftSeparatorLayer = SimpleLayer()
strongSelf.justCreatedCallLeftSeparatorLayer = justCreatedCallLeftSeparatorLayer
strongSelf.layer.addSublayer(justCreatedCallLeftSeparatorLayer)
}
let justCreatedCallRightSeparatorLayer: SimpleLayer
if let current = strongSelf.justCreatedCallRightSeparatorLayer {
justCreatedCallRightSeparatorLayer = current
} else {
justCreatedCallRightSeparatorLayer = SimpleLayer()
strongSelf.justCreatedCallRightSeparatorLayer = justCreatedCallRightSeparatorLayer
strongSelf.layer.addSublayer(justCreatedCallRightSeparatorLayer)
}
justCreatedCallLeftSeparatorLayer.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor.cgColor
justCreatedCallRightSeparatorLayer.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor.cgColor
let justCreatedCallSeparatorTextSize = justCreatedCallSeparatorText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: item.presentationData.strings.SendInviteLink_PremiumOrSendSectionSeparator, font: Font.regular(15.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0)
)
let justCreatedCallSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((params.width - justCreatedCallSeparatorTextSize.width) * 0.5), y: shareButtonNode.frame.maxY + justCreatedCallSeparatorSpacing), size: justCreatedCallSeparatorTextSize)
if let justCreatedCallSeparatorTextView = justCreatedCallSeparatorText.view {
if justCreatedCallSeparatorTextView.superview == nil {
strongSelf.view.addSubview(justCreatedCallSeparatorTextView)
}
justCreatedCallSeparatorTextView.frame = justCreatedCallSeparatorTextFrame
}
let separatorWidth: CGFloat = 72.0
let separatorSpacing: CGFloat = 10.0
justCreatedCallLeftSeparatorLayer.frame = CGRect(origin: CGPoint(x: justCreatedCallSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: justCreatedCallSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))
justCreatedCallRightSeparatorLayer.frame = CGRect(origin: CGPoint(x: justCreatedCallSeparatorTextFrame.maxX + separatorSpacing, y: justCreatedCallSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))
}
} else if let justCreatedCallTextNode = strongSelf.justCreatedCallTextNode {
strongSelf.justCreatedCallTextNode = nil
justCreatedCallTextNode.removeFromSupernode()
strongSelf.justCreatedCallLeftSeparatorLayer?.removeFromSuperlayer()
strongSelf.justCreatedCallLeftSeparatorLayer = nil
strongSelf.justCreatedCallRightSeparatorLayer?.removeFromSuperlayer()
strongSelf.justCreatedCallRightSeparatorLayer = nil
strongSelf.justCreatedCallSeparatorText?.view?.removeFromSuperview()
strongSelf.justCreatedCallSeparatorText = nil
}
var totalWidth = invitedPeersLayout.size.width
var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0)

@ -30,10 +30,11 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
let editing: Bool
let height: ItemListPeerActionItemHeight
let color: ItemListPeerActionItemColor
let noInsets: Bool
public let sectionId: ItemListSectionId
public let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) {
public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, noInsets: Bool = false, editing: Bool = false, action: (() -> Void)?) {
self.presentationData = presentationData
self.icon = icon
self.iconSignal = iconSignal
@ -43,6 +44,7 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
self.hasSeparator = hasSeparator
self.editing = editing
self.height = height
self.noInsets = noInsets
self.color = color
self.sectionId = sectionId
self.action = action
@ -217,7 +219,11 @@ public final class ItemListPeerActionItemNode: ListViewItemNode {
let separatorHeight = UIScreenPixel
let insets = itemListNeighborsGroupedInsets(neighbors, params)
var insets = itemListNeighborsGroupedInsets(neighbors, params)
if item.noInsets {
insets.top = 0.0
insets.bottom = 0.0
}
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset * 2.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)

@ -11,7 +11,7 @@ import PresentationDataUtils
import UndoUI
import OldChannelsController
public final class JoinLinkPreviewController: ViewController {
public final class LegacyJoinLinkPreviewController: ViewController {
private var controllerNode: JoinLinkPreviewControllerNode {
return self.displayNode as! JoinLinkPreviewControllerNode
}
@ -184,3 +184,39 @@ public final class JoinLinkPreviewController: ViewController {
}
}
public func JoinLinkPreviewController(
context: AccountContext,
link: String,
navigateToPeer: @escaping (EnginePeer, ChatPeekTimeout?) -> Void,
parentNavigationController: NavigationController?,
resolvedState: ExternalJoiningChatState? = nil
) -> ViewController {
if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_legacy_join_link"] != nil {
return LegacyJoinLinkPreviewController(context: context, link: link, navigateToPeer: navigateToPeer, parentNavigationController: parentNavigationController, resolvedState: resolvedState)
} else if case let .invite(invite) = resolvedState, !invite.flags.requestNeeded, !invite.flags.isBroadcast, !invite.flags.canRefulfillSubscription {
//TODO:release
var verificationStatus: JoinSubjectScreenMode.Group.VerificationStatus?
if invite.flags.isFake {
verificationStatus = .fake
} else if invite.flags.isScam {
verificationStatus = .scam
} else if invite.flags.isVerified {
verificationStatus = .verified
}
return context.sharedContext.makeJoinSubjectScreen(context: context, mode: .group(JoinSubjectScreenMode.Group(
link: link,
isGroup: !invite.flags.isChannel,
isPublic: invite.flags.isPublic,
isRequest: invite.flags.requestNeeded,
verificationStatus: verificationStatus,
image: invite.photoRepresentation,
title: invite.title,
about: invite.about,
memberCount: invite.participantsCount,
members: invite.participants ?? []
)))
} else {
return LegacyJoinLinkPreviewController(context: context, link: link, navigateToPeer: navigateToPeer, parentNavigationController: parentNavigationController, resolvedState: resolvedState)
}
}

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

@ -119,6 +119,7 @@ swift_library(
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/DirectMediaImageCache",
"//submodules/FastBlur",
"//submodules/InviteLinksUI",
],
visibility = [
"//visibility:public",

@ -1680,6 +1680,10 @@ public final class PresentationCallImpl: PresentationCall {
}
}
}
func deactivateIncomingAudio() {
self.ongoingContext?.deactivateIncomingAudio()
}
}
func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {

@ -539,6 +539,7 @@ private final class ScreencastInProcessIPCContext: ScreencastIPCContext {
onMutedSpeechActivityDetected: { _ in },
encryptionKey: nil,
isConference: self.isConference,
audioIsActiveByDefault: true,
isStream: false,
sharedAudioDevice: nil
)
@ -1937,6 +1938,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} else {
contextAudioSessionActive = self.audioSessionActive.get()
}
var audioIsActiveByDefault = true
if self.isConference && self.conferenceSourceId != nil {
audioIsActiveByDefault = false
}
genericCallContext = .call(OngoingGroupCallContext(audioSessionActive: contextAudioSessionActive, video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in
let disposable = MetaDisposable()
@ -1963,7 +1969,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
self.onMutedSpeechActivityDetected?(value)
}
}, encryptionKey: encryptionKey, isConference: self.isConference, isStream: self.isStream, sharedAudioDevice: self.sharedAudioContext?.audioDevice))
}, encryptionKey: encryptionKey, isConference: self.isConference, audioIsActiveByDefault: audioIsActiveByDefault, isStream: self.isStream, sharedAudioDevice: self.sharedAudioContext?.audioDevice))
}
self.genericCallContext = genericCallContext
@ -2756,6 +2762,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private func activateIncomingAudioIfNeeded() {
if let genericCallContext = self.genericCallContext, case let .call(groupCall) = genericCallContext {
groupCall.activateIncomingAudio()
if let pendingDisconnedUpgradedConferenceCall = self.pendingDisconnedUpgradedConferenceCall {
pendingDisconnedUpgradedConferenceCall.deactivateIncomingAudio()
}
}
}

@ -6,6 +6,9 @@ import SwiftSignalKit
import PeerInfoUI
import OverlayStatusController
import PresentationDataUtils
import InviteLinksUI
import UndoUI
import TelegramPresentationData
extension VideoChatScreenComponent.View {
func openInviteMembers() {
@ -13,6 +16,31 @@ 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)

@ -1326,21 +1326,66 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
navigationController?.pushViewController(controller)
})
} else {
strongSelf.presentController(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
let joinLinkPreviewController = JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
openPeer(peer, peekData)
}, parentNavigationController: navigationController, resolvedState: resolvedState), .window(.root), nil)
}, parentNavigationController: navigationController, resolvedState: resolvedState)
if joinLinkPreviewController.navigationPresentation == .flatModal {
strongSelf.pushController(joinLinkPreviewController)
} else {
strongSelf.presentController(joinLinkPreviewController, .window(.root), nil)
}
}
default:
strongSelf.presentController(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
let joinLinkPreviewController = JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
openPeer(peer, peekData)
}, parentNavigationController: navigationController, resolvedState: resolvedState), .window(.root), nil)
}, parentNavigationController: navigationController, resolvedState: resolvedState)
if joinLinkPreviewController.navigationPresentation == .flatModal {
strongSelf.pushController(joinLinkPreviewController)
} else {
strongSelf.presentController(joinLinkPreviewController, .window(.root), nil)
}
}
})
} else {
strongSelf.presentController(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
let joinLinkPreviewController = JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
openPeer(peer, peekData)
}, parentNavigationController: navigationController), .window(.root), nil)
}, parentNavigationController: navigationController, resolvedState: nil)
if joinLinkPreviewController.navigationPresentation == .flatModal {
strongSelf.pushController(joinLinkPreviewController)
} else {
strongSelf.presentController(joinLinkPreviewController, .window(.root), nil)
}
}
case let .joinCall(link):
let context = strongSelf.context
let navigationController = strongSelf.getNavigationController()
let progressSignal = Signal<Never, NoError> { subscriber in
progress?.set(.single(true))
return ActionDisposable {
Queue.mainQueue().async() {
progress?.set(.single(false))
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.1, queue: Queue.mainQueue())
let progressDisposable = progressSignal.startStrict()
var signal = context.engine.peers.joinCallLinkInformation(link)
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] resolvedCallLink in
navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall(
inviter: resolvedCallLink.inviter, members: resolvedCallLink.members, totalMemberCount: resolvedCallLink.totalMemberCount
))))
})
case let .localization(identifier):
strongSelf.presentController(LanguageLinkPreviewController(context: strongSelf.context, identifier: identifier), .window(.root), nil)
case .proxy, .confirmationCode, .cancelAccountReset, .share:

@ -2726,10 +2726,9 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor:
context.setLineCap(.round)
context.setStrokeColor(foregroundColor.cgColor)
context.beginPath()
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.strokePath()
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()

@ -0,0 +1,36 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "JoinSubjectScreen",
module_name = "JoinSubjectScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/AppBundle",
"//submodules/AccountContext",
"//submodules/Markdown",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramCore",
"//submodules/AvatarNode",
"//submodules/TelegramStringFormatting",
"//submodules/AnimatedAvatarSetNode",
"//submodules/UndoUI",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/PresentationDataUtils",
],
visibility = [
"//visibility:public",
],
)

File diff suppressed because it is too large Load Diff

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

@ -235,13 +235,6 @@ private func oldChannelsEntries(presentationData: PresentationData, state: OldCh
return entries
}
public enum OldChannelsControllerIntent {
case join
case create
case upgrade
}
public func oldChannelsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void = { _ in }) -> ViewController {
let initialState = OldChannelsState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)

@ -339,21 +339,65 @@ func openResolvedUrlImpl(
navigationController?.pushViewController(controller)
})
} else {
present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
let joinLinkPreviewController = JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
}, parentNavigationController: navigationController, resolvedState: resolvedState), nil)
}, parentNavigationController: navigationController, resolvedState: resolvedState)
if joinLinkPreviewController.navigationPresentation == .flatModal {
navigationController?.pushViewController(joinLinkPreviewController)
} else {
present(joinLinkPreviewController, nil)
}
}
default:
present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
let joinLinkPreviewController = JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
}, parentNavigationController: navigationController, resolvedState: resolvedState), nil)
}, parentNavigationController: navigationController, resolvedState: resolvedState)
if joinLinkPreviewController.navigationPresentation == .flatModal {
navigationController?.pushViewController(joinLinkPreviewController)
} else {
present(joinLinkPreviewController, nil)
}
}
})
} else {
present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
let joinLinkPreviewController = JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
}, parentNavigationController: navigationController), nil)
}, parentNavigationController: navigationController, resolvedState: nil)
if joinLinkPreviewController.navigationPresentation == .flatModal {
navigationController?.pushViewController(joinLinkPreviewController)
} else {
present(joinLinkPreviewController, nil)
}
}
case let .joinCall(link):
dismissInput()
let progressSignal = Signal<Never, NoError> { subscriber in
progress?.set(.single(true))
return ActionDisposable {
Queue.mainQueue().async() {
progress?.set(.single(false))
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.1, queue: Queue.mainQueue())
let progressDisposable = progressSignal.startStrict()
var signal = context.engine.peers.joinCallLinkInformation(link)
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] resolvedCallLink in
navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall(
inviter: resolvedCallLink.inviter, members: resolvedCallLink.members, totalMemberCount: resolvedCallLink.totalMemberCount
))))
})
case let .localization(identifier):
dismissInput()
present(LanguageLinkPreviewController(context: context, identifier: identifier), nil)

@ -78,6 +78,8 @@ import AffiliateProgramSetupScreen
import GalleryUI
import ShareController
import AccountFreezeInfoScreen
import JoinSubjectScreen
import OldChannelsController
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -473,6 +475,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|> deliverOnMainQueue).start(next: { sharedData in
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) {
let _ = immediateExperimentalUISettingsValue.swap(settings)
flatBuffers_checkedGet = settings.checkSerializedData
}
})
@ -3584,6 +3588,14 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return JoinAffiliateProgramScreen(context: context, sourcePeer: sourcePeer, commissionPermille: commissionPermille, programDuration: programDuration, revenuePerUser: revenuePerUser, mode: mode)
}
public func makeJoinSubjectScreen(context: AccountContext, mode: JoinSubjectScreenMode) -> ViewController {
return JoinSubjectScreen(context: context, mode: mode)
}
public func makeOldChannelsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void) -> ViewController {
return oldChannelsController(context: context, updatedPresentationData: updatedPresentationData, intent: intent, completed: completed)
}
public func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController {
let controller = GalleryController(context: context, source: source, streamSingleVideo: streamSingleVideo, replaceRootController: { _, _ in
}, baseNavigationController: nil)

@ -497,6 +497,7 @@ public final class OngoingGroupCallContext {
onMutedSpeechActivityDetected: @escaping (Bool) -> Void,
encryptionKey: Data?,
isConference: Bool,
audioIsActiveByDefault: Bool,
isStream: Bool,
sharedAudioDevice: OngoingCallContext.AudioDevice?
) {
@ -632,7 +633,8 @@ public final class OngoingGroupCallContext {
},
audioDevice: audioDevice?.impl,
encryptionKey: encryptionKey,
isConference: isConference
isConference: isConference,
isActiveByDefault: audioIsActiveByDefault
)
#else
self.context = GroupCallThreadLocalContext(
@ -732,7 +734,8 @@ public final class OngoingGroupCallContext {
statsLogPath: tempStatsLogPath,
audioDevice: nil,
encryptionKey: encryptionKey,
isConference: isConference
isConference: isConference,
isActiveByDefault: true
)
#endif
@ -1181,10 +1184,10 @@ public final class OngoingGroupCallContext {
}
}
public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal<Bool, NoError>, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, enableSystemMute: Bool, preferX264: Bool, logPath: String, onMutedSpeechActivityDetected: @escaping (Bool) -> Void, encryptionKey: Data?, isConference: Bool, isStream: Bool, sharedAudioDevice: OngoingCallContext.AudioDevice?) {
public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal<Bool, NoError>, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, enableSystemMute: Bool, preferX264: Bool, logPath: String, onMutedSpeechActivityDetected: @escaping (Bool) -> Void, encryptionKey: Data?, isConference: Bool, audioIsActiveByDefault: Bool, isStream: Bool, sharedAudioDevice: OngoingCallContext.AudioDevice?) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, enableSystemMute: enableSystemMute, preferX264: preferX264, logPath: logPath, onMutedSpeechActivityDetected: onMutedSpeechActivityDetected, encryptionKey: encryptionKey, isConference: isConference, isStream: isStream, sharedAudioDevice: sharedAudioDevice)
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, enableSystemMute: enableSystemMute, preferX264: preferX264, logPath: logPath, onMutedSpeechActivityDetected: onMutedSpeechActivityDetected, encryptionKey: encryptionKey, isConference: isConference, audioIsActiveByDefault: audioIsActiveByDefault, isStream: isStream, sharedAudioDevice: sharedAudioDevice)
})
}

@ -286,6 +286,7 @@ private protocol OngoingCallThreadLocalContextProtocol: AnyObject {
func nativeGetDerivedState() -> Data
func addExternalAudioData(data: Data)
func nativeSetIsAudioSessionActive(isActive: Bool)
func nativeDeactivateIncomingAudio()
}
private final class OngoingCallThreadLocalContextHolder {
@ -692,6 +693,10 @@ extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProt
self.addExternalAudioData(data)
}
func nativeDeactivateIncomingAudio() {
self.deactivateIncomingAudio()
}
func nativeSetIsAudioSessionActive(isActive: Bool) {
#if os(iOS)
self.setManualAudioSessionIsActive(isActive)
@ -1394,6 +1399,12 @@ public final class OngoingCallContext {
strongSelf.callSessionManager.sendSignalingData(internalId: strongSelf.internalId, data: data)
}
}
public func deactivateIncomingAudio() {
self.withContext { context in
context.nativeDeactivateIncomingAudio()
}
}
}
private protocol CallSignalingConnection: AnyObject {

@ -315,6 +315,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
- (void)switchAudioOutput:(NSString * _Nonnull)deviceId;
- (void)switchAudioInput:(NSString * _Nonnull)deviceId;
- (void)addExternalAudioData:(NSData * _Nonnull)data;
- (void)deactivateIncomingAudio;
@end
@ -452,7 +453,8 @@ statsLogPath:(NSString * _Nonnull)statsLogPath
onMutedSpeechActivityDetected:(void (^ _Nullable)(bool))onMutedSpeechActivityDetected
audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice
encryptionKey:(NSData * _Nullable)encryptionKey
isConference:(bool)isConference;
isConference:(bool)isConference
isActiveByDefault:(bool)isActiveByDefault;
- (void)stop:(void (^ _Nullable)())completion;

@ -127,7 +127,7 @@ public:
public:
virtual rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> audioDeviceModule() = 0;
virtual rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> makeChildAudioDeviceModule() = 0;
virtual rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> makeChildAudioDeviceModule(bool isActive) = 0;
virtual void start() = 0;
};
@ -147,14 +147,14 @@ public:
return 0;
}
void UpdateAudioCallback(webrtc::AudioTransport *previousAudioCallback, webrtc::AudioTransport *audioCallback) {
void UpdateAudioCallback(webrtc::AudioTransport *previousAudioCallback, webrtc::AudioTransport *audioCallback, bool isActive) {
_mutex.Lock();
if (audioCallback) {
_audioTransports.push_back(audioCallback);
_audioTransports.push_back(std::make_pair(audioCallback, isActive));
} else if (previousAudioCallback) {
for (size_t i = 0; i < _audioTransports.size(); i++) {
if (_audioTransports[i] == previousAudioCallback) {
if (_audioTransports[i].first == previousAudioCallback) {
_audioTransports.erase(_audioTransports.begin() + i);
break;
}
@ -163,6 +163,18 @@ public:
_mutex.Unlock();
}
void UpdateAudioCallbackIsActive(webrtc::AudioTransport *audioCallback, bool isActive) {
_mutex.Lock();
for (auto &it : _audioTransports) {
if (it.first == audioCallback) {
it.second = isActive;
}
}
_mutex.Unlock();
}
virtual int32_t RegisterAudioCallback(webrtc::AudioTransport *audioCallback) override {
return 0;
@ -474,7 +486,7 @@ public:
_mutex.Lock();
if (!_audioTransports.empty()) {
for (size_t i = 0; i < _audioTransports.size(); i++) {
_audioTransports[i]->RecordedDataIsAvailable(
_audioTransports[i].first->RecordedDataIsAvailable(
audioSamples,
nSamples,
nBytesPerSample,
@ -508,7 +520,7 @@ public:
_mutex.Lock();
if (!_audioTransports.empty()) {
for (size_t i = 0; i < _audioTransports.size(); i++) {
_audioTransports[i]->RecordedDataIsAvailable(
_audioTransports[i].first->RecordedDataIsAvailable(
audioSamples,
nSamples,
nBytesPerSample,
@ -552,11 +564,14 @@ public:
int16_t *resultAudioSamples = (int16_t *)audioSamples;
for (size_t i = 0; i < _audioTransports.size(); i++) {
if (!_audioTransports[i].second) {
continue;
}
int64_t localElapsedTimeMs = 0;
int64_t localNtpTimeMs = 0;
size_t localNSamplesOut = 0;
_audioTransports[i]->NeedMorePlayData(
_audioTransports[i].first->NeedMorePlayData(
nSamples,
nBytesPerSample,
nChannels,
@ -584,7 +599,7 @@ public:
}
nSamplesOut = nSamples;
} else {
result = _audioTransports[_audioTransports.size() - 1]->NeedMorePlayData(
result = _audioTransports[_audioTransports.size() - 1].first->NeedMorePlayData(
nSamples,
nBytesPerSample,
nChannels,
@ -616,7 +631,7 @@ public:
_mutex.Lock();
if (!_audioTransports.empty()) {
_audioTransports[_audioTransports.size() - 1]->PullRenderData(
_audioTransports[_audioTransports.size() - 1].first->PullRenderData(
bits_per_sample,
sample_rate,
number_of_channels,
@ -650,6 +665,9 @@ public:
virtual void Stop() override {
}
virtual void setIsActive(bool isActive) override {
}
virtual void ActualStop() {
if (_isStarted) {
_isStarted = false;
@ -661,22 +679,23 @@ public:
private:
bool _isStarted = false;
std::vector<webrtc::AudioTransport *> _audioTransports;
std::vector<std::pair<webrtc::AudioTransport *, bool>> _audioTransports;
webrtc::Mutex _mutex;
std::vector<int16_t> _mixAudioSamples;
};
class WrappedChildAudioDeviceModule : public tgcalls::DefaultWrappedAudioDeviceModule {
public:
WrappedChildAudioDeviceModule(webrtc::scoped_refptr<WrappedAudioDeviceModuleIOS> impl) :
tgcalls::DefaultWrappedAudioDeviceModule(impl) {
WrappedChildAudioDeviceModule(webrtc::scoped_refptr<WrappedAudioDeviceModuleIOS> impl, bool isActive) :
tgcalls::DefaultWrappedAudioDeviceModule(impl),
_isActive(isActive) {
}
virtual ~WrappedChildAudioDeviceModule() {
if (_audioCallback) {
auto previousAudioCallback = _audioCallback;
_audioCallback = nullptr;
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallback(previousAudioCallback, nullptr);
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallback(previousAudioCallback, nullptr, false);
}
}
@ -685,21 +704,20 @@ public:
_audioCallback = audioCallback;
if (_isActive) {
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallback(previousAudioCallback, audioCallback);
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallback(previousAudioCallback, audioCallback, _isActive);
}
return 0;
}
public:
void setIsActive() {
if (_isActive) {
void setIsActive(bool isActive) override {
if (_isActive == isActive) {
return;
}
_isActive = true;
_isActive = isActive;
if (_audioCallback) {
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallback(nullptr, _audioCallback);
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallbackIsActive(_audioCallback, isActive);
}
}
@ -733,8 +751,8 @@ public:
return _audioDeviceModule;
}
rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> makeChildAudioDeviceModule() override {
return rtc::make_ref_counted<WrappedChildAudioDeviceModule>(_audioDeviceModule);
rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> makeChildAudioDeviceModule(bool isActive) override {
return rtc::make_ref_counted<WrappedChildAudioDeviceModule>(_audioDeviceModule, isActive);
}
virtual void start() override {
@ -1928,8 +1946,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
},
.createWrappedAudioDeviceModule = [audioDeviceModule](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> {
if (audioDeviceModule) {
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule();
((WrappedChildAudioDeviceModule *)result.get())->setIsActive();
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(true);
return result;
} else {
return nullptr;
@ -2260,6 +2277,17 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
}
}
- (void)deactivateIncomingAudio {
if (_currentAudioDeviceModuleThread) {
auto currentAudioDeviceModule = _currentAudioDeviceModule;
if (currentAudioDeviceModule) {
_currentAudioDeviceModuleThread->PostTask([currentAudioDeviceModule]() {
((tgcalls::WrappedAudioDeviceModule *)currentAudioDeviceModule.get())->setIsActive(false);
});
}
}
}
@end
namespace {
@ -2347,7 +2375,8 @@ statsLogPath:(NSString * _Nonnull)statsLogPath
onMutedSpeechActivityDetected:(void (^ _Nullable)(bool))onMutedSpeechActivityDetected
audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice
encryptionKey:(NSData * _Nullable)encryptionKey
isConference:(bool)isConference {
isConference:(bool)isConference
isActiveByDefault:(bool)isActiveByDefault {
self = [super init];
if (self != nil) {
_queue = queue;
@ -2636,10 +2665,9 @@ isConference:(bool)isConference {
return resultModule;
}
},
.createWrappedAudioDeviceModule = [audioDeviceModule](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> {
.createWrappedAudioDeviceModule = [audioDeviceModule, isActiveByDefault](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> {
if (audioDeviceModule) {
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule();
((WrappedChildAudioDeviceModule *)result.get())->setIsActive();
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault);
return result;
} else {
return nullptr;
@ -2983,6 +3011,14 @@ isConference:(bool)isConference {
}
- (void)activateIncomingAudio {
if (_currentAudioDeviceModuleThread) {
auto currentAudioDeviceModule = _currentAudioDeviceModule;
if (currentAudioDeviceModule) {
_currentAudioDeviceModuleThread->PostTask([currentAudioDeviceModule]() {
((tgcalls::WrappedAudioDeviceModule *)currentAudioDeviceModule.get())->setIsActive(true);
});
}
}
}
@end

@ -1 +1 @@
Subproject commit bc8334224dbefb4591d669f7569d16f69134c5b6
Subproject commit 29862dd6202e5f71db38e9722fecfd0ab3078268

@ -91,6 +91,7 @@ public enum ParsedInternalUrl {
case stickerPack(name: String, type: StickerPackUrlType)
case invoice(String)
case join(String)
case joinCall(String)
case localization(String)
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?)
case internalInstantView(url: String)
@ -400,6 +401,12 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
return .invoice(pathComponents[1])
} else if pathComponents[0] == "joinchat" || pathComponents[0] == "joinchannel" {
return .join(pathComponents[1])
} else if pathComponents[0] == "call" {
var callHash = pathComponents[1]
if callHash.hasPrefix("+") {
callHash = String(callHash.dropFirst())
}
return .joinCall(callHash)
} else if pathComponents[0] == "setlanguage" {
return .localization(pathComponents[1])
} else if pathComponents[0] == "login" {
@ -1036,6 +1043,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
})
case let .join(link):
return .single(.result(.join(link)))
case let .joinCall(link):
return .single(.result(.joinCall(link)))
case let .localization(identifier):
return .single(.result(.localization(identifier)))
case let .proxy(host, port, username, password, secret):