mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Video chat improvements
This commit is contained in:
parent
243d0be940
commit
3d509c7bda
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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?
|
||||
|
Loading…
x
Reference in New Issue
Block a user