Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2024-09-12 15:21:47 +04:00
commit 3a9cacfa68
19 changed files with 1814 additions and 606 deletions

View File

@ -1370,7 +1370,7 @@ public class BrowserScreen: ViewController, MinimizableController {
inputHeight: layout.inputHeight ?? 0.0, inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics, metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics, deviceMetrics: layout.deviceMetrics,
orientation: nil, orientation: layout.metrics.orientation,
isVisible: true, isVisible: true,
theme: self.presentationData.theme, theme: self.presentationData.theme,
strings: self.presentationData.strings, strings: self.presentationData.strings,

View File

@ -65,7 +65,7 @@ open class ViewControllerComponentContainer: ViewController {
inputHeight: CGFloat, inputHeight: CGFloat,
metrics: LayoutMetrics, metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics, deviceMetrics: DeviceMetrics,
orientation: UIInterfaceOrientation? = nil, orientation: UIInterfaceOrientation?,
isVisible: Bool, isVisible: Bool,
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
@ -177,6 +177,7 @@ open class ViewControllerComponentContainer: ViewController {
inputHeight: layout.inputHeight ?? 0.0, inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics, metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics, deviceMetrics: layout.deviceMetrics,
orientation: layout.metrics.orientation,
isVisible: self.currentIsVisible, isVisible: self.currentIsVisible,
theme: self.resolvedTheme, theme: self.resolvedTheme,
strings: self.presentationData.strings, strings: self.presentationData.strings,

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

@ -481,6 +481,7 @@ public class ReplaceBoostScreen: ViewController {
inputHeight: layout.inputHeight ?? 0.0, inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics, metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics, deviceMetrics: layout.deviceMetrics,
orientation: layout.metrics.orientation,
isVisible: self.currentIsVisible, isVisible: self.currentIsVisible,
theme: self.presentationData.theme, theme: self.presentationData.theme,
strings: self.presentationData.strings, strings: self.presentationData.strings,

View File

@ -335,7 +335,7 @@ final class MediaStreamVideoComponent: Component {
stallTimer = _stallTimer stallTimer = _stallTimer
self.clipsToBounds = component.isFullscreen // or just true self.clipsToBounds = component.isFullscreen // or just true
if let videoView = self.videoRenderingContext.makeView(input: input, forceSampleBufferDisplayLayer: true) { if let videoView = self.videoRenderingContext.makeView(input: input, blur: false, forceSampleBufferDisplayLayer: true) {
self.videoView = videoView self.videoView = videoView
self.addSubview(videoView) self.addSubview(videoView)
videoView.alpha = 0 videoView.alpha = 0

View File

@ -134,10 +134,14 @@ final class VideoChatParticipantThumbnailComponent: Component {
if transition.animation.isImmediate { if transition.animation.isImmediate {
speakingAlphaTransition = .immediate speakingAlphaTransition = .immediate
} else { } else {
if !wasSpeaking { if let previousComponent, previousComponent.isSelected == component.isSelected {
speakingAlphaTransition = .easeInOut(duration: 0.1) if !wasSpeaking {
speakingAlphaTransition = .easeInOut(duration: 0.1)
} else {
speakingAlphaTransition = .easeInOut(duration: 0.25)
}
} else { } else {
speakingAlphaTransition = .easeInOut(duration: 0.25) speakingAlphaTransition = .immediate
} }
} }
@ -168,8 +172,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(VideoChatMuteIconComponent( component: AnyComponent(VideoChatMuteIconComponent(
color: .white, color: .white,
isFilled: true, content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking)
isMuted: component.participant.muteState != nil
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 36.0, height: 36.0) containerSize: CGSize(width: 36.0, height: 36.0)
@ -182,8 +185,6 @@ final class VideoChatParticipantThumbnailComponent: Component {
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
transition.setScale(view: muteStatusView, scale: 0.65) transition.setScale(view: muteStatusView, scale: 0.65)
speakingAlphaTransition.setTintColor(layer: muteStatusView.iconView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : .white)
} }
let titleSize = self.title.update( let titleSize = self.title.update(
@ -203,8 +204,6 @@ final class VideoChatParticipantThumbnailComponent: Component {
} }
transition.setPosition(view: titleView, position: titleFrame.origin) transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
speakingAlphaTransition.setTintColor(layer: titleView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : .white)
} }
if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription { if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription {
@ -289,20 +288,26 @@ final class VideoChatParticipantThumbnailComponent: Component {
var rotatedVideoResolution = videoResolution var rotatedVideoResolution = videoResolution
var rotatedVideoFrame = videoFrame var rotatedVideoFrame = videoFrame
var rotatedBlurredVideoFrame = blurredVideoFrame var rotatedBlurredVideoFrame = blurredVideoFrame
var rotatedVideoBoundsSize = videoFrame.size
var rotatedBlurredVideoBoundsSize = blurredVideoFrame.size
if videoIsRotated { if videoIsRotated {
rotatedVideoResolution = CGSize(width: rotatedVideoResolution.height, height: rotatedVideoResolution.width) rotatedVideoBoundsSize = CGSize(width: rotatedVideoBoundsSize.height, height: rotatedVideoBoundsSize.width)
rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center) rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center)
rotatedBlurredVideoBoundsSize = CGSize(width: rotatedBlurredVideoBoundsSize.height, height: rotatedBlurredVideoBoundsSize.width)
rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.center) rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.center)
} }
rotatedVideoResolution = rotatedVideoResolution.aspectFittedOrSmaller(CGSize(width: rotatedVideoFrame.width * UIScreenScale, height: rotatedVideoFrame.height * UIScreenScale))
transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center) transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoBoundsSize))
transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0)) transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0))
videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
transition.setPosition(layer: videoLayer.blurredLayer, position: rotatedBlurredVideoFrame.center) transition.setPosition(layer: videoLayer.blurredLayer, position: rotatedBlurredVideoFrame.center)
transition.setBounds(layer: videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBlurredVideoFrame.size)) transition.setBounds(layer: videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBlurredVideoBoundsSize))
transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0)) transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0))
} }
} else { } else {
@ -330,11 +335,13 @@ final class VideoChatParticipantThumbnailComponent: Component {
} else { } else {
selectedBorderView = UIImageView() selectedBorderView = UIImageView()
self.selectedBorderView = selectedBorderView self.selectedBorderView = selectedBorderView
selectedBorderView.alpha = 0.0
self.addSubview(selectedBorderView) self.addSubview(selectedBorderView)
selectedBorderView.image = View.selectedBorderImage selectedBorderView.image = View.selectedBorderImage
selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize) selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize)
speakingAlphaTransition.setAlpha(view: selectedBorderView, alpha: 1.0)
ComponentTransition.immediate.setTintColor(layer: selectedBorderView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) ComponentTransition.immediate.setTintColor(layer: selectedBorderView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor)
} }
} else if let selectedBorderView = self.selectedBorderView { } else if let selectedBorderView = self.selectedBorderView {

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

@ -6,50 +6,49 @@ import MultilineTextComponent
import TelegramPresentationData import TelegramPresentationData
import AppBundle import AppBundle
import LottieComponent import LottieComponent
import BundleIconComponent
final class VideoChatMuteIconComponent: Component { final class VideoChatMuteIconComponent: Component {
enum Content: Equatable {
case mute(isFilled: Bool, isMuted: Bool)
case screenshare
}
let color: UIColor let color: UIColor
let isFilled: Bool let content: Content
let isMuted: Bool
init( init(
color: UIColor, color: UIColor,
isFilled: Bool, content: Content
isMuted: Bool
) { ) {
self.color = color self.color = color
self.isFilled = isFilled self.content = content
self.isMuted = isMuted
} }
static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool { static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool {
if lhs.color != rhs.color { if lhs.color != rhs.color {
return false return false
} }
if lhs.isFilled != rhs.isFilled { if lhs.content != rhs.content {
return false
}
if lhs.isMuted != rhs.isMuted {
return false return false
} }
return true return true
} }
final class View: HighlightTrackingButton { final class View: HighlightTrackingButton {
private let icon: VoiceChatMicrophoneNode private var icon: VoiceChatMicrophoneNode?
private var scheenshareIcon: ComponentView<Empty>?
private var component: VideoChatMuteIconComponent? private var component: VideoChatMuteIconComponent?
private var isUpdating: Bool = false private var isUpdating: Bool = false
private var contentImage: UIImage? private var contentImage: UIImage?
var iconView: UIView { var iconView: UIView? {
return self.icon.view return self.icon?.view
} }
override init(frame: CGRect) { override init(frame: CGRect) {
self.icon = VoiceChatMicrophoneNode()
super.init(frame: frame) super.init(frame: frame)
} }
@ -65,14 +64,59 @@ final class VideoChatMuteIconComponent: Component {
self.component = component self.component = component
let animationSize = availableSize if case let .mute(isFilled, isMuted) = component.content {
let icon: VoiceChatMicrophoneNode
let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)) if let current = self.icon {
if self.icon.view.superview == nil { icon = current
self.addSubview(self.icon.view) } else {
icon = VoiceChatMicrophoneNode()
self.icon = icon
self.addSubview(icon.view)
}
let animationSize = availableSize
let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: icon.view, frame: animationFrame)
icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color), animated: !transition.animation.isImmediate)
} else {
if let icon = self.icon {
self.icon = nil
icon.view.removeFromSuperview()
}
}
if case .screenshare = component.content {
let scheenshareIcon: ComponentView<Empty>
if let current = self.scheenshareIcon {
scheenshareIcon = current
} else {
scheenshareIcon = ComponentView()
self.scheenshareIcon = scheenshareIcon
}
let scheenshareIconSize = scheenshareIcon.update(
transition: transition,
component: AnyComponent(BundleIconComponent(
name: "Call/StatusScreen",
tintColor: component.color
)),
environment: {},
containerSize: availableSize
)
let scheenshareIconFrame = scheenshareIconSize.centered(in: CGRect(origin: CGPoint(), size: availableSize))
if let scheenshareIconView = scheenshareIcon.view {
if scheenshareIconView.superview == nil {
self.addSubview(scheenshareIconView)
}
transition.setPosition(view: scheenshareIconView, position: scheenshareIconFrame.center)
transition.setBounds(view: scheenshareIconView, bounds: CGRect(origin: CGPoint(), size: scheenshareIconFrame.size))
transition.setScale(view: scheenshareIconView, scale: 1.5)
}
} else {
if let scheenshareIcon = self.scheenshareIcon {
self.scheenshareIcon = nil
scheenshareIcon.view?.removeFromSuperview()
}
} }
transition.setFrame(view: self.icon.view, frame: animationFrame)
self.icon.update(state: VoiceChatMicrophoneNode.State(muted: component.isMuted, filled: component.isFilled, color: component.color), animated: !transition.animation.isImmediate)
return availableSize return availableSize
} }

