[WIP] Video chat UI

This commit is contained in:
Isaac 2024-09-10 21:32:34 +08:00
parent ca5b6c0f0b
commit 5aa7784d2c
8 changed files with 452 additions and 41 deletions

View File

@ -102,7 +102,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case playlistPlayback(Bool) case playlistPlayback(Bool)
case enableQuickReactionSwitch(Bool) case enableQuickReactionSwitch(Bool)
case disableReloginTokens(Bool) case disableReloginTokens(Bool)
case callV2(Bool) case disableCallV2(Bool)
case experimentalCallMute(Bool) case experimentalCallMute(Bool)
case liveStreamV2(Bool) case liveStreamV2(Bool)
case preferredVideoCodec(Int, String, String?, Bool) case preferredVideoCodec(Int, String, String?, Bool)
@ -129,7 +129,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.web.rawValue return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .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 return DebugControllerSection.experiments.rawValue
case .logTranslationRecognition, .resetTranslationStates: case .logTranslationRecognition, .resetTranslationStates:
return DebugControllerSection.translation.rawValue return DebugControllerSection.translation.rawValue
@ -242,7 +242,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 49 return 49
case .enableQuickReactionSwitch: case .enableQuickReactionSwitch:
return 50 return 50
case .callV2: case .disableCallV2:
return 51 return 51
case .experimentalCallMute: case .experimentalCallMute:
return 52 return 52
@ -1318,12 +1318,12 @@ private enum DebugControllerEntry: ItemListNodeEntry {
}) })
}).start() }).start()
}) })
case let .callV2(value): case let .disableCallV2(value):
return ItemListSwitchItem(presentationData: presentationData, title: "[WIP] Video Chat V2", value: value, sectionId: self.section, style: .blocks, updated: { value in 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 let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
settings.callV2 = value settings.disableCallV2 = value
return PreferencesEntry(settings) return PreferencesEntry(settings)
}) })
}).start() }).start()
@ -1502,7 +1502,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
} }
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
entries.append(.callV2(experimentalSettings.callV2)) entries.append(.disableCallV2(experimentalSettings.disableCallV2))
entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute)) entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute))
entries.append(.liveStreamV2(experimentalSettings.liveStreamV2)) entries.append(.liveStreamV2(experimentalSettings.liveStreamV2))
} }

View File

@ -9,13 +9,16 @@ import BundleIconComponent
final class VideoChatListInviteComponent: Component { final class VideoChatListInviteComponent: Component {
let title: String let title: String
let theme: PresentationTheme let theme: PresentationTheme
let action: () -> Void
init( init(
title: String, title: String,
theme: PresentationTheme theme: PresentationTheme,
action: @escaping () -> Void
) { ) {
self.title = title self.title = title
self.theme = theme self.theme = theme
self.action = action
} }
static func ==(lhs: VideoChatListInviteComponent, rhs: VideoChatListInviteComponent) -> Bool { static func ==(lhs: VideoChatListInviteComponent, rhs: VideoChatListInviteComponent) -> Bool {
@ -28,21 +31,61 @@ final class VideoChatListInviteComponent: Component {
return true return true
} }
final class View: UIView { final class View: HighlightTrackingButton {
private let icon = ComponentView<Empty>() private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>() private let title = ComponentView<Empty>()
private var component: VideoChatListInviteComponent? private var component: VideoChatListInviteComponent?
private var isUpdating: Bool = false private var isUpdating: Bool = false
private var highlightBackgroundLayer: SimpleLayer?
private var highlightBackgroundFrame: CGRect?
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) 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) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") 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<Empty>, transition: ComponentTransition) -> CGSize { func update(component: VideoChatListInviteComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { 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) 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 let titleView = self.title.view {
if titleView.superview == nil { if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
titleView.layer.anchorPoint = CGPoint() titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView) 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) 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 let iconView = self.icon.view {
if iconView.superview == nil { if iconView.superview == nil {
iconView.isUserInteractionEnabled = false
self.addSubview(iconView) self.addSubview(iconView)
} }
transition.setFrame(view: iconView, frame: iconFrame) transition.setFrame(view: iconView, frame: iconFrame)
} }
//self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: size)
return size return size
} }
} }

View File

@ -415,6 +415,7 @@ final class VideoChatParticipantVideoComponent: Component {
self.loadingEffectView = loadingEffectView self.loadingEffectView = loadingEffectView
self.addSubview(loadingEffectView.view) self.addSubview(loadingEffectView.view)
rootVideoLoadingEffectView.portalSource.addPortal(view: loadingEffectView) rootVideoLoadingEffectView.portalSource.addPortal(view: loadingEffectView)
loadingEffectView.view.isUserInteractionEnabled = false
loadingEffectView.view.frame = CGRect(origin: CGPoint(), size: availableSize) loadingEffectView.view.frame = CGRect(origin: CGPoint(), size: availableSize)
} }
} }

