mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Video chat UI
This commit is contained in:
parent
ca5b6c0f0b
commit
5aa7784d2c
@ -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))
|
||||
}
|
||||
|
@ -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<Empty>()
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
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<Empty>, 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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<Empty>()
|
||||
|
||||
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)
|
||||
|
@ -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<TGMediaAvatarMenuMixin?>(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<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) {
|
||||
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: {},
|
||||
|
@ -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 {
|
||||
|
@ -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> {
|
||||
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)
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user