View File

@ -3,24 +3,25 @@ import UIKit
import Display import Display
import ComponentFlow import ComponentFlow
import TelegramPresentationData import TelegramPresentationData
import TelegramCore
final class VideoChatParticipantStatusComponent: Component { final class VideoChatParticipantStatusComponent: Component {
let isMuted: Bool let muteState: GroupCallParticipantsContext.Participant.MuteState?
let isSpeaking: Bool let isSpeaking: Bool
let theme: PresentationTheme let theme: PresentationTheme
init( init(
isMuted: Bool, muteState: GroupCallParticipantsContext.Participant.MuteState?,
isSpeaking: Bool, isSpeaking: Bool,
theme: PresentationTheme theme: PresentationTheme
) { ) {
self.isMuted = isMuted self.muteState = muteState
self.isSpeaking = isSpeaking self.isSpeaking = isSpeaking
self.theme = theme self.theme = theme
} }
static func ==(lhs: VideoChatParticipantStatusComponent, rhs: VideoChatParticipantStatusComponent) -> Bool { static func ==(lhs: VideoChatParticipantStatusComponent, rhs: VideoChatParticipantStatusComponent) -> Bool {
if lhs.isMuted != rhs.isMuted { if lhs.muteState != rhs.muteState {
return false return false
} }
if lhs.isSpeaking != rhs.isSpeaking { if lhs.isSpeaking != rhs.isSpeaking {
@ -61,8 +62,7 @@ final class VideoChatParticipantStatusComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(VideoChatMuteIconComponent( component: AnyComponent(VideoChatMuteIconComponent(
color: .white, color: .white,
isFilled: false, content: .mute(isFilled: false, isMuted: component.muteState != nil && !component.isSpeaking)
isMuted: component.isMuted && !component.isSpeaking
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 36.0, height: 36.0) containerSize: CGSize(width: 36.0, height: 36.0)
@ -80,7 +80,24 @@ final class VideoChatParticipantStatusComponent: Component {
} else { } else {
tintTransition = .immediate tintTransition = .immediate
} }
tintTransition.setTintColor(layer: muteStatusView.iconView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : UIColor(white: 1.0, alpha: 0.4)) if let iconView = muteStatusView.iconView {
let iconTintColor: UIColor
if component.isSpeaking {
iconTintColor = UIColor(rgb: 0x33C758)
} else {
if let muteState = component.muteState {
if muteState.canUnmute {
iconTintColor = UIColor(white: 1.0, alpha: 0.4)
} else {
iconTintColor = UIColor(rgb: 0xFF3B30)
}
} else {
iconTintColor = UIColor(white: 1.0, alpha: 0.4)
}
}
tintTransition.setTintColor(layer: iconView.layer, color: iconTintColor)
}
} }
return size return size

View File

@ -40,7 +40,10 @@ final class VideoChatParticipantVideoComponent: Component {
let isPresentation: Bool let isPresentation: Bool
let isSpeaking: Bool let isSpeaking: Bool
let isExpanded: Bool let isExpanded: Bool
let bottomInset: CGFloat let isUIHidden: Bool
let contentInsets: UIEdgeInsets
let controlInsets: UIEdgeInsets
let interfaceOrientation: UIInterfaceOrientation
weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView? weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?
let action: (() -> Void)? let action: (() -> Void)?
@ -50,7 +53,10 @@ final class VideoChatParticipantVideoComponent: Component {
isPresentation: Bool, isPresentation: Bool,
isSpeaking: Bool, isSpeaking: Bool,
isExpanded: Bool, isExpanded: Bool,
bottomInset: CGFloat, isUIHidden: Bool,
contentInsets: UIEdgeInsets,
controlInsets: UIEdgeInsets,
interfaceOrientation: UIInterfaceOrientation,
rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?, rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?,
action: (() -> Void)? action: (() -> Void)?
) { ) {
@ -59,7 +65,10 @@ final class VideoChatParticipantVideoComponent: Component {
self.isPresentation = isPresentation self.isPresentation = isPresentation
self.isSpeaking = isSpeaking self.isSpeaking = isSpeaking
self.isExpanded = isExpanded self.isExpanded = isExpanded
self.bottomInset = bottomInset self.isUIHidden = isUIHidden
self.contentInsets = contentInsets
self.controlInsets = controlInsets
self.interfaceOrientation = interfaceOrientation
self.rootVideoLoadingEffectView = rootVideoLoadingEffectView self.rootVideoLoadingEffectView = rootVideoLoadingEffectView
self.action = action self.action = action
} }
@ -77,7 +86,16 @@ final class VideoChatParticipantVideoComponent: Component {
if lhs.isExpanded != rhs.isExpanded { if lhs.isExpanded != rhs.isExpanded {
return false return false
} }
if lhs.bottomInset != rhs.bottomInset { if lhs.isUIHidden != rhs.isUIHidden {
return false
}
if lhs.contentInsets != rhs.contentInsets {
return false
}
if lhs.controlInsets != rhs.controlInsets {
return false
}
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false return false
} }
if (lhs.action == nil) != (rhs.action == nil) { if (lhs.action == nil) != (rhs.action == nil) {
@ -89,10 +107,12 @@ final class VideoChatParticipantVideoComponent: Component {
private struct VideoSpec: Equatable { private struct VideoSpec: Equatable {
var resolution: CGSize var resolution: CGSize
var rotationAngle: Float var rotationAngle: Float
var followsDeviceOrientation: Bool
init(resolution: CGSize, rotationAngle: Float) { init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool) {
self.resolution = resolution self.resolution = resolution
self.rotationAngle = rotationAngle self.rotationAngle = rotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
} }
} }
@ -153,6 +173,15 @@ final class VideoChatParticipantVideoComponent: Component {
self.component = component self.component = component
self.componentState = state self.componentState = state
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.2)
} else {
alphaTransition = .immediate
}
let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0
let nameColor = component.participant.peer.nameColor ?? .blue let nameColor = component.participant.peer.nameColor ?? .blue
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true) let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
self.backgroundColor = nameColors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.4) self.backgroundColor = nameColors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.4)
@ -210,25 +239,26 @@ final class VideoChatParticipantVideoComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(VideoChatMuteIconComponent( component: AnyComponent(VideoChatMuteIconComponent(
color: .white, color: .white,
isFilled: true, content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking)
isMuted: component.participant.muteState != nil
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 36.0, height: 36.0) containerSize: CGSize(width: 36.0, height: 36.0)
) )
let muteStatusFrame: CGRect let muteStatusFrame: CGRect
if component.isExpanded { if component.isExpanded {
muteStatusFrame = CGRect(origin: CGPoint(x: 5.0, y: availableSize.height - component.bottomInset + 1.0 - muteStatusSize.height), size: muteStatusSize) muteStatusFrame = CGRect(origin: CGPoint(x: 5.0, y: availableSize.height - component.controlInsets.bottom + 1.0 - muteStatusSize.height), size: muteStatusSize)
} else { } else {
muteStatusFrame = CGRect(origin: CGPoint(x: 1.0, y: availableSize.height - component.bottomInset + 3.0 - muteStatusSize.height), size: muteStatusSize) muteStatusFrame = CGRect(origin: CGPoint(x: 1.0, y: availableSize.height - component.controlInsets.bottom + 3.0 - muteStatusSize.height), size: muteStatusSize)
} }
if let muteStatusView = self.muteStatus.view { if let muteStatusView = self.muteStatus.view {
if muteStatusView.superview == nil { if muteStatusView.superview == nil {
self.addSubview(muteStatusView) self.addSubview(muteStatusView)
muteStatusView.alpha = controlsAlpha
} }
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
transition.setScale(view: muteStatusView, scale: component.isExpanded ? 1.0 : 0.7) transition.setScale(view: muteStatusView, scale: component.isExpanded ? 1.0 : 0.7)
alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha)
} }
let titleSize = self.title.update( let titleSize = self.title.update(
@ -241,18 +271,20 @@ final class VideoChatParticipantVideoComponent: Component {
) )
let titleFrame: CGRect let titleFrame: CGRect
if component.isExpanded { if component.isExpanded {
titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.bottomInset - 8.0 - titleSize.height), size: titleSize) titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height), size: titleSize)
} else { } else {
titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.bottomInset - 4.0 - titleSize.height), size: titleSize) titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height), size: titleSize)
} }
if let titleView = self.title.view { if let titleView = self.title.view {
if titleView.superview == nil { if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint() titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView) self.addSubview(titleView)
titleView.alpha = controlsAlpha
} }
transition.setPosition(view: titleView, position: titleFrame.origin) transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
transition.setScale(view: titleView, scale: component.isExpanded ? 1.0 : 0.825) transition.setScale(view: titleView, scale: component.isExpanded ? 1.0 : 0.825)
alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha)
} }
if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription { if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription {
@ -296,7 +328,7 @@ final class VideoChatParticipantVideoComponent: Component {
videoLayer.video = videoOutput videoLayer.video = videoOutput
if let videoOutput { if let videoOutput {
let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle) let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation)
if self.videoSpec != videoSpec { if self.videoSpec != videoSpec {
self.videoSpec = videoSpec self.videoSpec = videoSpec
if !self.isUpdating { if !self.isUpdating {
@ -311,69 +343,6 @@ final class VideoChatParticipantVideoComponent: Component {
} }
} }
} }
/*var notifyOrientationUpdated = false
var notifyIsMirroredUpdated = false
if !self.didReportFirstFrame {
notifyOrientationUpdated = true
notifyIsMirroredUpdated = true
}
if let currentOutput = videoOutput {
let currentAspect: CGFloat
if currentOutput.resolution.height > 0.0 {
currentAspect = currentOutput.resolution.width / currentOutput.resolution.height
} else {
currentAspect = 1.0
}
if self.currentAspect != currentAspect {
self.currentAspect = currentAspect
notifyOrientationUpdated = true
}
let currentOrientation: PresentationCallVideoView.Orientation
if currentOutput.followsDeviceOrientation {
currentOrientation = .rotation0
} else {
if abs(currentOutput.rotationAngle - 0.0) < .ulpOfOne {
currentOrientation = .rotation0
} else if abs(currentOutput.rotationAngle - Float.pi * 0.5) < .ulpOfOne {
currentOrientation = .rotation90
} else if abs(currentOutput.rotationAngle - Float.pi) < .ulpOfOne {
currentOrientation = .rotation180
} else if abs(currentOutput.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne {
currentOrientation = .rotation270
} else {
currentOrientation = .rotation0
}
}
if self.currentOrientation != currentOrientation {
self.currentOrientation = currentOrientation
notifyOrientationUpdated = true
}
let currentIsMirrored = !currentOutput.mirrorDirection.isEmpty
if self.currentIsMirrored != currentIsMirrored {
self.currentIsMirrored = currentIsMirrored
notifyIsMirroredUpdated = true
}
}
if !self.didReportFirstFrame {
self.didReportFirstFrame = true
self.onFirstFrameReceived?(Float(self.currentAspect))
}
if notifyOrientationUpdated {
self.onOrientationUpdated?(self.currentOrientation, self.currentAspect)
}
if notifyIsMirroredUpdated {
self.onIsMirroredUpdated?(self.currentIsMirrored)
}*/
} }
} }
} }
@ -383,9 +352,11 @@ final class VideoChatParticipantVideoComponent: Component {
if let videoSpec = self.videoSpec { if let videoSpec = self.videoSpec {
videoBackgroundLayer.isHidden = false videoBackgroundLayer.isHidden = false
let rotationAngle = resolveCallVideoRotationAngle(angle: videoSpec.rotationAngle, followsDeviceOrientation: videoSpec.followsDeviceOrientation, interfaceOrientation: component.interfaceOrientation)
var rotatedResolution = videoSpec.resolution var rotatedResolution = videoSpec.resolution
var videoIsRotated = false var videoIsRotated = false
if abs(videoSpec.rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(videoSpec.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne { if abs(rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne {
videoIsRotated = true videoIsRotated = true
} }
if videoIsRotated { if videoIsRotated {
@ -397,26 +368,31 @@ final class VideoChatParticipantVideoComponent: Component {
let blurredVideoSize = rotatedResolution.aspectFilled(availableSize) let blurredVideoSize = rotatedResolution.aspectFilled(availableSize)
let blurredVideoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - blurredVideoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - blurredVideoSize.height) * 0.5)), size: blurredVideoSize) let blurredVideoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - blurredVideoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - blurredVideoSize.height) * 0.5)), size: blurredVideoSize)
let videoResolution = rotatedResolution.aspectFitted(CGSize(width: availableSize.width * 3.0, height: availableSize.height * 3.0)) let videoResolution = rotatedResolution
var rotatedVideoResolution = videoResolution var rotatedVideoResolution = videoResolution
var rotatedVideoFrame = videoFrame var rotatedVideoFrame = videoFrame
var rotatedBlurredVideoFrame = blurredVideoFrame var rotatedBlurredVideoFrame = blurredVideoFrame
var rotatedVideoBoundsSize = videoFrame.size
var rotatedBlurredVideoBoundsSize = blurredVideoFrame.size
if videoIsRotated { if videoIsRotated {
rotatedVideoResolution = CGSize(width: rotatedVideoResolution.height, height: rotatedVideoResolution.width) rotatedVideoBoundsSize = CGSize(width: rotatedVideoBoundsSize.height, height: rotatedVideoBoundsSize.width)
rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center) rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center)
rotatedBlurredVideoBoundsSize = CGSize(width: rotatedBlurredVideoBoundsSize.height, height: rotatedBlurredVideoBoundsSize.width)
rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.center) rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.center)
} }
rotatedVideoResolution = rotatedVideoResolution.aspectFittedOrSmaller(CGSize(width: rotatedVideoFrame.width * UIScreenScale, height: rotatedVideoFrame.height * UIScreenScale))
transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center) transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoBoundsSize))
transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0)) transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0))
videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
transition.setPosition(layer: videoLayer.blurredLayer, position: rotatedBlurredVideoFrame.center) transition.setPosition(layer: videoLayer.blurredLayer, position: rotatedBlurredVideoFrame.center)
transition.setBounds(layer: videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBlurredVideoFrame.size)) transition.setBounds(layer: videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBlurredVideoBoundsSize))
transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0)) transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0))
} }
} else { } else {
if let videoBackgroundLayer = self.videoBackgroundLayer { if let videoBackgroundLayer = self.videoBackgroundLayer {
@ -439,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

@ -12,21 +12,26 @@ import TelegramPresentationData
import PeerListItemComponent import PeerListItemComponent
final class VideoChatParticipantsComponent: Component { final class VideoChatParticipantsComponent: Component {
enum LayoutType: Equatable { struct Layout: Equatable {
struct Horizontal: Equatable { struct Column: Equatable {
var rightColumnWidth: CGFloat var width: CGFloat
var columnSpacing: CGFloat var insets: UIEdgeInsets
var isCentered: Bool
init(rightColumnWidth: CGFloat, columnSpacing: CGFloat, isCentered: Bool) { init(width: CGFloat, insets: UIEdgeInsets) {
self.rightColumnWidth = rightColumnWidth self.width = width
self.columnSpacing = columnSpacing self.insets = insets
self.isCentered = isCentered
} }
} }
case vertical var videoColumn: Column?
case horizontal(Horizontal) var mainColumn: Column
var columnSpacing: CGFloat
init(videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat) {
self.videoColumn = videoColumn
self.mainColumn = mainColumn
self.columnSpacing = columnSpacing
}
} }
final class Participants: Equatable { final class Participants: Equatable {
@ -75,10 +80,12 @@ final class VideoChatParticipantsComponent: Component {
final class ExpandedVideoState: Equatable { final class ExpandedVideoState: Equatable {
let mainParticipant: VideoParticipantKey let mainParticipant: VideoParticipantKey
let isMainParticipantPinned: Bool let isMainParticipantPinned: Bool
let isUIHidden: Bool
init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool) { init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool, isUIHidden: Bool) {
self.mainParticipant = mainParticipant self.mainParticipant = mainParticipant
self.isMainParticipantPinned = isMainParticipantPinned self.isMainParticipantPinned = isMainParticipantPinned
self.isUIHidden = isUIHidden
} }
static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool { static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool {
@ -91,6 +98,9 @@ final class VideoChatParticipantsComponent: Component {
if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned { if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned {
return false return false
} }
if lhs.isUIHidden != rhs.isUIHidden {
return false
}
return true return true
} }
} }
@ -101,12 +111,15 @@ final class VideoChatParticipantsComponent: Component {
let expandedVideoState: ExpandedVideoState? let expandedVideoState: ExpandedVideoState?
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let layoutType: LayoutType let layout: Layout
let collapsedContainerInsets: UIEdgeInsets let expandedInsets: UIEdgeInsets
let expandedContainerInsets: UIEdgeInsets let safeInsets: UIEdgeInsets
let sideInset: CGFloat let interfaceOrientation: UIInterfaceOrientation
let openParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void
let updateMainParticipant: (VideoParticipantKey?) -> Void let updateMainParticipant: (VideoParticipantKey?) -> Void
let updateIsMainParticipantPinned: (Bool) -> Void let updateIsMainParticipantPinned: (Bool) -> Void
let updateIsExpandedUIHidden: (Bool) -> Void
let openInviteMembers: () -> Void
init( init(
call: PresentationGroupCall, call: PresentationGroupCall,
@ -115,12 +128,15 @@ final class VideoChatParticipantsComponent: Component {
expandedVideoState: ExpandedVideoState?, expandedVideoState: ExpandedVideoState?,
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
layoutType: LayoutType, layout: Layout,
collapsedContainerInsets: UIEdgeInsets, expandedInsets: UIEdgeInsets,
expandedContainerInsets: UIEdgeInsets, safeInsets: UIEdgeInsets,
sideInset: CGFloat, interfaceOrientation: UIInterfaceOrientation,
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,
openInviteMembers: @escaping () -> Void
) { ) {
self.call = call self.call = call
self.participants = participants self.participants = participants
@ -128,12 +144,15 @@ final class VideoChatParticipantsComponent: Component {
self.expandedVideoState = expandedVideoState self.expandedVideoState = expandedVideoState
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.layoutType = layoutType self.layout = layout
self.collapsedContainerInsets = collapsedContainerInsets self.expandedInsets = expandedInsets
self.expandedContainerInsets = expandedContainerInsets self.safeInsets = safeInsets
self.sideInset = sideInset self.interfaceOrientation = interfaceOrientation
self.openParticipantContextMenu = openParticipantContextMenu
self.updateMainParticipant = updateMainParticipant self.updateMainParticipant = updateMainParticipant
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
self.openInviteMembers = openInviteMembers
} }
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool { static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
@ -152,16 +171,16 @@ final class VideoChatParticipantsComponent: Component {
if lhs.strings !== rhs.strings { if lhs.strings !== rhs.strings {
return false return false
} }
if lhs.layoutType != rhs.layoutType { if lhs.layout != rhs.layout {
return false return false
} }
if lhs.collapsedContainerInsets != rhs.collapsedContainerInsets { if lhs.expandedInsets != rhs.expandedInsets {
return false return false
} }
if lhs.expandedContainerInsets != rhs.expandedContainerInsets { if lhs.safeInsets != rhs.safeInsets {
return false return false
} }
if lhs.sideInset != rhs.sideInset { if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false return false
} }
return true return true
@ -178,40 +197,84 @@ final class VideoChatParticipantsComponent: Component {
let containerSize: CGSize let containerSize: CGSize
let sideInset: CGFloat let sideInset: CGFloat
let itemCount: Int let itemCount: Int
let isDedicatedColumn: Bool
let itemSize: CGSize let itemSize: CGSize
let itemSpacing: CGFloat let itemSpacing: CGFloat
let lastItemSize: CGFloat let lastItemSize: CGFloat
let lastRowItemCount: Int
let lastRowItemSize: CGFloat
let itemsPerRow: Int let itemsPerRow: Int
let rowCount: Int
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int) { init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, isDedicatedColumn: Bool) {
self.containerSize = containerSize self.containerSize = containerSize
self.sideInset = sideInset self.sideInset = sideInset
self.itemCount = itemCount self.itemCount = itemCount
self.isDedicatedColumn = isDedicatedColumn
let width: CGFloat = containerSize.width - sideInset * 2.0 let width: CGFloat = containerSize.width - sideInset * 2.0
self.itemSpacing = 4.0 self.itemSpacing = 4.0
let itemsPerRow: Int let itemsPerRow: Int
if itemCount == 1 { if isDedicatedColumn {
itemsPerRow = 1 if itemCount <= 2 {
itemsPerRow = 1
} else {
itemsPerRow = 2
}
} else { } else {
itemsPerRow = 2 if itemCount == 1 {
itemsPerRow = 1
} else {
itemsPerRow = 2
}
} }
self.itemsPerRow = Int(itemsPerRow) self.itemsPerRow = Int(itemsPerRow)
let itemWidth = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / CGFloat(itemsPerRow)) let itemWidth = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / CGFloat(itemsPerRow))
let itemHeight = min(180.0, itemWidth) let itemHeight = min(180.0, itemWidth)
self.itemSize = CGSize(width: itemWidth, height: itemHeight) var itemSize = CGSize(width: itemWidth, height: itemHeight)
self.rowCount = itemCount / self.itemsPerRow + ((itemCount % self.itemsPerRow) != 0 ? 1 : 0)
if isDedicatedColumn && itemCount != 0 {
let contentHeight = itemSize.height * CGFloat(self.rowCount) + self.itemSpacing * CGFloat(max(0, self.rowCount - 1))
if contentHeight < containerSize.height {
itemSize.height = (containerSize.height - self.itemSpacing * CGFloat(max(0, self.rowCount - 1))) / CGFloat(self.rowCount)
itemSize.height = floor(itemSize.height)
}
}
self.itemSize = itemSize
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1) self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
var lastRowItemCount = itemCount % self.itemsPerRow
if lastRowItemCount == 0 {
lastRowItemCount = self.itemsPerRow
}
self.lastRowItemCount = lastRowItemCount
self.lastRowItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(lastRowItemCount - 1)
} }
func frame(at index: Int) -> CGRect { func frame(at index: Int) -> CGRect {
let row = index / self.itemsPerRow let row = index / self.itemsPerRow
let column = index % self.itemsPerRow let column = index % self.itemsPerRow
let frame = CGRect(origin: CGPoint(x: self.sideInset + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height)) let itemWidth: CGFloat
if row == self.rowCount - 1 && column == self.lastRowItemCount - 1 {
itemWidth = self.lastRowItemSize
} else if column == self.itemsPerRow - 1 {
if row == self.rowCount - 1 {
itemWidth = self.lastRowItemSize
} else {
itemWidth = self.lastItemSize
}
} else {
itemWidth = self.itemSize.width
}
let frame = CGRect(origin: CGPoint(x: self.sideInset + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: itemWidth, height: itemSize.height))
return frame return frame
} }
@ -237,21 +300,37 @@ final class VideoChatParticipantsComponent: Component {
struct ExpandedGrid { struct ExpandedGrid {
let containerSize: CGSize let containerSize: CGSize
let layoutType: LayoutType let layout: Layout
let containerInsets: UIEdgeInsets let expandedInsets: UIEdgeInsets
let isUIHidden: Bool
init(containerSize: CGSize, layoutType: LayoutType, containerInsets: UIEdgeInsets) { init(containerSize: CGSize, layout: Layout, expandedInsets: UIEdgeInsets, isUIHidden: Bool) {
self.containerSize = containerSize self.containerSize = containerSize
self.layoutType = layoutType self.layout = layout
self.containerInsets = containerInsets self.expandedInsets = expandedInsets
self.isUIHidden = isUIHidden
} }
func itemContainerFrame() -> CGRect { func itemContainerFrame() -> CGRect {
switch self.layoutType { let containerInsets: UIEdgeInsets
case .vertical: if self.isUIHidden {
return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top - containerInsets.bottom)) containerInsets = UIEdgeInsets()
case .horizontal: } else {
return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top)) containerInsets = self.expandedInsets
}
if self.layout.videoColumn != nil {
return CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: CGSize(width: self.containerSize.width - containerInsets.left - containerInsets.right, height: self.containerSize.height - containerInsets.top - containerInsets.bottom))
} else {
return CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: CGSize(width: self.containerSize.width - containerInsets.left - containerInsets.right, height: self.containerSize.height - containerInsets.top - containerInsets.bottom))
}
}
func itemContainerInsets() -> UIEdgeInsets {
if self.isUIHidden {
return self.expandedInsets
} else {
return UIEdgeInsets()
} }
} }
} }
@ -306,9 +385,10 @@ final class VideoChatParticipantsComponent: Component {
} }
let containerSize: CGSize let containerSize: CGSize
let layoutType: LayoutType let layout: Layout
let collapsedContainerInsets: UIEdgeInsets let isUIHidden: Bool
let sideInset: CGFloat let expandedInsets: UIEdgeInsets
let safeInsets: UIEdgeInsets
let grid: Grid let grid: Grid
let expandedGrid: ExpandedGrid let expandedGrid: ExpandedGrid
let list: List let list: List
@ -320,32 +400,41 @@ final class VideoChatParticipantsComponent: Component {
let scrollClippingFrame: CGRect let scrollClippingFrame: CGRect
let separateVideoScrollClippingFrame: CGRect let separateVideoScrollClippingFrame: CGRect
init(containerSize: CGSize, layoutType: LayoutType, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) { init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
self.containerSize = containerSize self.containerSize = containerSize
self.layoutType = layoutType self.layout = layout
self.collapsedContainerInsets = collapsedContainerInsets self.isUIHidden = isUIHidden
self.sideInset = sideInset self.expandedInsets = expandedInsets
self.safeInsets = safeInsets
let listWidth: CGFloat = layout.mainColumn.width
let gridWidth: CGFloat let gridWidth: CGFloat
let listWidth: CGFloat let gridSideInset: CGFloat
switch layoutType { let gridContainerHeight: CGFloat
case .vertical: if let videoColumn = layout.videoColumn {
listWidth = containerSize.width - sideInset * 2.0 gridWidth = videoColumn.width
gridSideInset = videoColumn.insets.left
gridContainerHeight = containerSize.height - videoColumn.insets.top - videoColumn.insets.bottom
} else {
gridWidth = listWidth gridWidth = listWidth
case let .horizontal(horizontal): gridSideInset = layout.mainColumn.insets.left
listWidth = horizontal.rightColumnWidth gridContainerHeight = containerSize.height
gridWidth = max(10.0, containerSize.width - sideInset * 2.0 - horizontal.rightColumnWidth - horizontal.columnSpacing)
} }
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount) self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil)
self.expandedGrid = ExpandedGrid(containerSize: containerSize, layoutType: layoutType, containerInsets: expandedContainerInsets) self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.spacing = 4.0 self.spacing = 4.0
self.gridOffsetY = collapsedContainerInsets.top if let videoColumn = layout.videoColumn, !isUIHidden {
self.expandedGrid = ExpandedGrid(containerSize: CGSize(width: videoColumn.width + expandedInsets.left, height: containerSize.height), layout: layout, expandedInsets: UIEdgeInsets(top: expandedInsets.top, left: expandedInsets.left, bottom: expandedInsets.bottom, right: 0.0), isUIHidden: isUIHidden)
} else {
self.expandedGrid = ExpandedGrid(containerSize: containerSize, layout: layout, expandedInsets: expandedInsets, isUIHidden: isUIHidden)
}
self.gridOffsetY = layout.mainColumn.insets.top
var listOffsetY: CGFloat = self.gridOffsetY var listOffsetY: CGFloat = self.gridOffsetY
if case .vertical = layoutType { if layout.videoColumn == nil {
if self.grid.itemCount != 0 { if self.grid.itemCount != 0 {
listOffsetY += self.grid.contentHeight() listOffsetY += self.grid.contentHeight()
listOffsetY += self.spacing listOffsetY += self.spacing
@ -353,55 +442,54 @@ final class VideoChatParticipantsComponent: Component {
} }
self.listOffsetY = listOffsetY self.listOffsetY = listOffsetY
switch layoutType { if let videoColumn = layout.videoColumn {
case .vertical: let columnsWidth: CGFloat = videoColumn.width + layout.columnSpacing + layout.mainColumn.width
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.sideInset, y: collapsedContainerInsets.top), size: CGSize(width: containerSize.width - self.sideInset * 2.0, height: containerSize.height - collapsedContainerInsets.top - collapsedContainerInsets.bottom)) let columnsSideInset: CGFloat = floorToScreenPixels((containerSize.width - columnsWidth) * 0.5)
self.listFrame = CGRect(origin: CGPoint(), size: containerSize)
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: -containerSize.width, y: 0.0), size: containerSize) var separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height))
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top))
case let .horizontal(horizontal): var listFrame = CGRect(origin: CGPoint(x: separateVideoGridFrame.maxX + layout.columnSpacing, y: 0.0), size: CGSize(width: listWidth, height: containerSize.height))
if horizontal.isCentered { if isUIHidden {
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - horizontal.rightColumnWidth) * 0.5), y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height)) listFrame.origin.x += columnsSideInset + layout.mainColumn.width
} else { separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: columnsWidth, height: containerSize.height))
self.listFrame = CGRect(origin: CGPoint(x: containerSize.width - self.sideInset - horizontal.rightColumnWidth, y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height))
} }
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - collapsedContainerInsets.top))
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: min(self.sideInset, self.scrollClippingFrame.minX - horizontal.columnSpacing - gridWidth), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height)) self.separateVideoGridFrame = separateVideoGridFrame
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top)) self.listFrame = listFrame
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: videoColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - videoColumn.insets.top))
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - layout.mainColumn.insets.top))
} else {
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height))
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth, height: containerSize.height - layout.mainColumn.insets.top - layout.mainColumn.insets.bottom))
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: containerSize.height))
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - layout.mainColumn.insets.top))
} }
} }
func contentHeight() -> CGFloat { func contentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY var result: CGFloat = self.gridOffsetY
switch self.layoutType { if self.layout.videoColumn == nil {
case .vertical:
if self.grid.itemCount != 0 { if self.grid.itemCount != 0 {
result += self.grid.contentHeight() result += self.grid.contentHeight()
result += self.spacing result += self.spacing
} }
case .horizontal:
break
} }
result += self.list.contentHeight() result += self.list.contentHeight()
result += self.collapsedContainerInsets.bottom result += self.layout.mainColumn.insets.bottom
result += 24.0 result += 24.0
return result return result
} }
func separateVideoGridContentHeight() -> CGFloat { func separateVideoGridContentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY var result: CGFloat = self.gridOffsetY
switch self.layoutType { if let videoColumn = self.layout.videoColumn {
case .vertical:
break
case .horizontal:
if self.grid.itemCount != 0 { if self.grid.itemCount != 0 {
result += self.grid.contentHeight() result += self.grid.contentHeight()
} }
result += videoColumn.insets.bottom
} }
result += self.collapsedContainerInsets.bottom
result += 24.0
return result return result
} }
@ -414,11 +502,10 @@ final class VideoChatParticipantsComponent: Component {
} }
func gridItemContainerFrame() -> CGRect { func gridItemContainerFrame() -> CGRect {
switch self.layoutType { if let _ = self.layout.videoColumn {
case .vertical:
return CGRect(origin: CGPoint(x: self.sideInset, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.grid.contentHeight()))
case .horizontal:
return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.grid.contentHeight())) return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.grid.contentHeight()))
} else {
return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width, height: self.grid.contentHeight()))
} }
} }
@ -431,11 +518,10 @@ final class VideoChatParticipantsComponent: Component {
} }
func listItemContainerFrame() -> CGRect { func listItemContainerFrame() -> CGRect {
switch self.layoutType { if let _ = self.layout.videoColumn {
case .vertical: return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.list.contentHeight()))
return CGRect(origin: CGPoint(x: self.sideInset, y: self.listOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.list.contentHeight())) } else {
case .horizontal: return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.containerSize.width, height: self.list.contentHeight()))
return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.listFrame.width, height: self.list.contentHeight()))
} }
} }
@ -444,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
@ -496,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
@ -519,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
@ -581,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) {
@ -609,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)
@ -620,6 +747,13 @@ final class VideoChatParticipantsComponent: Component {
return return
} }
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.2)
} else {
alphaTransition = .immediate
}
let gridWasEmpty = self.appliedGridIsEmpty let gridWasEmpty = self.appliedGridIsEmpty
let gridIsEmpty = self.gridParticipants.isEmpty let gridIsEmpty = self.gridParticipants.isEmpty
self.appliedGridIsEmpty = gridIsEmpty self.appliedGridIsEmpty = gridIsEmpty
@ -636,28 +770,30 @@ 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 {
switch itemLayout.layoutType { if let videoColumn = itemLayout.layout.videoColumn {
case .vertical:
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top {
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top
}
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - component.collapsedContainerInsets.bottom {
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - component.collapsedContainerInsets.bottom)
}
case .horizontal:
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)
if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top { if expandedGridItemContainerFrame.origin.y < videoColumn.insets.top {
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y expandedGridItemContainerFrame.size.height -= videoColumn.insets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top expandedGridItemContainerFrame.origin.y = videoColumn.insets.top
} }
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height { if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height {
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height) expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height)
} }
} else {
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if expandedGridItemContainerFrame.origin.y < itemLayout.layout.mainColumn.insets.top {
expandedGridItemContainerFrame.size.height -= itemLayout.layout.mainColumn.insets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = itemLayout.layout.mainColumn.insets.top
}
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - itemLayout.layout.mainColumn.insets.bottom {
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - itemLayout.layout.mainColumn.insets.bottom)
}
} }
if expandedGridItemContainerFrame.size.height < 0.0 { if expandedGridItemContainerFrame.size.height < 0.0 {
expandedGridItemContainerFrame.size.height = 0.0 expandedGridItemContainerFrame.size.height = 0.0
@ -670,10 +806,9 @@ final class VideoChatParticipantsComponent: Component {
var validGridItemIndices: [Int] = [] var validGridItemIndices: [Int] = []
let visibleGridItemRange: (minIndex: Int, maxIndex: Int) let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
switch itemLayout.layoutType { if itemLayout.layout.videoColumn == nil {
case .vertical:
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds) visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
case .horizontal: } else {
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds) visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
} }
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex { if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
@ -707,8 +842,14 @@ final class VideoChatParticipantsComponent: Component {
} }
var isItemExpanded = false var isItemExpanded = false
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey { var isItemUIHidden = false
isItemExpanded = true if let expandedVideoState = component.expandedVideoState {
if expandedVideoState.mainParticipant == videoParticipantKey {
isItemExpanded = true
}
if expandedVideoState.isUIHidden {
isItemUIHidden = true
}
} }
var suppressItemExpansionCollapseAnimation = false var suppressItemExpansionCollapseAnimation = false
@ -734,14 +875,28 @@ final class VideoChatParticipantsComponent: Component {
itemFrame = itemLayout.gridItemFrame(at: index) itemFrame = itemLayout.gridItemFrame(at: index)
} }
var itemBottomInset: CGFloat = isItemExpanded ? 96.0 : 0.0 let itemContentInsets: UIEdgeInsets
switch itemLayout.layoutType { if isItemExpanded {
case .vertical: itemContentInsets = itemLayout.expandedGrid.itemContainerInsets()
break } else {
case .horizontal: itemContentInsets = UIEdgeInsets()
if isItemExpanded { }
itemBottomInset += itemLayout.expandedGrid.containerInsets.bottom
} var itemControlInsets: UIEdgeInsets
if isItemExpanded {
itemControlInsets = itemContentInsets
itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0)
} else {
itemControlInsets = itemContentInsets
}
let itemAlpha: CGFloat
if isItemExpanded {
itemAlpha = 1.0
} else if component.expandedVideoState != nil && itemLayout.layout.videoColumn != nil {
itemAlpha = 0.0
} else {
itemAlpha = 1.0
} }
let _ = itemView.view.update( let _ = itemView.view.update(
@ -752,14 +907,17 @@ final class VideoChatParticipantsComponent: Component {
isPresentation: videoParticipant.isPresentation, isPresentation: videoParticipant.isPresentation,
isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id), isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id),
isExpanded: isItemExpanded, isExpanded: isItemExpanded,
bottomInset: itemBottomInset, isUIHidden: isItemUIHidden,
contentInsets: itemContentInsets,
controlInsets: itemControlInsets,
interfaceOrientation: component.interfaceOrientation,
rootVideoLoadingEffectView: self.rootVideoLoadingEffectView, rootVideoLoadingEffectView: self.rootVideoLoadingEffectView,
action: { [weak self] in action: { [weak self] in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
if component.expandedVideoState?.mainParticipant == videoParticipantKey { if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey {
component.updateMainParticipant(nil) component.updateIsExpandedUIHidden(!expandedVideoState.isUIHidden)
} else { } else {
component.updateMainParticipant(videoParticipantKey) component.updateMainParticipant(videoParticipantKey)
} }
@ -770,6 +928,8 @@ final class VideoChatParticipantsComponent: Component {
) )
if let itemComponentView = itemView.view.view { if let itemComponentView = itemView.view.view {
if itemComponentView.superview == nil { if itemComponentView.superview == nil {
itemComponentView.layer.allowsGroupOpacity = true
if isItemExpanded { if isItemExpanded {
if let expandedThumbnailsView = self.expandedThumbnailsView?.view { if let expandedThumbnailsView = self.expandedThumbnailsView?.view {
self.expandedGridItemContainer.insertSubview(itemComponentView, belowSubview: expandedThumbnailsView) self.expandedGridItemContainer.insertSubview(itemComponentView, belowSubview: expandedThumbnailsView)
@ -781,12 +941,13 @@ final class VideoChatParticipantsComponent: Component {
} }
itemComponentView.frame = itemFrame itemComponentView.frame = itemFrame
itemComponentView.alpha = itemAlpha
if !resultingItemTransition.animation.isImmediate { if !resultingItemTransition.animation.isImmediate {
resultingItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0) resultingItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0)
} }
if !resultingItemTransition.animation.isImmediate { if !resultingItemTransition.animation.isImmediate && itemAlpha != 0.0 {
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) itemComponentView.layer.animateAlpha(from: 0.0, to: itemAlpha, duration: 0.1)
} }
} else if isItemExpanded && itemComponentView.superview != self.expandedGridItemContainer { } else if isItemExpanded && itemComponentView.superview != self.expandedGridItemContainer {
let fromFrame = itemComponentView.convert(itemComponentView.bounds, to: self.expandedGridItemContainer) let fromFrame = itemComponentView.convert(itemComponentView.bounds, to: self.expandedGridItemContainer)
@ -820,6 +981,14 @@ final class VideoChatParticipantsComponent: Component {
if !itemView.isCollapsing { if !itemView.isCollapsing {
resultingItemTransition.setPosition(view: itemComponentView, position: itemFrame.center) resultingItemTransition.setPosition(view: itemComponentView, position: itemFrame.center)
resultingItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) resultingItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
let resultingItemAlphaTransition: ComponentTransition
if !resultingItemTransition.animation.isImmediate {
resultingItemAlphaTransition = alphaTransition
} else {
resultingItemAlphaTransition = .immediate
}
resultingItemAlphaTransition.setAlpha(view: itemComponentView, alpha: itemAlpha)
} }
} }
} }
@ -876,7 +1045,7 @@ final class VideoChatParticipantsComponent: Component {
} }
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent( let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
isMuted: participant.muteState != nil, muteState: participant.muteState,
isSpeaking: component.speakingParticipants.contains(participant.peer.id), isSpeaking: component.speakingParticipants.contains(participant.peer.id),
theme: component.theme theme: component.theme
)) ))
@ -903,12 +1072,21 @@ final class VideoChatParticipantsComponent: Component {
rightAccessoryComponent: rightAccessoryComponent, rightAccessoryComponent: rightAccessoryComponent,
selectionState: .none, selectionState: .none,
hasNext: false, hasNext: false,
action: { [weak self] peer, _, _ in extractedTheme: PeerListItemComponent.ExtractedTheme(
guard let self else { inset: 2.0,
background: UIColor(white: 0.1, alpha: 1.0)
),
action: { [weak self] peer, _, itemView in
guard let self, let component = self.component else {
return return
} }
let _ = self component.openParticipantContextMenu(peer.id, itemView.extractedContainerView, nil)
let _ = peer },
contextAction: { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, sourceView, gesture)
} }
)), )),
environment: {}, environment: {},
@ -1017,6 +1195,18 @@ final class VideoChatParticipantsComponent: Component {
)) ))
}*/ }*/
let expandedControlsAlpha: CGFloat = expandedVideoState.isUIHidden ? 0.0 : 1.0
let expandedThumbnailsAlpha: CGFloat = expandedControlsAlpha
/*if itemLayout.layout.videoColumn == nil {
if expandedVideoState.isUIHidden {
expandedThumbnailsAlpha = 0.0
} else {
expandedThumbnailsAlpha = 1.0
}
} else {
expandedThumbnailsAlpha = 0.0
}*/
var expandedThumbnailsTransition = transition var expandedThumbnailsTransition = transition
let expandedThumbnailsView: ComponentView<Empty> let expandedThumbnailsView: ComponentView<Empty>
if let current = self.expandedThumbnailsView { if let current = self.expandedThumbnailsView {
@ -1046,16 +1236,11 @@ final class VideoChatParticipantsComponent: Component {
environment: {}, environment: {},
containerSize: itemLayout.expandedGrid.itemContainerFrame().size containerSize: itemLayout.expandedGrid.itemContainerFrame().size
) )
var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize) let expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
switch itemLayout.layoutType {
case .vertical:
break
case .horizontal:
expandedThumbnailsFrame.origin.y -= itemLayout.expandedGrid.containerInsets.bottom
}
if let expandedThumbnailsComponentView = expandedThumbnailsView.view { if let expandedThumbnailsComponentView = expandedThumbnailsView.view {
if expandedThumbnailsComponentView.superview == nil { if expandedThumbnailsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView) self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView)
expandedThumbnailsComponentView.alpha = expandedThumbnailsAlpha
let fromReferenceFrame: CGRect let fromReferenceFrame: CGRect
if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) {
@ -1066,11 +1251,12 @@ final class VideoChatParticipantsComponent: Component {
expandedThumbnailsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.maxY - expandedThumbnailsSize.height), size: expandedThumbnailsFrame.size) expandedThumbnailsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.maxY - expandedThumbnailsSize.height), size: expandedThumbnailsFrame.size)
if !transition.animation.isImmediate { if !transition.animation.isImmediate && expandedThumbnailsAlpha != 0.0 {
expandedThumbnailsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) expandedThumbnailsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} }
} }
transition.setFrame(view: expandedThumbnailsComponentView, frame: expandedThumbnailsFrame) transition.setFrame(view: expandedThumbnailsComponentView, frame: expandedThumbnailsFrame)
alphaTransition.setAlpha(view: expandedThumbnailsComponentView, alpha: expandedThumbnailsAlpha)
} }
var expandedControlsTransition = transition var expandedControlsTransition = transition
@ -1113,6 +1299,8 @@ final class VideoChatParticipantsComponent: Component {
if expandedControlsComponentView.superview == nil { if expandedControlsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedControlsComponentView) self.expandedGridItemContainer.addSubview(expandedControlsComponentView)
expandedControlsComponentView.alpha = expandedControlsAlpha
let fromReferenceFrame: CGRect let fromReferenceFrame: CGRect
if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) {
fromReferenceFrame = self.gridItemViewContainer.convert(itemLayout.gridItemFrame(at: index), to: self.expandedGridItemContainer) fromReferenceFrame = self.gridItemViewContainer.convert(itemLayout.gridItemFrame(at: index), to: self.expandedGridItemContainer)
@ -1122,11 +1310,12 @@ final class VideoChatParticipantsComponent: Component {
expandedControlsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.minY - previousExpandedGridItemContainerFrame.minY), size: expandedControlsFrame.size) expandedControlsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.minY - previousExpandedGridItemContainerFrame.minY), size: expandedControlsFrame.size)
if !transition.animation.isImmediate { if !transition.animation.isImmediate && expandedControlsAlpha != 0.0 {
expandedControlsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) expandedControlsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} }
} }
transition.setFrame(view: expandedControlsComponentView, frame: expandedControlsFrame) transition.setFrame(view: expandedControlsComponentView, frame: expandedControlsFrame)
alphaTransition.setAlpha(view: expandedControlsComponentView, alpha: expandedControlsAlpha)
} }
} else { } else {
if let expandedThumbnailsView = self.expandedThumbnailsView { if let expandedThumbnailsView = self.expandedThumbnailsView {
@ -1142,7 +1331,7 @@ final class VideoChatParticipantsComponent: Component {
let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.maxY - expandedThumbnailsComponentView.bounds.height), size: expandedThumbnailsComponentView.bounds.size) let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.maxY - expandedThumbnailsComponentView.bounds.height), size: expandedThumbnailsComponentView.bounds.size)
transition.setFrame(view: expandedThumbnailsComponentView, frame: targetThumbnailsFrame) transition.setFrame(view: expandedThumbnailsComponentView, frame: targetThumbnailsFrame)
} }
expandedThumbnailsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedThumbnailsComponentView] _ in expandedThumbnailsComponentView.layer.animateAlpha(from: expandedThumbnailsComponentView.alpha, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedThumbnailsComponentView] _ in
expandedThumbnailsComponentView?.removeFromSuperview() expandedThumbnailsComponentView?.removeFromSuperview()
}) })
} else { } else {
@ -1163,7 +1352,7 @@ final class VideoChatParticipantsComponent: Component {
let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.minY), size: expandedControlsComponentView.bounds.size) let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.minY), size: expandedControlsComponentView.bounds.size)
transition.setFrame(view: expandedControlsComponentView, frame: targetThumbnailsFrame) transition.setFrame(view: expandedControlsComponentView, frame: targetThumbnailsFrame)
} }
expandedControlsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedControlsComponentView] _ in expandedControlsComponentView.layer.animateAlpha(from: expandedControlsComponentView.alpha, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedControlsComponentView] _ in
expandedControlsComponentView?.removeFromSuperview() expandedControlsComponentView?.removeFromSuperview()
}) })
} else { } else {
@ -1180,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,
@ -1229,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)
@ -1258,7 +1432,7 @@ final class VideoChatParticipantsComponent: Component {
gridParticipants.append(videoParticipant) gridParticipants.append(videoParticipant)
} }
} }
if !hasVideo { if !hasVideo || component.layout.videoColumn != nil {
if participant.peer.id == component.call.accountContext.account.peerId { if participant.peer.id == component.call.accountContext.account.peerId {
listParticipants.insert(participant, at: 0) listParticipants.insert(participant, at: 0)
} else { } else {
@ -1272,10 +1446,10 @@ final class VideoChatParticipantsComponent: Component {
let itemLayout = ItemLayout( let itemLayout = ItemLayout(
containerSize: availableSize, containerSize: availableSize,
layoutType: component.layoutType, layout: component.layout,
sideInset: component.sideInset, isUIHidden: component.expandedVideoState?.isUIHidden ?? false,
collapsedContainerInsets: component.collapsedContainerInsets, expandedInsets: component.expandedInsets,
expandedContainerInsets: component.expandedContainerInsets, safeInsets: component.safeInsets,
gridItemCount: gridParticipants.count, gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count, listItemCount: listParticipants.count,
listItemHeight: measureListItemSize.height, listItemHeight: measureListItemSize.height,
@ -1290,9 +1464,9 @@ final class VideoChatParticipantsComponent: Component {
cornerRadius: 10.0 cornerRadius: 10.0
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: itemLayout.list.containerSize.width, height: itemLayout.list.contentHeight()) containerSize: CGSize(width: itemLayout.listFrame.width - itemLayout.layout.mainColumn.insets.left - itemLayout.layout.mainColumn.insets.right, height: itemLayout.list.contentHeight())
) )
let listItemsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: listItemsBackgroundSize) let listItemsBackgroundFrame = CGRect(origin: CGPoint(x: itemLayout.layout.mainColumn.insets.left, y: 0.0), size: listItemsBackgroundSize)
if let listItemsBackgroundView = self.listItemsBackground.view { if let listItemsBackgroundView = self.listItemsBackground.view {
if listItemsBackgroundView.superview == nil { if listItemsBackgroundView.superview == nil {
self.listItemViewContainer.addSubview(listItemsBackgroundView) self.listItemViewContainer.addSubview(listItemsBackgroundView)
@ -1322,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)
@ -1375,12 +1554,11 @@ final class VideoChatParticipantsComponent: Component {
self.ignoreScrolling = false self.ignoreScrolling = false
switch component.layoutType { if itemLayout.layout.videoColumn == nil {
case .vertical:
if self.gridItemViewContainer.superview !== self.scrollView { if self.gridItemViewContainer.superview !== self.scrollView {
self.scrollView.addSubview(self.gridItemViewContainer) self.scrollView.addSubview(self.gridItemViewContainer)
} }
case .horizontal: } else {
if self.gridItemViewContainer.superview !== self.separateVideoScrollView { if self.gridItemViewContainer.superview !== self.separateVideoScrollView {
self.separateVideoScrollView.addSubview(self.gridItemViewContainer) self.separateVideoScrollView.addSubview(self.gridItemViewContainer)
} }

File diff suppressed because it is too large Load Diff

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

@ -6,8 +6,6 @@ import SwiftSignalKit
import AccountContext import AccountContext
import TelegramVoip import TelegramVoip
import AVFoundation import AVFoundation
import CallScreen
import MetalEngine
protocol VideoRenderingView: UIView { protocol VideoRenderingView: UIView {
func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void)
@ -36,40 +34,30 @@ class VideoRenderingContext {
} }
#endif #endif
func makeView(input: Signal<OngoingGroupCallContext.VideoFrameData, NoError>, forceSampleBufferDisplayLayer: Bool = false) -> VideoRenderingView? { func makeView(input: Signal<OngoingGroupCallContext.VideoFrameData, NoError>, blur: Bool, forceSampleBufferDisplayLayer: Bool = false) -> VideoRenderingView? {
if !forceSampleBufferDisplayLayer {
return CallScreenVideoView(input: input)
}
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
if blur {
#if DEBUG
return SampleBufferVideoRenderingView(input: input)
#else
return nil
#endif
}
return SampleBufferVideoRenderingView(input: input) return SampleBufferVideoRenderingView(input: input)
#else #else
if #available(iOS 13.0, *), !forceSampleBufferDisplayLayer { if #available(iOS 13.0, *), !forceSampleBufferDisplayLayer {
return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: false) return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: blur)
} else { } else {
if blur {
return nil
}
return SampleBufferVideoRenderingView(input: input) return SampleBufferVideoRenderingView(input: input)
} }
#endif #endif
} }
func makeBlurView(input: Signal<OngoingGroupCallContext.VideoFrameData, NoError>, mainView: VideoRenderingView?, forceSampleBufferDisplayLayer: Bool = false) -> VideoRenderingView? { func makeBlurView(input: Signal<OngoingGroupCallContext.VideoFrameData, NoError>, mainView: VideoRenderingView?, forceSampleBufferDisplayLayer: Bool = false) -> VideoRenderingView? {
if let mainView = mainView as? CallScreenVideoView { return self.makeView(input: input, blur: true, forceSampleBufferDisplayLayer: forceSampleBufferDisplayLayer)
return CallScreenVideoBlurView(mainView: mainView)
}
#if targetEnvironment(simulator)
#if DEBUG
return SampleBufferVideoRenderingView(input: input)
#else
return nil
#endif
#else
if #available(iOS 13.0, *), !forceSampleBufferDisplayLayer {
return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: true)
} else {
return nil
}
#endif
} }
func updateVisibility(isVisible: Bool) { func updateVisibility(isVisible: Bool) {
@ -96,194 +84,3 @@ extension PresentationCallVideoView.Orientation {
} }
} }
} }
private final class CallScreenVideoView: UIView, VideoRenderingView {
private var isEnabled: Bool = false
private var onFirstFrameReceived: ((Float) -> Void)?
private var onOrientationUpdated: ((PresentationCallVideoView.Orientation, CGFloat) -> Void)?
private var onIsMirroredUpdated: ((Bool) -> Void)?
private var didReportFirstFrame: Bool = false
private var currentIsMirrored: Bool = false
private var currentOrientation: PresentationCallVideoView.Orientation = .rotation0
private var currentAspect: CGFloat = 1.0
fileprivate let videoSource: AdaptedCallVideoSource
private var disposable: Disposable?
fileprivate let videoLayer: PrivateCallVideoLayer
init(input: Signal<OngoingGroupCallContext.VideoFrameData, NoError>) {
self.videoLayer = PrivateCallVideoLayer()
self.videoLayer.masksToBounds = true
self.videoSource = AdaptedCallVideoSource(videoStreamSignal: input)
super.init(frame: CGRect())
self.layer.addSublayer(self.videoLayer)
self.disposable = self.videoSource.addOnUpdated { [weak self] in
guard let self else {
return
}
let videoOutput = self.videoSource.currentOutput
self.videoLayer.video = videoOutput
var notifyOrientationUpdated = false
var notifyIsMirroredUpdated = false
if !self.didReportFirstFrame {
notifyOrientationUpdated = true
notifyIsMirroredUpdated = true
}
if let currentOutput = videoOutput {
let currentAspect: CGFloat
if currentOutput.resolution.height > 0.0 {
currentAspect = currentOutput.resolution.width / currentOutput.resolution.height
} else {
currentAspect = 1.0
}
if self.currentAspect != currentAspect {
self.currentAspect = currentAspect
notifyOrientationUpdated = true
}
let currentOrientation: PresentationCallVideoView.Orientation
if currentOutput.followsDeviceOrientation {
currentOrientation = .rotation0
} else {
if abs(currentOutput.rotationAngle - 0.0) < .ulpOfOne {
currentOrientation = .rotation0
} else if abs(currentOutput.rotationAngle - Float.pi * 0.5) < .ulpOfOne {
currentOrientation = .rotation90
} else if abs(currentOutput.rotationAngle - Float.pi) < .ulpOfOne {
currentOrientation = .rotation180
} else if abs(currentOutput.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne {
currentOrientation = .rotation270
} else {
currentOrientation = .rotation0
}
}
if self.currentOrientation != currentOrientation {
self.currentOrientation = currentOrientation
notifyOrientationUpdated = true
}
let currentIsMirrored = !currentOutput.mirrorDirection.isEmpty
if self.currentIsMirrored != currentIsMirrored {
self.currentIsMirrored = currentIsMirrored
notifyIsMirroredUpdated = true
}
}
if !self.didReportFirstFrame {
self.didReportFirstFrame = true
self.onFirstFrameReceived?(Float(self.currentAspect))
}
if notifyOrientationUpdated {
self.onOrientationUpdated?(self.currentOrientation, self.currentAspect)
}
if notifyIsMirroredUpdated {
self.onIsMirroredUpdated?(self.currentIsMirrored)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable?.dispose()
}
func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) {
self.onFirstFrameReceived = f
self.didReportFirstFrame = false
}
func setOnOrientationUpdated(_ f: @escaping (PresentationCallVideoView.Orientation, CGFloat) -> Void) {
self.onOrientationUpdated = f
}
func getOrientation() -> PresentationCallVideoView.Orientation {
return self.currentOrientation
}
func getAspect() -> CGFloat {
return self.currentAspect
}
func setOnIsMirroredUpdated(_ f: @escaping (Bool) -> Void) {
self.onIsMirroredUpdated = f
}
func updateIsEnabled(_ isEnabled: Bool) {
self.isEnabled = isEnabled
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
if let currentOutput = self.videoSource.currentOutput {
let rotatedResolution = currentOutput.resolution
let videoSize = size
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
let rotatedVideoResolution = videoResolution
transition.updateFrame(layer: self.videoLayer, frame: CGRect(origin: CGPoint(), size: size))
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
}
}
}
private final class CallScreenVideoBlurView: UIView, VideoRenderingView {
private weak var mainView: CallScreenVideoView?
private let blurredLayer: MetalEngineSubjectLayer
init(mainView: CallScreenVideoView) {
self.mainView = mainView
self.blurredLayer = mainView.videoLayer.blurredLayer
super.init(frame: CGRect())
self.layer.addSublayer(self.blurredLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) {
}
func setOnOrientationUpdated(_ f: @escaping (PresentationCallVideoView.Orientation, CGFloat) -> Void) {
}
func getOrientation() -> PresentationCallVideoView.Orientation {
return .rotation0
}
func getAspect() -> CGFloat {
return 1.0
}
func setOnIsMirroredUpdated(_ f: @escaping (Bool) -> Void) {
}
func updateIsEnabled(_ isEnabled: Bool) {
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(layer: self.blurredLayer, frame: CGRect(origin: CGPoint(), size: size))
}
}

View File

@ -2418,7 +2418,7 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
} }
} else { } else {
if let input = (strongSelf.call as! PresentationGroupCallImpl).video(endpointId: endpointId) { if let input = (strongSelf.call as! PresentationGroupCallImpl).video(endpointId: endpointId) {
if let videoView = strongSelf.videoRenderingContext.makeView(input: input) { if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) {
completion(GroupVideoNode(videoView: videoView, backdropVideoView: strongSelf.videoRenderingContext.makeBlurView(input: input, mainView: videoView))) completion(GroupVideoNode(videoView: videoView, backdropVideoView: strongSelf.videoRenderingContext.makeBlurView(input: input, mainView: videoView)))
} }
} }
@ -3738,7 +3738,7 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
var isFrontCamera = true var isFrontCamera = true
let videoCapturer = OngoingCallVideoCapturer() let videoCapturer = OngoingCallVideoCapturer()
let input = videoCapturer.video() let input = videoCapturer.video()
if let videoView = strongSelf.videoRenderingContext.makeView(input: input) { if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) {
videoView.updateIsEnabled(true) videoView.updateIsEnabled(true)
let cameraNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil) let cameraNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil)
@ -5514,7 +5514,7 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
self.requestedVideoSources.insert(channel.endpointId) self.requestedVideoSources.insert(channel.endpointId)
let input = (self.call as! PresentationGroupCallImpl).video(endpointId: channel.endpointId) let input = (self.call as! PresentationGroupCallImpl).video(endpointId: channel.endpointId)
if let input = input, let videoView = self.videoRenderingContext.makeView(input: input) { if let input = input, let videoView = self.videoRenderingContext.makeView(input: input, blur: false) {
let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: self.videoRenderingContext.makeBlurView(input: input, mainView: videoView)) let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: self.videoRenderingContext.makeBlurView(input: input, mainView: videoView))
self.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue()))) self.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue())))
@ -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

