diff --git a/Telegram/BroadcastUpload/BroadcastUploadExtension.swift b/Telegram/BroadcastUpload/BroadcastUploadExtension.swift index f7a2b7bf6b..684b766b84 100644 --- a/Telegram/BroadcastUpload/BroadcastUploadExtension.swift +++ b/Telegram/BroadcastUpload/BroadcastUploadExtension.swift @@ -168,6 +168,7 @@ private final class EmbeddedBroadcastUploadImpl: BroadcastUploadImpl { onMutedSpeechActivityDetected: { _ in }, encryptionKey: nil, isConference: false, + audioIsActiveByDefault: true, isStream: false, sharedAudioDevice: nil ) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c8360edda3..97754b71aa 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 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)?, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void) -> ViewController + func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController diff --git a/submodules/CallListUI/BUILD b/submodules/CallListUI/BUILD index 636bbe2779..72112be5ed 100644 --- a/submodules/CallListUI/BUILD +++ b/submodules/CallListUI/BUILD @@ -30,6 +30,9 @@ swift_library( "//submodules/TelegramBaseController:TelegramBaseController", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/ItemListPeerActionItem", + "//submodules/InviteLinksUI", + "//submodules/UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 45cd61a894..e14df4f150 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -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 { diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index fcf5d2537b..e4e0c66218 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -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() 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 in + |> mapToQueue { (updateAndType, state, groupCalls, showCallsTab, currentGroupCallPeerId, canCreateGroupCall) -> Signal 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) diff --git a/submodules/CallListUI/Sources/CallListNodeEntries.swift b/submodules/CallListUI/Sources/CallListNodeEntries.swift index d4a917251e..22356ea608 100644 --- a/submodules/CallListUI/Sources/CallListNodeEntries.swift +++ b/submodules/CallListUI/Sources/CallListNodeEntries.swift @@ -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)) diff --git a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift index 21e1ff7868..a41104016f 100644 --- a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift +++ b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift @@ -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 { diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index 37f6192719..bed130117c 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -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", diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index 4185b462b1..abd09c75b8 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -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)? = nil, peerId: EnginePeer.Id, parentNavigationController: NavigationController?) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = 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(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 = .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 = .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 = .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() + }) +} diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteHeaderItem.swift index 052de0d4a3..22ec323310 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteHeaderItem.swift @@ -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) } }) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index e303fe8b3b..17b5068a7b 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -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) diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 35dc5ed1b8..26d2b819f7 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -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? + 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 + 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) diff --git a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift index c6bce9e12a..29ea6129d2 100644 --- a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift +++ b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift @@ -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? = 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? = 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) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index 801468bc2c..91c373bc47 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -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) + } +} diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 282c219f2f..854b42cfc0 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -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() diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index 4a9bc49f2d..bd6281c776 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -119,6 +119,7 @@ swift_library( "//submodules/Components/BlurredBackgroundComponent", "//submodules/DirectMediaImageCache", "//submodules/FastBlur", + "//submodules/InviteLinksUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index de7be12e55..2aa536fca2 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -1680,6 +1680,10 @@ public final class PresentationCallImpl: PresentationCall { } } } + + func deactivateIncomingAudio() { + self.ongoingContext?.deactivateIncomingAudio() + } } func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 7374021ee5..4a410b9657 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -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() + } } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift index 22557857bc..7eed5568b4 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index c732dcb85f..82b8b9d88c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -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 { 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: diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index b4b1cddf04..3cabbce74b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -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() diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD b/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD new file mode 100644 index 0000000000..ae5f1212af --- /dev/null +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift new file mode 100644 index 0000000000..34090d0df4 --- /dev/null +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -0,0 +1,1090 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AccountContext +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import ButtonComponent +import BundleIconComponent +import Markdown +import TelegramCore +import AvatarNode +import TelegramStringFormatting +import AnimatedAvatarSetNode +import UndoUI +import PresentationDataUtils + +private final class JoinSubjectScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let mode: JoinSubjectScreenMode + + init( + context: AccountContext, + mode: JoinSubjectScreenMode + ) { + self.context = context + self.mode = mode + } + + static func ==(lhs: JoinSubjectScreenComponent, rhs: JoinSubjectScreenComponent) -> Bool { + return true + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let navigationBackgroundView: BlurredBackgroundView + private let navigationBarSeparator: SimpleLayer + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let closeButton = ComponentView() + + private let peerAvatar = ComponentView() + + private let callIconBackground = ComponentView() + private let callIcon = ComponentView() + + private let title = ComponentView() + private var subtitle: ComponentView? + private var descriptionText: ComponentView? + + private var contentSeparator: SimpleLayer? + private var previewPeersText: ComponentView? + private var previewPeersAvatarsNode: AnimatedAvatarSetNode? + private var previewPeersAvatarsContext: AnimatedAvatarSetContext? + + private let titleTransformContainer: UIView + private let bottomPanelContainer: UIView + private let actionButton = ComponentView() + private let bottomText = ComponentView() + + private let bottomOverscrollLimit: CGFloat + + private var isFirstTimeApplyingModalFactor: Bool = true + private var ignoreScrolling: Bool = false + + private var component: JoinSubjectScreenComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var isUpdating: Bool = false + + private var itemLayout: ItemLayout? + private var topOffsetDistance: CGFloat? + + private var cachedCloseImage: UIImage? + + private var isJoining: Bool = false + private var joinDisposable: Disposable? + + override init(frame: CGRect) { + self.bottomOverscrollLimit = 200.0 + + self.dimView = UIView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 10.0 + + self.navigationBarContainer = SparseContainerView() + + self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.navigationBarSeparator = SimpleLayer() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.titleTransformContainer = UIView() + self.titleTransformContainer.isUserInteractionEnabled = false + + self.bottomPanelContainer = UIView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.addSubview(self.navigationBarContainer) + self.addSubview(self.titleTransformContainer) + self.addSubview(self.bottomPanelContainer) + + self.navigationBarContainer.addSubview(self.navigationBackgroundView) + self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.joinDisposable?.dispose() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func updateScrolling(transition: ComponentTransition) { + guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + + let titleCenterY: CGFloat = -itemLayout.topInset + itemLayout.containerInset + 54.0 * 0.5 + + let titleTransformDistance: CGFloat = 20.0 + let titleY: CGFloat = max(titleCenterY, self.titleTransformContainer.center.y + topOffset + itemLayout.containerInset) + + transition.setSublayerTransform(view: self.titleTransformContainer, transform: CATransform3DMakeTranslation(0.0, titleY - self.titleTransformContainer.center.y, 0.0)) + + let titleYDistance: CGFloat = titleY - titleCenterY + let titleTransformFraction: CGFloat = 1.0 - max(0.0, min(1.0, titleYDistance / titleTransformDistance)) + let titleMinScale: CGFloat = 17.0 / 24.0 + let titleScale: CGFloat = 1.0 * (1.0 - titleTransformFraction) + titleMinScale * titleTransformFraction + if let titleView = self.title.view { + transition.setScale(view: titleView, scale: titleScale) + } + + let navigationAlpha: CGFloat = titleTransformFraction + transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) + transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) + + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) + self.topOffsetDistance = topOffsetDistance + var topOffsetFraction = topOffset / topOffsetDistance + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let transitionFactor: CGFloat = 1.0 - topOffsetFraction + var modalOverlayTransition = transition + if self.isFirstTimeApplyingModalFactor { + self.isFirstTimeApplyingModalFactor = false + modalOverlayTransition = .spring(duration: 0.5) + } + if self.isUpdating { + DispatchQueue.main.async { [weak controller] in + guard let controller else { + return + } + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition) + } + } else { + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition) + } + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.titleTransformContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.bottomPanelContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.titleTransformContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.bottomPanelContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + + if let environment = self.environment, let controller = environment.controller() { + controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + + private func navigateToPeer(peer: EnginePeer) { + guard let component = self.component else { + return + } + guard let controller = self.environment?.controller() else { + return + } + guard let navigationController = controller.navigationController as? NavigationController else { + return + } + var viewControllers = navigationController.viewControllers + guard let index = viewControllers.firstIndex(where: { $0 === controller }) else { + return + } + + let context = component.context + + if case .user = peer { + if let peerInfoController = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + viewControllers.insert(peerInfoController, at: index) + } + } else { + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + viewControllers.insert(chatController, at: index) + } + navigationController.setViewControllers(viewControllers, animated: true) + controller.dismiss() + } + + private func performJoinAction() { + if self.isJoining { + return + } + guard let component = self.component else { + return + } + + switch component.mode { + case let .group(group): + self.joinDisposable?.dispose() + + self.isJoining = true + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + + self.joinDisposable = (component.context.engine.peers.joinChatInteractively(with: group.link) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let component = self.component else { + return + } + if group.isRequest { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .inviteRequestSent(title: presentationData.strings.MemberRequests_RequestToJoinSent, text: group.isGroup ? presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } else { + if let peer { + self.navigateToPeer(peer: peer) + } + } + self.environment?.controller()?.dismiss() + }, error: { [weak self] error in + guard let self, let component = self.component else { + return + } + + self.isJoining = false + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + switch error { + case .tooMuchJoined: + if let parentNavigationController = self.environment?.controller()?.navigationController as? NavigationController { + let context = component.context + parentNavigationController.pushViewController(component.context.sharedContext.makeOldChannelsController(context: component.context, updatedPresentationData: nil, intent: .join, completed: { [weak parentNavigationController] value in + if value { + parentNavigationController?.pushViewController(JoinSubjectScreen(context: context, mode: .group(group))) + } + })) + } else { + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: presentationData.strings.Join_ChannelsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + case .tooMuchUsers: + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: presentationData.strings.Conversation_UsersTooMuchError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + case .requestSent: + if group.isRequest { + self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .inviteRequestSent(title: presentationData.strings.MemberRequests_RequestToJoinSent, text: group.isGroup ? presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + case .flood: + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: presentationData.strings.TwoStepAuth_FloodError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + case .generic: + break + } + self.environment?.controller()?.dismiss() + }) + case let .groupCall(groupCall): + let _ = groupCall + self.environment?.controller()?.dismiss() + } + } + + func update(component: JoinSubjectScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + if self.component == nil { + } + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor + + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var contentHeight: CGFloat = 0.0 + + let closeImage: UIImage + if let image = self.cachedCloseImage, !themeUpdated { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))! + self.cachedCloseImage = closeImage + } + + let closeButtonSize = self.closeButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image(image: closeImage, size: closeImage.size)), + action: { [weak self] in + guard let self, let controller = self.environment?.controller() else { + return + } + controller.dismiss() + } + ).minSize(CGSize(width: 62.0, height: 56.0))), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - closeButtonSize.width, y: 0.0), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + let clippingY: CGFloat + + let titleString: String + let subtitleString: String? + let descriptionTextString: String? + let previewPeers: [EnginePeer] + let totalMemberCount: Int + + switch component.mode { + case let .group(group): + contentHeight += 31.0 + + titleString = group.title + subtitleString = group.isPublic ? "public group" : "private group" + descriptionTextString = group.about + + previewPeers = group.members + totalMemberCount = Int(group.memberCount) + + let peerAvatarSize = self.peerAvatar.update( + transition: transition, + component: AnyComponent(AvatarComponent( + context: component.context, + peer: EnginePeer.legacyGroup(TelegramGroup( + id: EnginePeer.Id(namespace: Namespaces.Peer.CloudGroup, id: EnginePeer.Id.Id._internalFromInt64Value(1)), + title: group.title, + photo: group.image.flatMap { image in + [image] + } ?? [], + participantCount: 0, + role: .member, + membership: .Left, + flags: [], + defaultBannedRights: nil, + migrationReference: nil, + creationDate: 0, + version: 0 + )) + )), + environment: {}, + containerSize: CGSize(width: 90.0, height: 90.0) + ) + let peerAvatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - peerAvatarSize.width) * 0.5), y: contentHeight), size: peerAvatarSize) + if let peerAvatarView = self.peerAvatar.view { + if peerAvatarView.superview == nil { + self.scrollContentView.addSubview(peerAvatarView) + } + transition.setFrame(view: peerAvatarView, frame: peerAvatarFrame) + } + contentHeight += peerAvatarSize.height + 21.0 + case let .groupCall(groupCall): + //TODO:localize + titleString = "Group Call" + subtitleString = nil + descriptionTextString = "You are invited to join a group call." + + previewPeers = groupCall.members + totalMemberCount = groupCall.totalMemberCount + + contentHeight += 31.0 + + let callIconBackgroundSize = self.callIconBackground.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: environment.theme.list.itemCheckColors.fillColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: CGSize(width: 90.0, height: 90.0) + ) + let callIconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - callIconBackgroundSize.width) * 0.5), y: contentHeight), size: callIconBackgroundSize) + if let callIconBackgroundView = self.callIconBackground.view { + if callIconBackgroundView.superview == nil { + self.scrollContentView.addSubview(callIconBackgroundView) + } + transition.setFrame(view: callIconBackgroundView, frame: callIconBackgroundFrame) + } + + let callIconSize = self.callIcon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Call/CallAcceptButton", + tintColor: environment.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 1.1 + )), + environment: {}, + containerSize: callIconBackgroundSize + ) + let callIconFrame = CGRect(origin: CGPoint(x: callIconBackgroundFrame.minX + floor((callIconBackgroundFrame.width - callIconSize.width) * 0.5), y: callIconBackgroundFrame.minY + floor((callIconBackgroundFrame.height - callIconSize.height) * 0.5)), size: callIconSize) + if let callIconView = self.callIcon.view { + if callIconView.superview == nil { + self.scrollContentView.addSubview(callIconView) + } + transition.setFrame(view: callIconView, frame: callIconFrame) + } + contentHeight += callIconBackgroundSize.height + 21.0 + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.bold(24.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) + if let titleView = title.view { + if titleView.superview == nil { + self.titleTransformContainer.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + transition.setPosition(view: self.titleTransformContainer, position: titleFrame.center) + } + contentHeight += titleSize.height + 4.0 + + let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) + transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) + self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + if let subtitleString { + let subtitle: ComponentView + if let current = self.subtitle { + subtitle = current + } else { + subtitle = ComponentView() + self.subtitle = subtitle + } + + let subtitleSize = subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: subtitleString, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemSecondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ) + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = subtitle.view { + if subtitleView.superview == nil { + self.scrollContentView.addSubview(subtitleView) + } + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + } + contentHeight += subtitleSize.height + } else if let subtitle = self.subtitle { + self.subtitle = nil + subtitle.view?.removeFromSuperview() + } + + if let descriptionTextString { + contentHeight += 10.0 + let descriptionText: ComponentView + if let current = self.descriptionText { + descriptionText = current + } else { + descriptionText = ComponentView() + self.descriptionText = descriptionText + } + + let descriptionTextSize = descriptionText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .markdown( + text: descriptionTextString, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ) + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) + if let descriptionTextView = descriptionText.view { + if descriptionTextView.superview == nil { + self.scrollContentView.addSubview(descriptionTextView) + } + transition.setPosition(view: descriptionTextView, position: descriptionTextFrame.center) + descriptionTextView.bounds = CGRect(origin: CGPoint(), size: descriptionTextFrame.size) + } + contentHeight += descriptionTextSize.height + } else if let descriptionText = self.descriptionText { + self.descriptionText = nil + descriptionText.view?.removeFromSuperview() + } + + if !previewPeers.isEmpty { + contentHeight += 11.0 + + //TODO:localize + let previewPeersString: String + switch component.mode { + case .group: + if previewPeers.count == 1 { + previewPeersString = "**\(previewPeers[0].compactDisplayTitle)** already joined this group." + } else { + let firstPeers = previewPeers.prefix(upTo: 2) + let peersTextArray = firstPeers.map { "**\($0.compactDisplayTitle)**" } + var peersText = "" + if #available(iOS 13.0, *) { + let listFormatter = ListFormatter() + listFormatter.locale = localeWithStrings(environment.strings) + if let value = listFormatter.string(from: peersTextArray) { + peersText = value + } + } + if peersText.isEmpty { + for i in 0 ..< peersTextArray.count { + if i != 0 { + peersText.append(", ") + } + peersText.append(peersTextArray[i]) + } + } + if totalMemberCount > firstPeers.count { + previewPeersString = "\(peersText) and **\(totalMemberCount - firstPeers.count)** other people already joined this group." + } else { + previewPeersString = "\(peersText) already joined this group." + } + } + case .groupCall: + if previewPeers.count == 1 { + previewPeersString = "**\(previewPeers[0].compactDisplayTitle)** already joined this call." + } else { + let firstPeers = previewPeers.prefix(upTo: 2) + let peersTextArray = firstPeers.map { "**\($0.compactDisplayTitle)**" } + var peersText = "" + if #available(iOS 13.0, *) { + let listFormatter = ListFormatter() + listFormatter.locale = localeWithStrings(environment.strings) + if let value = listFormatter.string(from: peersTextArray) { + peersText = value + } + } + if peersText.isEmpty { + for i in 0 ..< peersTextArray.count { + if i != 0 { + peersText.append(", ") + } + peersText.append(peersTextArray[i]) + } + } + if totalMemberCount > firstPeers.count { + previewPeersString = "\(peersText) and **\(totalMemberCount - firstPeers.count)** other people already joined this call." + } else { + previewPeersString = "\(peersText) already joined this call." + } + } + } + + let contentSeparator: SimpleLayer + if let current = self.contentSeparator { + contentSeparator = current + } else { + contentSeparator = SimpleLayer() + self.contentSeparator = contentSeparator + self.scrollContentView.layer.addSublayer(contentSeparator) + } + + if themeUpdated { + contentSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + } + + contentHeight += 8.0 + transition.setFrame(layer: contentSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel))) + contentHeight += 10.0 + + let previewPeersAvatarsNode: AnimatedAvatarSetNode + let previewPeersAvatarsContext: AnimatedAvatarSetContext + if let current = self.previewPeersAvatarsNode, let currentContext = self.previewPeersAvatarsContext { + previewPeersAvatarsNode = current + previewPeersAvatarsContext = currentContext + } else { + previewPeersAvatarsNode = AnimatedAvatarSetNode() + previewPeersAvatarsContext = AnimatedAvatarSetContext() + self.previewPeersAvatarsNode = previewPeersAvatarsNode + self.previewPeersAvatarsContext = previewPeersAvatarsContext + } + + let avatarsContent = previewPeersAvatarsContext.update(peers: previewPeers.count <= 3 ? previewPeers : Array(previewPeers.prefix(upTo: 3)), animated: false) + let avatarsSize = previewPeersAvatarsNode.update( + context: component.context, + content: avatarsContent, + itemSize: CGSize(width: 40.0, height: 40.0), + customSpacing: 24.0, + font: avatarPlaceholderFont(size: 18.0), + animated: false, + synchronousLoad: true + ) + contentHeight += 8.0 + let previewPeersAvatarsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarsSize.width) * 0.5), y: contentHeight), size: avatarsSize) + if previewPeersAvatarsNode.view.superview == nil { + self.scrollContentView.addSubview(previewPeersAvatarsNode.view) + } + transition.setFrame(view: previewPeersAvatarsNode.view, frame: previewPeersAvatarsFrame) + + contentHeight += 53.0 + + let previewPeersText: ComponentView + if let current = self.previewPeersText { + previewPeersText = current + } else { + previewPeersText = ComponentView() + self.previewPeersText = previewPeersText + } + let previewPeersTextSize = previewPeersText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .markdown( + text: previewPeersString, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ) + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let previewPeersTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - previewPeersTextSize.width) * 0.5), y: contentHeight), size: previewPeersTextSize) + if let previewPeersTextView = previewPeersText.view { + if previewPeersTextView.superview == nil { + self.scrollContentView.addSubview(previewPeersTextView) + } + transition.setFrame(view: previewPeersTextView, frame: previewPeersTextFrame) + } + contentHeight += previewPeersTextSize.height + 23.0 + } else { + contentHeight += 18.0 + + if let contentSeparator = self.contentSeparator { + self.contentSeparator = nil + contentSeparator.removeFromSuperlayer() + } + if let previewPeersText = self.previewPeersText { + self.previewPeersText = nil + previewPeersText.view?.removeFromSuperview() + } + } + + let actionButtonTitle: String + switch component.mode { + case .group: + actionButtonTitle = environment.strings.Invitation_JoinGroup + case .groupCall: + //TODO:localize + actionButtonTitle = "Join Group Call" + } + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: actionButtonTitle, + badge: 0, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, + badgeForeground: environment.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: self.isJoining, + action: { [weak self] in + guard let self else { + return + } + self.performJoinAction() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + + let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height + + let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight)) + transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame) + + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.bottomPanelContainer.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + contentHeight += bottomPanelHeight + + clippingY = bottomPanelFrame.minY - 8.0 + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + let previousBounds = self.scrollView.bounds + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } else { + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class JoinSubjectScreen: ViewControllerComponentContainer { + private let context: AccountContext + private var isDismissed: Bool = false + + public init( + context: AccountContext, + mode: JoinSubjectScreenMode + ) { + self.context = context + + super.init(context: context, component: JoinSubjectScreenComponent( + context: context, + mode: mode + ), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if let componentView = self.node.hostView.componentView as? JoinSubjectScreenComponent.View { + componentView.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? JoinSubjectScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private final class AvatarComponent: Component { + let context: AccountContext + let peer: EnginePeer + let size: CGSize? + + init(context: AccountContext, peer: EnginePeer, size: CGSize? = nil) { + self.context = context + self.peer = peer + self.size = size + } + + static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + + final class View: UIView { + private var avatarNode: AvatarNode? + + private var component: AvatarComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let size = component.size ?? availableSize + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(size.width * 0.5))) + avatarNode.displaysAsynchronously = false + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + avatarNode.frame = CGRect(origin: CGPoint(), size: size) + avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: true, + displayDimensions: size + ) + avatarNode.updateSize(size: size) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +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() + }) +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c4ae4a17fe..7e9eac1521 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift index 7b29fcea7a..ac6b3e26b2 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift @@ -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)? = nil, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void = { _ in }) -> ViewController { let initialState = OldChannelsState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 6e2a4b01cd..de7ecb7979 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -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 { 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) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e3c287fa6e..4275334cee 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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)?, 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) diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index bb08d239cc..9bfb55e49c 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -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, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @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, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @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) }) } diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index cf5002ad5f..04b6b0d376 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -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 { diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index ca6be80c5a..f9bd9ab71e 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -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; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index c3333d6282..4752a7b1ff 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -127,7 +127,7 @@ public: public: virtual rtc::scoped_refptr audioDeviceModule() = 0; - virtual rtc::scoped_refptr makeChildAudioDeviceModule() = 0; + virtual rtc::scoped_refptr 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 _audioTransports; + std::vector> _audioTransports; webrtc::Mutex _mutex; std::vector _mixAudioSamples; }; class WrappedChildAudioDeviceModule : public tgcalls::DefaultWrappedAudioDeviceModule { public: - WrappedChildAudioDeviceModule(webrtc::scoped_refptr impl) : - tgcalls::DefaultWrappedAudioDeviceModule(impl) { + WrappedChildAudioDeviceModule(webrtc::scoped_refptr 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 makeChildAudioDeviceModule() override { - return rtc::make_ref_counted(_audioDeviceModule); + rtc::scoped_refptr makeChildAudioDeviceModule(bool isActive) override { + return rtc::make_ref_counted(_audioDeviceModule, isActive); } virtual void start() override { @@ -1928,8 +1946,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; }, .createWrappedAudioDeviceModule = [audioDeviceModule](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr { 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 { + .createWrappedAudioDeviceModule = [audioDeviceModule, isActiveByDefault](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr { 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 diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index bc8334224d..29862dd620 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit bc8334224dbefb4591d669f7569d16f69134c5b6 +Subproject commit 29862dd6202e5f71db38e9722fecfd0ab3078268 diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 6f294faddc..006982c311 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -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):