Video chat improvements

This commit is contained in:
Isaac 2024-09-25 01:14:28 +08:00
parent 243d0be940
commit 3d509c7bda
13 changed files with 315 additions and 28 deletions

View File

@ -212,6 +212,7 @@ public struct PresentationGroupCallState: Equatable {
public var subscribedToScheduled: Bool
public var isVideoEnabled: Bool
public var isVideoWatchersLimitReached: Bool
public var hasVideo: Bool
public init(
myPeerId: EnginePeer.Id,
@ -226,7 +227,8 @@ public struct PresentationGroupCallState: Equatable {
scheduleTimestamp: Int32?,
subscribedToScheduled: Bool,
isVideoEnabled: Bool,
isVideoWatchersLimitReached: Bool
isVideoWatchersLimitReached: Bool,
hasVideo: Bool
) {
self.myPeerId = myPeerId
self.networkState = networkState
@ -241,6 +243,7 @@ public struct PresentationGroupCallState: Equatable {
self.subscribedToScheduled = subscribedToScheduled
self.isVideoEnabled = isVideoEnabled
self.isVideoWatchersLimitReached = isVideoWatchersLimitReached
self.hasVideo = hasVideo
}
}

View File

@ -115,6 +115,7 @@ swift_library(
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/TelegramUI/Components/BackButtonComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/DirectMediaImageCache",
"//submodules/FastBlur",
],

View File

@ -268,7 +268,8 @@ private extension PresentationGroupCallState {
scheduleTimestamp: scheduleTimestamp,
subscribedToScheduled: subscribedToScheduled,
isVideoEnabled: false,
isVideoWatchersLimitReached: false
isVideoWatchersLimitReached: false,
hasVideo: false
)
}
}
@ -2971,11 +2972,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.updateLocalVideoState()
}
self.stateValue.hasVideo = self.hasVideo
}
public func disableVideo() {
self.hasVideo = false
self.useFrontCamera = true;
self.useFrontCamera = true
if let _ = self.videoCapturer {
self.videoCapturer = nil
self.isVideoMutedDisposable.set(nil)
@ -2984,6 +2986,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.updateLocalVideoState()
}
self.stateValue.hasVideo = self.hasVideo
}
private func updateLocalVideoState() {

View File

@ -13,6 +13,10 @@ import BalancedTextComponent
import TelegramPresentationData
import TelegramStringFormatting
import Markdown
import HierarchyTrackingLayer
private let purple = UIColor(rgb: 0x3252ef)
private let pink = UIColor(rgb: 0xef436c)
private final class ScheduleVideoChatSheetContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -33,7 +37,11 @@ private final class ScheduleVideoChatSheetContentComponent: Component {
}
final class View: UIView {
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private let button = ComponentView<Empty>()
private let buttonBackgroundLayer: SimpleGradientLayer
private let cancelButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
@ -52,7 +60,29 @@ private final class ScheduleVideoChatSheetContentComponent: Component {
self.dateFormatter.dateStyle = .short
self.dateFormatter.timeZone = TimeZone.current
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
self.buttonBackgroundLayer = SimpleGradientLayer()
self.buttonBackgroundLayer.type = .radial
self.buttonBackgroundLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
self.buttonBackgroundLayer.locations = [0.0, 0.85, 1.0]
self.buttonBackgroundLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
let radius = CGSize(width: 1.0, height: 2.0)
let endEndPoint = CGPoint(x: (self.buttonBackgroundLayer.startPoint.x + radius.width) * 1.0, y: (self.buttonBackgroundLayer.startPoint.y + radius.height) * 1.0)
self.buttonBackgroundLayer.endPoint = endEndPoint
self.buttonBackgroundLayer.cornerRadius = 10.0
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in
guard let self else {
return
}
if value {
self.updateAnimations()
}
}
}
required init?(coder: NSCoder) {
@ -96,6 +126,46 @@ private final class ScheduleVideoChatSheetContentComponent: Component {
}
}
private func updateAnimations() {
if let _ = self.buttonBackgroundLayer.animation(forKey: "movement") {
} else {
let previousValue = self.buttonBackgroundLayer.startPoint
let previousEndValue = self.buttonBackgroundLayer.endPoint
let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45))
self.buttonBackgroundLayer.startPoint = newValue
let radius = CGSize(width: 1.0, height: 2.0)
let newEndValue = CGPoint(x: (self.buttonBackgroundLayer.startPoint.x + radius.width) * 1.0, y: (self.buttonBackgroundLayer.startPoint.y + radius.height) * 1.0)
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "startPoint")
animation.duration = Double.random(in: 0.8 ..< 1.4)
animation.fromValue = previousValue
animation.toValue = newValue
CATransaction.setCompletionBlock { [weak self] in
guard let self else {
return
}
if self.hierarchyTrackingLayer.isInHierarchy {
self.updateAnimations()
}
}
self.buttonBackgroundLayer.add(animation, forKey: "movement")
let endAnimation = CABasicAnimation(keyPath: "endPoint")
endAnimation.duration = animation.duration
endAnimation.fromValue = previousEndValue
endAnimation.toValue = newEndValue
self.buttonBackgroundLayer.add(animation, forKey: "movementEnd")
CATransaction.commit()
}
}
func update(component: ScheduleVideoChatSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
let _ = previousComponent
@ -233,9 +303,9 @@ private final class ScheduleVideoChatSheetContentComponent: Component {
transition: buttonTransition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: UIColor(rgb: 0x3252EF),
color: .clear,
foreground: .white,
pressedColor: UIColor(rgb: 0x3252EF).withMultipliedAlpha(0.8)
pressedColor: UIColor(white: 1.0, alpha: 0.1)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
HStack(buttonContents, spacing: 5.0)
@ -256,8 +326,10 @@ private final class ScheduleVideoChatSheetContentComponent: Component {
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.layer.addSublayer(self.buttonBackgroundLayer)
self.addSubview(buttonView)
}
transition.setFrame(layer: self.buttonBackgroundLayer, frame: buttonFrame)
transition.setFrame(view: buttonView, frame: buttonFrame)
}
contentHeight += buttonSize.height
@ -302,6 +374,8 @@ private final class ScheduleVideoChatSheetContentComponent: Component {
contentHeight += environment.safeInsets.bottom + 14.0
}
self.updateAnimations()
return CGSize(width: availableSize.width, height: contentHeight)
}
}