@ -180,6 +180,29 @@ public final class PeerListItemComponent: Component {
} }
} }
public final class ExtractedTheme: Equatable {
public let inset: CGFloat
public let background: UIColor
public init(inset: CGFloat, background: UIColor) {
self.inset = inset
self.background = background
}
public static func ==(lhs: ExtractedTheme, rhs: ExtractedTheme) -> Bool {
if lhs === rhs {
return true
}
if lhs.inset != rhs.inset {
return false
}
if lhs.background != rhs.background {
return false
}
return true
}
}
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
@ -202,7 +225,8 @@ public final class PeerListItemComponent: Component {
let selectionPosition: SelectionPosition let selectionPosition: SelectionPosition
let isEnabled: Bool let isEnabled: Bool
let hasNext: Bool let hasNext: Bool
let action: (EnginePeer, EngineMessage.Id?, UIView?) -> Void let extractedTheme: ExtractedTheme?
let action: (EnginePeer, EngineMessage.Id?, PeerListItemComponent.View) -> Void
let inlineActions: InlineActionsState? let inlineActions: InlineActionsState?
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
let openStories: ((EnginePeer, AvatarNode) -> Void)? let openStories: ((EnginePeer, AvatarNode) -> Void)?
@ -230,7 +254,8 @@ public final class PeerListItemComponent: Component {
selectionPosition: SelectionPosition = .left, selectionPosition: SelectionPosition = .left,
isEnabled: Bool = true, isEnabled: Bool = true,
hasNext: Bool, hasNext: Bool,
action: @escaping (EnginePeer, EngineMessage.Id?, UIView?) -> Void, extractedTheme: ExtractedTheme? = nil,
action: @escaping (EnginePeer, EngineMessage.Id?, PeerListItemComponent.View) -> Void,
inlineActions: InlineActionsState? = nil, inlineActions: InlineActionsState? = nil,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
@ -257,6 +282,7 @@ public final class PeerListItemComponent: Component {
self.selectionPosition = selectionPosition self.selectionPosition = selectionPosition
self.isEnabled = isEnabled self.isEnabled = isEnabled
self.hasNext = hasNext self.hasNext = hasNext
self.extractedTheme = extractedTheme
self.action = action self.action = action
self.inlineActions = inlineActions self.inlineActions = inlineActions
self.contextAction = contextAction self.contextAction = contextAction
@ -337,7 +363,7 @@ public final class PeerListItemComponent: Component {
} }
public final class View: ContextControllerSourceView, ListSectionComponent.ChildView { public final class View: ContextControllerSourceView, ListSectionComponent.ChildView {
private let extractedContainerView: ContextExtractedContentContainingView public let extractedContainerView: ContextExtractedContentContainingView
private let containerButton: HighlightTrackingButton private let containerButton: HighlightTrackingButton
private let swipeOptionContainer: ListItemSwipeOptionContainer private let swipeOptionContainer: ListItemSwipeOptionContainer
@ -432,8 +458,16 @@ public final class PeerListItemComponent: Component {
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
let extractedBackgroundColor: UIColor
if let extractedTheme = component.extractedTheme {
extractedBackgroundColor = extractedTheme.background
} else {
extractedBackgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
}
self.containerButton.clipsToBounds = value self.containerButton.clipsToBounds = value
self.containerButton.backgroundColor = value ? component.theme.rootController.navigationBar.blurredBackgroundColor : nil self.containerButton.backgroundColor = value ? extractedBackgroundColor : nil
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0 self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
} }
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
@ -500,7 +534,7 @@ public final class PeerListItemComponent: Component {
guard let component = self.component, let peer = component.peer else { guard let component = self.component, let peer = component.peer else {
return return
} }
component.action(peer, component.message?.id, self.imageNode?.view) component.action(peer, component.message?.id, self)
} }
@objc private func avatarButtonPressed() { @objc private func avatarButtonPressed() {
@ -620,7 +654,16 @@ public final class PeerListItemComponent: Component {
labelData = ("", .neutral) labelData = ("", .neutral)
} }
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0 let contextInset: CGFloat
if self.isExtractedToContextMenu {
if let extractedTheme = component.extractedTheme {
contextInset = extractedTheme.inset
} else {
contextInset = 12.0
}
} else {
contextInset = 0.0
}
let height: CGFloat let height: CGFloat
let titleFont: UIFont let titleFont: UIFont
@ -1104,10 +1147,11 @@ public final class PeerListItemComponent: Component {
if let rightAccessoryComponentViewImpl = self.rightAccessoryComponentView?.view, let rightAccessoryComponentSize { if let rightAccessoryComponentViewImpl = self.rightAccessoryComponentView?.view, let rightAccessoryComponentSize {
var rightAccessoryComponentTransition = transition var rightAccessoryComponentTransition = transition
if rightAccessoryComponentViewImpl.superview == nil { if rightAccessoryComponentViewImpl.superview == nil {
rightAccessoryComponentViewImpl.isUserInteractionEnabled = false
rightAccessoryComponentTransition = rightAccessoryComponentTransition.withAnimation(.none) rightAccessoryComponentTransition = rightAccessoryComponentTransition.withAnimation(.none)
self.containerButton.addSubview(rightAccessoryComponentViewImpl) self.containerButton.addSubview(rightAccessoryComponentViewImpl)
} }
rightAccessoryComponentTransition.setFrame(view: rightAccessoryComponentViewImpl, frame: CGRect(origin: CGPoint(x: availableSize.width - rightAccessoryComponentSize.width, y: floor((height - verticalInset * 2.0 - rightAccessoryComponentSize.width) / 2.0)), size: rightAccessoryComponentSize)) rightAccessoryComponentTransition.setFrame(view: rightAccessoryComponentViewImpl, frame: CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + component.sideInset) - rightAccessoryComponentSize.width, y: floor((height - verticalInset * 2.0 - rightAccessoryComponentSize.width) / 2.0)), size: rightAccessoryComponentSize))
} }
var reactionIconTransition = transition var reactionIconTransition = transition

View File

@ -589,7 +589,7 @@ final class StoryItemSetViewListComponent: Component {
message: item.message, message: item.message,
selectionState: .none, selectionState: .none,
hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil, hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil,
action: { [weak self] peer, messageId, sourceView in action: { [weak self] peer, messageId, itemView in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
@ -598,7 +598,7 @@ final class StoryItemSetViewListComponent: Component {
} }
if let messageId { if let messageId {
component.openMessage(peer, messageId) component.openMessage(peer, messageId)
} else if let storyItem, let sourceView { } else if let storyItem, let sourceView = itemView.imageNode?.view {
component.openReposts(peer, storyItem.id, sourceView) component.openReposts(peer, storyItem.id, sourceView)
} else { } else {
component.openPeer(peer) component.openPeer(peer)

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")

View File

@ -730,6 +730,7 @@ public class TranslateScreen: ViewController {
inputHeight: layout.inputHeight ?? 0.0, inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics, metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics, deviceMetrics: layout.deviceMetrics,
orientation: layout.metrics.orientation,
isVisible: self.currentIsVisible, isVisible: self.currentIsVisible,
theme: self.theme ?? self.presentationData.theme, theme: self.theme ?? self.presentationData.theme,
strings: self.presentationData.strings, strings: self.presentationData.strings,