View File

@ -119,6 +119,7 @@ final class VideoChatParticipantsComponent: Component {
let updateMainParticipant: (VideoParticipantKey?) -> Void let updateMainParticipant: (VideoParticipantKey?) -> Void
let updateIsMainParticipantPinned: (Bool) -> Void let updateIsMainParticipantPinned: (Bool) -> Void
let updateIsExpandedUIHidden: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void
let openInviteMembers: () -> Void
init( init(
call: PresentationGroupCall, call: PresentationGroupCall,
@ -134,7 +135,8 @@ final class VideoChatParticipantsComponent: Component {
openParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void, openParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void,
updateMainParticipant: @escaping (VideoParticipantKey?) -> Void, updateMainParticipant: @escaping (VideoParticipantKey?) -> Void,
updateIsMainParticipantPinned: @escaping (Bool) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void,
updateIsExpandedUIHidden: @escaping (Bool) -> Void updateIsExpandedUIHidden: @escaping (Bool) -> Void,
openInviteMembers: @escaping () -> Void
) { ) {
self.call = call self.call = call
self.participants = participants self.participants = participants
@ -150,6 +152,7 @@ final class VideoChatParticipantsComponent: Component {
self.updateMainParticipant = updateMainParticipant self.updateMainParticipant = updateMainParticipant
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
self.openInviteMembers = openInviteMembers
} }
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool { 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 { private final class VideoParticipant: Equatable {
let participant: GroupCallParticipantsContext.Participant let participant: GroupCallParticipantsContext.Participant
let isPresentation: Bool let isPresentation: Bool
@ -579,6 +590,7 @@ final class VideoChatParticipantsComponent: Component {
private let separateVideoScrollView: ScrollView private let separateVideoScrollView: ScrollView
private var component: VideoChatParticipantsComponent? private var component: VideoChatParticipantsComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false private var isUpdating: Bool = false
private var ignoreScrolling: Bool = false private var ignoreScrolling: Bool = false
@ -602,6 +614,7 @@ final class VideoChatParticipantsComponent: Component {
private let listItemsBackground = ComponentView<Empty>() private let listItemsBackground = ComponentView<Empty>()
private var itemLayout: ItemLayout? private var itemLayout: ItemLayout?
private var expandedGridSwipeState: ExpandedGridSwipeState?
private var appliedGridIsEmpty: Bool = true private var appliedGridIsEmpty: Bool = true
@ -664,6 +677,8 @@ final class VideoChatParticipantsComponent: Component {
self.scrollView.addSubview(self.listItemViewContainer) self.scrollView.addSubview(self.listItemViewContainer)
self.addSubview(self.expandedGridItemContainer) self.addSubview(self.expandedGridItemContainer)
self.expandedGridItemContainer.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.expandedGridPanGesture(_:))))
} }
required init?(coder: NSCoder) { 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) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling { if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate) self.updateScrolling(transition: .immediate)
@ -726,6 +770,9 @@ final class VideoChatParticipantsComponent: Component {
var expandedGridItemContainerFrame: CGRect var expandedGridItemContainerFrame: CGRect
if component.expandedVideoState != nil { if component.expandedVideoState != nil {
expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame() expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame()
if let expandedGridSwipeState = self.expandedGridSwipeState {
expandedGridItemContainerFrame.origin.y += expandedGridSwipeState.fraction * itemLayout.containerSize.height
}
} else { } else {
if let videoColumn = itemLayout.layout.videoColumn { 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) 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 self.component = component
self.state = state
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)
}
let measureListItemSize = self.measureListItemView.update( let measureListItemSize = self.measureListItemView.update(
transition: .immediate, transition: .immediate,
@ -1371,7 +1397,13 @@ final class VideoChatParticipantsComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(VideoChatListInviteComponent( component: AnyComponent(VideoChatListInviteComponent(
title: "Invite Members", 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: {}, environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0) 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 let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) {
if !requestedVideo.contains(videoChannel) { if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel) requestedVideo.append(videoChannel)

View File

@ -22,6 +22,8 @@ import ShareController
import AvatarNode import AvatarNode
import TelegramAudio import TelegramAudio
import PeerInfoUI
import DeleteChatPeerActionSheetItem import DeleteChatPeerActionSheetItem
import PeerListItemComponent import PeerListItemComponent
import LegacyComponents import LegacyComponents
@ -105,6 +107,7 @@ private final class VideoChatScreenComponent: Component {
private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState? private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState?
private let inviteDisposable = MetaDisposable()
private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil) private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
private let updateAvatarDisposable = MetaDisposable() private let updateAvatarDisposable = MetaDisposable()
private var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)? private var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)?
@ -138,6 +141,7 @@ private final class VideoChatScreenComponent: Component {
self.audioOutputStateDisposable?.dispose() self.audioOutputStateDisposable?.dispose()
self.inviteLinksDisposable?.dispose() self.inviteLinksDisposable?.dispose()
self.updateAvatarDisposable.dispose() self.updateAvatarDisposable.dispose()
self.inviteDisposable.dispose()
} }
func animateIn() { 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) 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<Never, NoError> { [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<Never, NoError> { [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) { private func presentShare(_ inviteLinks: GroupCallInviteLinks) {
guard let component = self.component else { guard let component = self.component else {
return return
@ -2198,6 +2501,12 @@ private final class VideoChatScreenComponent: Component {
self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState
self.state?.updated(transition: .spring(duration: 0.4)) self.state?.updated(transition: .spring(duration: 0.4))
} }
},
openInviteMembers: { [weak self] in
guard let self else {
return
}
self.openInviteMembers()
} }
)), )),
environment: {}, environment: {},

View File

@ -45,6 +45,8 @@ final class VideoChatVideoLoadingEffectView: UIView {
super.init(frame: .zero) super.init(frame: .zero)
self.portalSource.backgroundColor = .red
self.portalSource.layer.addSublayer(self.hierarchyTrackingLayer) self.portalSource.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let self, self.bounds.width != 0.0 else { guard let self, self.bounds.width != 0.0 else {

View File

@ -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<Any, NoError> { public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal<Any, NoError> {
if sharedContext.immediateExperimentalUISettings.callV2 { let useV2 = calculateUseV2(context: accountContext)
if useV2 {
return VideoChatScreenV2Impl.initialData(call: call) |> map { $0 as Any } return VideoChatScreenV2Impl.initialData(call: call) |> map { $0 as Any }
} else { } else {
return .single(Void()) return .single(Void())
@ -7106,7 +7119,9 @@ public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountConte
} }
public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any) -> VoiceChatController { 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) return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call)
} else { } else {
return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call) return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call)

View File

@ -54,7 +54,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var storiesJpegExperiment: Bool public var storiesJpegExperiment: Bool
public var crashOnMemoryPressure: Bool public var crashOnMemoryPressure: Bool
public var dustEffect: Bool public var dustEffect: Bool
public var callV2: Bool public var disableCallV2: Bool
public var experimentalCallMute: Bool public var experimentalCallMute: Bool
public var allowWebViewInspection: Bool public var allowWebViewInspection: Bool
public var disableReloginTokens: Bool public var disableReloginTokens: Bool
@ -91,7 +91,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
storiesJpegExperiment: false, storiesJpegExperiment: false,
crashOnMemoryPressure: false, crashOnMemoryPressure: false,
dustEffect: false, dustEffect: false,
callV2: false, disableCallV2: false,
experimentalCallMute: false, experimentalCallMute: false,
allowWebViewInspection: false, allowWebViewInspection: false,
disableReloginTokens: false, disableReloginTokens: false,
@ -129,7 +129,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
storiesJpegExperiment: Bool, storiesJpegExperiment: Bool,
crashOnMemoryPressure: Bool, crashOnMemoryPressure: Bool,
dustEffect: Bool, dustEffect: Bool,
callV2: Bool, disableCallV2: Bool,
experimentalCallMute: Bool, experimentalCallMute: Bool,
allowWebViewInspection: Bool, allowWebViewInspection: Bool,
disableReloginTokens: Bool, disableReloginTokens: Bool,
@ -164,7 +164,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.storiesJpegExperiment = storiesJpegExperiment self.storiesJpegExperiment = storiesJpegExperiment
self.crashOnMemoryPressure = crashOnMemoryPressure self.crashOnMemoryPressure = crashOnMemoryPressure
self.dustEffect = dustEffect self.dustEffect = dustEffect
self.callV2 = callV2 self.disableCallV2 = disableCallV2
self.experimentalCallMute = experimentalCallMute self.experimentalCallMute = experimentalCallMute
self.allowWebViewInspection = allowWebViewInspection self.allowWebViewInspection = allowWebViewInspection
self.disableReloginTokens = disableReloginTokens self.disableReloginTokens = disableReloginTokens
@ -203,7 +203,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false
self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false
self.dustEffect = try container.decodeIfPresent(Bool.self, forKey: "dustEffect") ?? 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.experimentalCallMute = try container.decodeIfPresent(Bool.self, forKey: "experimentalCallMute") ?? false
self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false
self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? 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.storiesJpegExperiment, forKey: "storiesJpegExperiment")
try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure") try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure")
try container.encode(self.dustEffect, forKey: "dustEffect") 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.experimentalCallMute, forKey: "experimentalCallMute")
try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection") try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection")
try container.encode(self.disableReloginTokens, forKey: "disableReloginTokens") try container.encode(self.disableReloginTokens, forKey: "disableReloginTokens")