View File

@ -34,11 +34,13 @@ final class VideoChatActionButtonComponent: Component {
case audio(audio: Audio)
case video
case leave
case switchVideo
}
case audio(audio: Audio)
case video(isActive: Bool)
case leave
case switchVideo
fileprivate var iconType: IconType {
switch self {
@ -57,6 +59,8 @@ final class VideoChatActionButtonComponent: Component {
return .video
case .leave:
return .leave
case .switchVideo:
return .switchVideo
}
}
}
@ -174,6 +178,19 @@ final class VideoChatActionButtonComponent: Component {
backgroundColor = UIColor(rgb: 0x3252EF)
}
iconDiameter = 60.0
case .switchVideo:
titleText = ""
switch component.microphoneState {
case .connecting:
backgroundColor = UIColor(white: 0.1, alpha: 1.0)
case .muted:
backgroundColor = UIColor(rgb: 0x027FFF)
case .unmuted:
backgroundColor = UIColor(rgb: 0x34C659)
case .raiseHand, .scheduled:
backgroundColor = UIColor(rgb: 0x3252EF)
}
iconDiameter = 54.0
case .leave:
titleText = "leave"
backgroundColor = UIColor(rgb: 0x47191E)
@ -204,6 +221,8 @@ final class VideoChatActionButtonComponent: Component {
self.contentImage = UIImage(bundleImageName: iconName)?.precomposed().withRenderingMode(.alwaysTemplate)
case .video:
self.contentImage = UIImage(bundleImageName: "Call/CallCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate)
case .switchVideo:
self.contentImage = UIImage(bundleImageName: "Call/CallSwitchCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate)
case .leave:
self.contentImage = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
@ -275,7 +294,9 @@ final class VideoChatActionButtonComponent: Component {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
transition.setPosition(view: iconView, position: iconFrame.center)
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setScale(view: iconView, scale: availableSize.width / 56.0)
}
return size

View File

@ -62,6 +62,7 @@ final class VideoChatMuteIconComponent: Component {
self.isUpdating = false
}
let previousComponent = self.component
self.component = component
if case let .mute(isFilled, isMuted) = component.content {
@ -77,7 +78,10 @@ final class VideoChatMuteIconComponent: Component {
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)
if let previousComponent, previousComponent.content == component.content, previousComponent.color == component.color {
} else {
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

View File

@ -136,6 +136,7 @@ final class VideoChatParticipantAvatarComponent: Component {
let peer: EnginePeer
let myPeerId: EnginePeer.Id
let isSpeaking: Bool
let isMutedForMe: Bool
let theme: PresentationTheme
init(
@ -143,12 +144,14 @@ final class VideoChatParticipantAvatarComponent: Component {
peer: EnginePeer,
myPeerId: EnginePeer.Id,
isSpeaking: Bool,
isMutedForMe: Bool,
theme: PresentationTheme
) {
self.call = call
self.peer = peer
self.myPeerId = myPeerId
self.isSpeaking = isSpeaking
self.isMutedForMe = isMutedForMe
self.theme = theme
}
@ -159,10 +162,13 @@ final class VideoChatParticipantAvatarComponent: Component {
if lhs.peer != rhs.peer {
return false
}
if lhs.myPeerId != rhs.myPeerId {
return false
}
if lhs.isSpeaking != rhs.isSpeaking {
return false
}
if lhs.myPeerId != rhs.myPeerId {
if lhs.isMutedForMe != rhs.isMutedForMe {
return false
}
if lhs.theme !== rhs.theme {
@ -259,7 +265,15 @@ final class VideoChatParticipantAvatarComponent: Component {
} else {
tintTransition = .immediate
}
tintTransition.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor)
let tintColor: UIColor
if component.isMutedForMe {
tintColor = UIColor(rgb: 0xff3b30)
} else if component.isSpeaking {
tintColor = UIColor(rgb: 0x33C758)
} else {
tintColor = component.theme.list.itemAccentColor
}
tintTransition.setTintColor(layer: blobView.blobsLayer, color: tintColor)
}
if component.peer.smallProfileImage != nil {
@ -362,7 +376,15 @@ final class VideoChatParticipantAvatarComponent: Component {
avatarNode.layer.transform = CATransform3DMakeScale(1.0 + additionalScale, 1.0 + additionalScale, 1.0)
}
ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor)
let tintColor: UIColor
if component.isMutedForMe {
tintColor = UIColor(rgb: 0xff3b30)
} else if component.isSpeaking {
tintColor = UIColor(rgb: 0x33C758)
} else {
tintColor = component.theme.list.itemAccentColor
}
ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: tintColor)
}
if blobView.alpha == 0.0 {

View File

@ -121,7 +121,9 @@ final class VideoChatParticipantStatusComponent: Component {
}
if let iconView = muteStatusView.iconView {
let iconTintColor: UIColor
if component.isSpeaking {
if let muteState = component.muteState, muteState.mutedByYou {
iconTintColor = UIColor(rgb: 0xff3b30)
} else if component.isSpeaking {
iconTintColor = UIColor(rgb: 0x33C758)
} else {
if let muteState = component.muteState {

View File

@ -352,7 +352,10 @@ final class VideoChatParticipantVideoComponent: Component {
alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha)
}
let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription
var videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription
if component.isPresentation && component.isMyPeer {
videoDescription = nil
}
var isEffectivelyPaused = false
if let videoDescription, videoDescription.isPaused {

View File

@ -1154,9 +1154,16 @@ final class VideoChatParticipantsComponent: Component {
let itemFrame = itemLayout.listItemFrame(at: i)
var isMutedForMe = false
if let muteState = participant.muteState, muteState.mutedByYou {
isMutedForMe = true
}
let subtitle: PeerListItemComponent.Subtitle
if participant.peer.id == component.call.accountContext.account.peerId {
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
} else if let muteState = participant.muteState, muteState.mutedByYou {
subtitle = PeerListItemComponent.Subtitle(text: "muted for you", color: .destructive)
} else if component.speakingParticipants.contains(participant.peer.id) {
if let volume = participant.volume, volume != 10000 {
subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive)
@ -1190,6 +1197,7 @@ final class VideoChatParticipantsComponent: Component {
peer: EnginePeer(participant.peer),
myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId,
isSpeaking: component.speakingParticipants.contains(participant.peer.id),
isMutedForMe: isMutedForMe,
theme: component.theme
)),
peer: EnginePeer(participant.peer),

View File

@ -6,6 +6,7 @@ import MultilineTextComponent
import TelegramPresentationData
import TelegramStringFormatting
import HierarchyTrackingLayer
import AnimatedTextComponent
private let purple = UIColor(rgb: 0x3252ef)
private let pink = UIColor(rgb: 0xef436c)
@ -13,6 +14,33 @@ private let pink = UIColor(rgb: 0xef436c)
private let latePurple = UIColor(rgb: 0x974aa9)
private let latePink = UIColor(rgb: 0xf0436c)
private func textItemsForTimeout(value: Int32) -> [AnimatedTextComponent.Item] {
if value < 3600 {
let minutes = value / 60
let seconds = value % 60
var items: [AnimatedTextComponent.Item] = []
items.append(AnimatedTextComponent.Item(id: AnyHashable(11), content: .number(Int(minutes), minDigits: 1)))
items.append(AnimatedTextComponent.Item(id: AnyHashable(12), content: .text(":")))
items.append(AnimatedTextComponent.Item(id: AnyHashable(13), content: .number(Int(seconds), minDigits: 2)))
return items
} else {
let hours = value / 3600
let minutes = (value % 3600) / 60
let seconds = value % 60
var items: [AnimatedTextComponent.Item] = []
items.append(AnimatedTextComponent.Item(id: AnyHashable(9), content: .number(Int(hours), minDigits: 1)))
items.append(AnimatedTextComponent.Item(id: AnyHashable(10), content: .text(":")))
items.append(AnimatedTextComponent.Item(id: AnyHashable(11), content: .number(Int(minutes), minDigits: 2)))
items.append(AnimatedTextComponent.Item(id: AnyHashable(12), content: .text(":")))
items.append(AnimatedTextComponent.Item(id: AnyHashable(13), content: .number(Int(seconds), minDigits: 2)))
return items
}
}
final class VideoChatScheduledInfoComponent: Component {
let timestamp: Int32
let strings: PresentationStrings
@ -46,8 +74,11 @@ final class VideoChatScheduledInfoComponent: Component {
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var component: VideoChatScheduledInfoComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var countdownTimer: Foundation.Timer?
override init(frame: CGRect) {
self.countdownContainerView = UIView()
self.countdownMaskView = UIView()
@ -76,6 +107,9 @@ final class VideoChatScheduledInfoComponent: Component {
}
if value {
self.updateAnimations()
} else {
self.countdownTimer?.invalidate()
self.countdownTimer = nil
}
}
}
@ -84,6 +118,10 @@ final class VideoChatScheduledInfoComponent: Component {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.countdownTimer?.invalidate()
}
private func updateAnimations() {
if let _ = self.countdownGradientLayer.animation(forKey: "movement") {
} else {
@ -110,6 +148,15 @@ final class VideoChatScheduledInfoComponent: Component {
self.countdownGradientLayer.add(animation, forKey: "movement")
CATransaction.commit()
}
if self.countdownTimer == nil {
self.countdownTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
self.state?.updated(transition: .easeInOut(duration: 0.2))
})
}
}
func update(component: VideoChatScheduledInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
@ -119,6 +166,7 @@ final class VideoChatScheduledInfoComponent: Component {
}
self.component = component
self.state = state
let titleSize = self.title.update(
transition: .immediate,
@ -130,21 +178,20 @@ final class VideoChatScheduledInfoComponent: Component {
)
let remainingSeconds: Int32 = max(0, component.timestamp - Int32(Date().timeIntervalSince1970))
let countdownText: String
var items: [AnimatedTextComponent.Item] = []
if remainingSeconds >= 86400 {
countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds)
let countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds)
items.append(AnimatedTextComponent.Item(id: AnyHashable(0), content: .text(countdownText)))
} else {
countdownText = textForTimeout(value: abs(remainingSeconds))
/*if remainingSeconds < 0 && !self.isLate {
self.isLate = true
self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor]
}*/
items = textItemsForTimeout(value: remainingSeconds)
}
let countdownTextSize = self.countdownText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: countdownText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white))
transition: transition,
component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]),
color: .white,
items: items
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0)

View File

@ -76,6 +76,7 @@ final class VideoChatScreenComponent: Component {
var navigationSidebarButton: ComponentView<Empty>?
let videoButton = ComponentView<Empty>()
var switchVideoButton: ComponentView<Empty>?
let leaveButton = ComponentView<Empty>()
let microphoneButton = ComponentView<Empty>()
@ -1316,10 +1317,17 @@ final class VideoChatScreenComponent: Component {
}
}
let actionButtonPlacementArea: (x: CGFloat, width: CGFloat)
if isTwoColumnLayout {
actionButtonPlacementArea = (availableSize.width - sideInset - mainColumnWidth, mainColumnWidth)
} else {
actionButtonPlacementArea = (0.0, availableSize.width)
}
let buttonsSideInset: CGFloat = 26.0
let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
let remainingButtonsSpace: CGFloat = actionButtonPlacementArea.width - buttonsSideInset * 2.0 - buttonsWidth
let effectiveMaxActionMicrophoneButtonSpacing: CGFloat
if areButtonsCollapsed {
@ -1355,7 +1363,7 @@ final class VideoChatScreenComponent: Component {
}
}
let microphoneButtonFrame: CGRect
var microphoneButtonFrame: CGRect
if areButtonsCollapsed {
microphoneButtonFrame = expandedMicrophoneButtonFrame
} else {
@ -1376,8 +1384,41 @@ final class VideoChatScreenComponent: Component {
expandedParticipantsClippingY = expandedMicrophoneButtonFrame.minY - 24.0
}
let leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
let rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
var leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
var rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
var additionalLeftActionButtonFrame: CGRect?
if let callState = self.callState, callState.hasVideo {
let additionalButtonDiameter: CGFloat
if areButtonsCollapsed {
additionalButtonDiameter = actionButtonDiameter
} else {
additionalButtonDiameter = floor(actionButtonDiameter * 0.64)
}
if areButtonsCollapsed {
let buttonCount: CGFloat = 4.0
let buttonsWidth: CGFloat = actionButtonDiameter * buttonCount
let remainingButtonsSpace: CGFloat = actionButtonPlacementArea.width - buttonsSideInset * 2.0 - buttonsWidth
let maxSpacing: CGFloat = 80.0
let effectiveSpacing = min(maxSpacing, floor(remainingButtonsSpace / (buttonCount - 1.0)))
let totalButtonsWidth: CGFloat = buttonsWidth + (buttonCount - 1.0) * effectiveSpacing
let totalButtonsX: CGFloat = actionButtonPlacementArea.x + floor((actionButtonPlacementArea.width - totalButtonsWidth) * 0.5)
additionalLeftActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(0.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
leftActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(1.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
microphoneButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(2.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
rightActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(3.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
} else {
let additionalButtonSpacing = 12.0
let totalLeftButtonHeight: CGFloat = leftActionButtonFrame.height + additionalButtonSpacing + additionalButtonDiameter
let totalLeftButtonOriginY: CGFloat = leftActionButtonFrame.minY + floor((leftActionButtonFrame.height - totalLeftButtonHeight) * 0.5)
leftActionButtonFrame.origin.y = totalLeftButtonOriginY + additionalButtonDiameter + additionalButtonSpacing
additionalLeftActionButtonFrame = CGRect(origin: CGPoint(x: leftActionButtonFrame.minX + floor((leftActionButtonFrame.width - additionalButtonDiameter) * 0.5), y: leftActionButtonFrame.minY - additionalButtonSpacing - additionalButtonDiameter), size: CGSize(width: additionalButtonDiameter, height: additionalButtonDiameter))
}
}
let participantsSize = availableSize
@ -1728,7 +1769,7 @@ final class VideoChatScreenComponent: Component {
videoButtonContent = .audio(audio: buttonAudio)
} else {
//TODO:release
videoButtonContent = .video(isActive: false)
videoButtonContent = .video(isActive: self.callState?.hasVideo ?? false)
}
let _ = self.videoButton.update(
transition: transition,
@ -1763,6 +1804,62 @@ final class VideoChatScreenComponent: Component {
transition.setBounds(view: videoButtonView, bounds: CGRect(origin: CGPoint(), size: leftActionButtonFrame.size))
}
if let additionalLeftActionButtonFrame {
let switchVideoButton: ComponentView<Empty>
var switchVideoButtonTransition = transition
if let current = self.switchVideoButton {
switchVideoButton = current
} else {
switchVideoButtonTransition = switchVideoButtonTransition.withAnimation(.none)
switchVideoButton = ComponentView()
self.switchVideoButton = switchVideoButton
}
let switchVideoButtonContent: VideoChatActionButtonComponent.Content = .switchVideo
let _ = switchVideoButton.update(
transition: switchVideoButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatActionButtonComponent(
strings: environment.strings,
content: switchVideoButtonContent,
microphoneState: actionButtonMicrophoneState,
isCollapsed: areButtonsCollapsed
)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.call.switchVideoCamera()
},
animateAlpha: false
)),
environment: {},
containerSize: additionalLeftActionButtonFrame.size
)
if let switchVideoButtonView = switchVideoButton.view {
var animateIn = false
if switchVideoButtonView.superview == nil {
self.containerView.addSubview(switchVideoButtonView)
animateIn = true
}
switchVideoButtonTransition.setFrame(view: switchVideoButtonView, frame: additionalLeftActionButtonFrame)
if animateIn {
alphaTransition.animateAlpha(view: switchVideoButtonView, from: 0.0, to: 1.0)
transition.animateScale(view: switchVideoButtonView, from: 0.001, to: 1.0)
}
}
} else if let switchVideoButton = self.switchVideoButton {
self.switchVideoButton = nil
if let switchVideoButtonView = switchVideoButton.view {
alphaTransition.setAlpha(view: switchVideoButtonView, alpha: 0.0, completion: { [weak switchVideoButtonView] _ in
switchVideoButtonView?.removeFromSuperview()
})
transition.setScale(view: switchVideoButtonView, scale: 0.001)
}
}
let _ = self.leaveButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(

View File

@ -169,6 +169,7 @@ public final class PeerListItemComponent: Component {
case neutral
case accent
case constructive
case destructive
}
public var text: String
@ -937,8 +938,9 @@ public final class PeerListItemComponent: Component {
case .accent:
labelColor = component.theme.list.itemAccentColor
case .constructive:
//TODO:release
labelColor = UIColor(rgb: 0x33C758)
case .destructive:
labelColor = UIColor(rgb: 0xff3b30)
}
var animateLabelDirection: Bool?