diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 3b4b859f7b..b42a95bdb1 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -102,7 +102,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case playlistPlayback(Bool) case enableQuickReactionSwitch(Bool) case disableReloginTokens(Bool) - case callV2(Bool) + case disableCallV2(Bool) case experimentalCallMute(Bool) case liveStreamV2(Bool) case preferredVideoCodec(Int, String, String?, Bool) @@ -129,7 +129,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .callV2, .experimentalCallMute, .liveStreamV2: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .liveStreamV2: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -242,7 +242,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 49 case .enableQuickReactionSwitch: return 50 - case .callV2: + case .disableCallV2: return 51 case .experimentalCallMute: return 52 @@ -1318,12 +1318,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .callV2(value): - return ItemListSwitchItem(presentationData: presentationData, title: "[WIP] Video Chat V2", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .disableCallV2(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Disable Video Chat V2", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings - settings.callV2 = value + settings.disableCallV2 = value return PreferencesEntry(settings) }) }).start() @@ -1502,7 +1502,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present } entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) - entries.append(.callV2(experimentalSettings.callV2)) + entries.append(.disableCallV2(experimentalSettings.disableCallV2)) entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute)) entries.append(.liveStreamV2(experimentalSettings.liveStreamV2)) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift index 5f46cdf23a..5e25476df2 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift @@ -9,13 +9,16 @@ import BundleIconComponent final class VideoChatListInviteComponent: Component { let title: String let theme: PresentationTheme + let action: () -> Void init( title: String, - theme: PresentationTheme + theme: PresentationTheme, + action: @escaping () -> Void ) { self.title = title self.theme = theme + self.action = action } static func ==(lhs: VideoChatListInviteComponent, rhs: VideoChatListInviteComponent) -> Bool { @@ -28,21 +31,61 @@ final class VideoChatListInviteComponent: Component { return true } - final class View: UIView { + final class View: HighlightTrackingButton { private let icon = ComponentView() private let title = ComponentView() private var component: VideoChatListInviteComponent? private var isUpdating: Bool = false + private var highlightBackgroundLayer: SimpleLayer? + private var highlightBackgroundFrame: CGRect? + override init(frame: CGRect) { super.init(frame: frame) + + self.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, at: 0) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + func update(component: VideoChatListInviteComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -65,6 +108,7 @@ final class VideoChatListInviteComponent: Component { let titleFrame = CGRect(origin: CGPoint(x: 62.0, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { + titleView.isUserInteractionEnabled = false titleView.layer.anchorPoint = CGPoint() self.addSubview(titleView) } @@ -84,11 +128,14 @@ final class VideoChatListInviteComponent: Component { let iconFrame = CGRect(origin: CGPoint(x: floor((62.0 - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) if let iconView = self.icon.view { if iconView.superview == nil { + iconView.isUserInteractionEnabled = false self.addSubview(iconView) } transition.setFrame(view: iconView, frame: iconFrame) } + //self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: size) + return size } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index 79569c9371..cde0ff9ad9 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -415,6 +415,7 @@ final class VideoChatParticipantVideoComponent: Component { self.loadingEffectView = loadingEffectView self.addSubview(loadingEffectView.view) rootVideoLoadingEffectView.portalSource.addPortal(view: loadingEffectView) + loadingEffectView.view.isUserInteractionEnabled = false loadingEffectView.view.frame = CGRect(origin: CGPoint(), size: availableSize) } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index c8574cda10..092dd64cda 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -119,6 +119,7 @@ final class VideoChatParticipantsComponent: Component { let updateMainParticipant: (VideoParticipantKey?) -> Void let updateIsMainParticipantPinned: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void + let openInviteMembers: () -> Void init( call: PresentationGroupCall, @@ -134,7 +135,8 @@ final class VideoChatParticipantsComponent: Component { openParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void, updateMainParticipant: @escaping (VideoParticipantKey?) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void, - updateIsExpandedUIHidden: @escaping (Bool) -> Void + updateIsExpandedUIHidden: @escaping (Bool) -> Void, + openInviteMembers: @escaping () -> Void ) { self.call = call self.participants = participants @@ -150,6 +152,7 @@ final class VideoChatParticipantsComponent: Component { self.updateMainParticipant = updateMainParticipant self.updateIsMainParticipantPinned = updateIsMainParticipantPinned self.updateIsExpandedUIHidden = updateIsExpandedUIHidden + self.openInviteMembers = openInviteMembers } static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool { @@ -527,6 +530,14 @@ final class VideoChatParticipantsComponent: Component { } } + private struct ExpandedGridSwipeState { + var fraction: CGFloat + + init(fraction: CGFloat) { + self.fraction = fraction + } + } + private final class VideoParticipant: Equatable { let participant: GroupCallParticipantsContext.Participant let isPresentation: Bool @@ -579,6 +590,7 @@ final class VideoChatParticipantsComponent: Component { private let separateVideoScrollView: ScrollView private var component: VideoChatParticipantsComponent? + private weak var state: EmptyComponentState? private var isUpdating: Bool = false private var ignoreScrolling: Bool = false @@ -602,6 +614,7 @@ final class VideoChatParticipantsComponent: Component { private let listItemsBackground = ComponentView() private var itemLayout: ItemLayout? + private var expandedGridSwipeState: ExpandedGridSwipeState? private var appliedGridIsEmpty: Bool = true @@ -664,6 +677,8 @@ final class VideoChatParticipantsComponent: Component { self.scrollView.addSubview(self.listItemViewContainer) self.addSubview(self.expandedGridItemContainer) + + self.expandedGridItemContainer.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.expandedGridPanGesture(_:)))) } required init?(coder: NSCoder) { @@ -692,6 +707,35 @@ final class VideoChatParticipantsComponent: Component { } } + @objc private func expandedGridPanGesture(_ recognizer: UIPanGestureRecognizer) { + guard let component = self.component else { + return + } + if self.bounds.height == 0.0 { + return + } + switch recognizer.state { + case .began, .changed: + let translation = recognizer.translation(in: self) + let fraction = translation.y / self.bounds.height + self.expandedGridSwipeState = ExpandedGridSwipeState(fraction: fraction) + self.state?.updated(transition: .immediate) + case .ended, .cancelled: + let translation = recognizer.translation(in: self) + let fraction = translation.y / self.bounds.height + self.expandedGridSwipeState = nil + + let velocity = recognizer.velocity(in: self) + if abs(velocity.y) > 100.0 || abs(fraction) >= 0.5 { + component.updateMainParticipant(nil) + } else { + self.state?.updated(transition: .spring(duration: 0.4)) + } + default: + break + } + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) @@ -726,6 +770,9 @@ final class VideoChatParticipantsComponent: Component { var expandedGridItemContainerFrame: CGRect if component.expandedVideoState != nil { expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame() + if let expandedGridSwipeState = self.expandedGridSwipeState { + expandedGridItemContainerFrame.origin.y += expandedGridSwipeState.fraction * itemLayout.containerSize.height + } } else { if let videoColumn = itemLayout.layout.videoColumn { expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: itemLayout.separateVideoScrollClippingFrame.minX, dy: 0.0).offsetBy(dx: 0.0, dy: -self.separateVideoScrollView.bounds.minY) @@ -1322,28 +1369,7 @@ final class VideoChatParticipantsComponent: Component { } self.component = component - - if !"".isEmpty { - let rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView - if let current = self.rootVideoLoadingEffectView { - rootVideoLoadingEffectView = current - } else { - rootVideoLoadingEffectView = VideoChatVideoLoadingEffectView( - effectAlpha: 0.1, - borderAlpha: 0.0, - gradientWidth: 260.0, - duration: 1.0, - hasCustomBorder: false, - playOnce: false - ) - self.rootVideoLoadingEffectView = rootVideoLoadingEffectView - self.insertSubview(rootVideoLoadingEffectView, at: 0) - rootVideoLoadingEffectView.alpha = 0.0 - rootVideoLoadingEffectView.isUserInteractionEnabled = false - } - - rootVideoLoadingEffectView.update(size: availableSize, transition: transition) - } + self.state = state let measureListItemSize = self.measureListItemView.update( transition: .immediate, @@ -1371,7 +1397,13 @@ final class VideoChatParticipantsComponent: Component { transition: transition, component: AnyComponent(VideoChatListInviteComponent( title: "Invite Members", - theme: component.theme + theme: component.theme, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.openInviteMembers() + } )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 1000.0) @@ -1464,6 +1496,11 @@ final class VideoChatParticipantsComponent: Component { } } + if component.layout.videoColumn != nil && gridParticipants.count == 1 { + maxVideoQuality = .full + maxPresentationQuality = .full + } + if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) { if !requestedVideo.contains(videoChannel) { requestedVideo.append(videoChannel) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index fcdf560bd5..3878d2077c 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -22,6 +22,8 @@ import ShareController import AvatarNode import TelegramAudio +import PeerInfoUI + import DeleteChatPeerActionSheetItem import PeerListItemComponent import LegacyComponents @@ -105,6 +107,7 @@ private final class VideoChatScreenComponent: Component { private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState? + private let inviteDisposable = MetaDisposable() private let currentAvatarMixin = Atomic(value: nil) private let updateAvatarDisposable = MetaDisposable() private var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)? @@ -138,6 +141,7 @@ private final class VideoChatScreenComponent: Component { self.audioOutputStateDisposable?.dispose() self.inviteLinksDisposable?.dispose() self.updateAvatarDisposable.dispose() + self.inviteDisposable.dispose() } func animateIn() { @@ -1423,6 +1427,305 @@ private final class VideoChatScreenComponent: Component { environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: action), in: .current) } + private func openInviteMembers() { + guard let component = self.component else { + return + } + + let groupPeer = component.call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.call.peerId)) + let _ = (groupPeer + |> deliverOnMainQueue).start(next: { [weak self] groupPeer in + guard let self, let component = self.component, let environment = self.environment, let groupPeer else { + return + } + let inviteLinks = self.inviteLinks + + if case let .channel(groupPeer) = groupPeer { + var canInviteMembers = true + if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) { + canInviteMembers = false + } + if !canInviteMembers { + if let inviteLinks { + self.presentShare(inviteLinks) + } + return + } + } + + var filters: [ChannelMembersSearchFilter] = [] + if let members = self.members { + filters.append(.disable(Array(members.participants.map { $0.peer.id }))) + } + if case let .channel(groupPeer) = groupPeer { + if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil { + filters.append(.excludeNonMembers) + } + } else if case let .legacyGroup(groupPeer) = groupPeer { + if groupPeer.hasBannedPermission(.banAddMembers) { + filters.append(.excludeNonMembers) + } + } + filters.append(.excludeBots) + + var dismissController: (() -> Void)? + let controller = ChannelMembersSearchController(context: component.call.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + guard let callState = self.callState else { + return + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + if peer.id == callState.myPeerId { + return + } + if let participant { + dismissController?() + + if component.call.invitePeer(participant.peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + } else { + if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { + let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in + dismissController?() + + guard let self, let component = self.component else { + return + } + + let _ = (enqueueMessages(account: component.call.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + }) + })]), in: .window(.root)) + } else { + let text: String + if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info { + text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } + + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + if case let .channel(groupPeer) = groupPeer { + guard let selfController = environment.controller() else { + return + } + let inviteDisposable = self.inviteDisposable + var inviteSignal = component.call.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: component.call.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id]) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { [weak selfController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + selfController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + inviteSignal = inviteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + inviteDisposable.set(nil) + } + + inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in + dismissController?() + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let text: String + switch error { + case .limitExceeded: + text = environment.strings.Channel_ErrorAddTooMuch + case .tooMuchJoined: + text = environment.strings.Invite_ChannelsTooMuch + case .generic: + text = environment.strings.Login_UnknownError + case .restricted: + text = environment.strings.Channel_ErrorAddBlocked + case .notMutualContact: + if case .broadcast = groupPeer.info { + text = environment.strings.Channel_AddUserLeftError + } else { + text = environment.strings.GroupInfo_AddUserLeftError + } + case .botDoesntSupportGroups: + text = environment.strings.Channel_BotDoesntSupportGroups + case .tooMuchBots: + text = environment.strings.Channel_TooMuchBots + case .bot: + text = environment.strings.Login_UnknownError + case .kicked: + text = environment.strings.Channel_AddUserKickedError + } + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + dismissController?() + + if component.call.invitePeer(peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + })) + } else if case let .legacyGroup(groupPeer) = groupPeer { + guard let selfController = environment.controller() else { + return + } + let inviteDisposable = self.inviteDisposable + var inviteSignal = component.call.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { [weak selfController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + selfController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + inviteSignal = inviteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + inviteDisposable.set(nil) + } + + inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in + dismissController?() + guard let self, let component = self.component, let environment = self.environment else { + return + } + let context = component.call.accountContext + + switch error { + case .privacy: + let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peer.id) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let component = self.component, let environment = self.environment else { + return + } + environment.controller()?.present(textAlertController(context: component.call.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + }) + case .notMutualContact: + environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + case .tooManyChannels: + environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + case .groupFull, .generic: + environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + } + }, completed: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + dismissController?() + + if component.call.invitePeer(peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + })) + } + })]), in: .window(.root)) + } + } + }) + controller.copyInviteLink = { [weak self] in + dismissController?() + + guard let self, let component = self.component else { + return + } + let callPeerId = component.call.peerId + + let _ = (component.call.accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId), + TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId) + ) + |> map { peer, exportedInvitation -> String? in + if let link = inviteLinks?.listenerLink { + return link + } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { + return "https://t.me/\(addressName)" + } else if let link = exportedInvitation?.link { + return link + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { [weak self] link in + guard let self, let environment = self.environment else { + return + } + + if let link { + UIPasteboard.general.string = link + + self.presentUndoOverlay(content: .linkCopied(text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false }) + } + }) + } + dismissController = { [weak controller] in + controller?.dismiss() + } + environment.controller()?.push(controller) + }) + } + private func presentShare(_ inviteLinks: GroupCallInviteLinks) { guard let component = self.component else { return @@ -2198,6 +2501,12 @@ private final class VideoChatScreenComponent: Component { self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState self.state?.updated(transition: .spring(duration: 0.4)) } + }, + openInviteMembers: { [weak self] in + guard let self else { + return + } + self.openInviteMembers() } )), environment: {}, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift b/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift index 67b9fc168b..8b81268148 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift @@ -45,6 +45,8 @@ final class VideoChatVideoLoadingEffectView: UIView { super.init(frame: .zero) + self.portalSource.backgroundColor = .red + self.portalSource.layer.addSublayer(self.hierarchyTrackingLayer) self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in guard let self, self.bounds.width != 0.0 else { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 0e1f1b0e2f..533e8b89ca 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7097,8 +7097,21 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } } +private func calculateUseV2(context: AccountContext) -> Bool { + var useV2 = true + if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { + useV2 = false + } + if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_videochatui_v2"] { + useV2 = false + } + return useV2 +} + public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal { - if sharedContext.immediateExperimentalUISettings.callV2 { + let useV2 = calculateUseV2(context: accountContext) + + if useV2 { return VideoChatScreenV2Impl.initialData(call: call) |> map { $0 as Any } } else { return .single(Void()) @@ -7106,7 +7119,9 @@ public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountConte } public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any) -> VoiceChatController { - if sharedContext.immediateExperimentalUISettings.callV2 { + let useV2 = calculateUseV2(context: accountContext) + + if useV2 { return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call) } else { return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call) diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 5640fefa00..cefaf5e573 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -54,7 +54,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var storiesJpegExperiment: Bool public var crashOnMemoryPressure: Bool public var dustEffect: Bool - public var callV2: Bool + public var disableCallV2: Bool public var experimentalCallMute: Bool public var allowWebViewInspection: Bool public var disableReloginTokens: Bool @@ -91,7 +91,7 @@ public struct ExperimentalUISettings: Codable, Equatable { storiesJpegExperiment: false, crashOnMemoryPressure: false, dustEffect: false, - callV2: false, + disableCallV2: false, experimentalCallMute: false, allowWebViewInspection: false, disableReloginTokens: false, @@ -129,7 +129,7 @@ public struct ExperimentalUISettings: Codable, Equatable { storiesJpegExperiment: Bool, crashOnMemoryPressure: Bool, dustEffect: Bool, - callV2: Bool, + disableCallV2: Bool, experimentalCallMute: Bool, allowWebViewInspection: Bool, disableReloginTokens: Bool, @@ -164,7 +164,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.storiesJpegExperiment = storiesJpegExperiment self.crashOnMemoryPressure = crashOnMemoryPressure self.dustEffect = dustEffect - self.callV2 = callV2 + self.disableCallV2 = disableCallV2 self.experimentalCallMute = experimentalCallMute self.allowWebViewInspection = allowWebViewInspection self.disableReloginTokens = disableReloginTokens @@ -203,7 +203,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false self.dustEffect = try container.decodeIfPresent(Bool.self, forKey: "dustEffect") ?? false - self.callV2 = try container.decodeIfPresent(Bool.self, forKey: "callV2") ?? false + self.disableCallV2 = try container.decodeIfPresent(Bool.self, forKey: "disableCallV2") ?? false self.experimentalCallMute = try container.decodeIfPresent(Bool.self, forKey: "experimentalCallMute") ?? false self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? false @@ -242,7 +242,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.storiesJpegExperiment, forKey: "storiesJpegExperiment") try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure") try container.encode(self.dustEffect, forKey: "dustEffect") - try container.encode(self.callV2, forKey: "callV2") + try container.encode(self.disableCallV2, forKey: "disableCallV2") try container.encode(self.experimentalCallMute, forKey: "experimentalCallMute") try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection") try container.encode(self.disableReloginTokens, forKey: "disableReloginTokens")