mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
54d2340bbe
commit
762eb19cc1
@ -245,7 +245,7 @@ public func galleryItemForEntry(
|
|||||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||||
} else {
|
} else {
|
||||||
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
||||||
if NativeVideoContent.isHLSVideo(file: file), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming {
|
if NativeVideoContent.isHLSVideo(file: file) {
|
||||||
content = HLSVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
content = HLSVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||||
} else {
|
} else {
|
||||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||||
|
@ -0,0 +1,185 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import MultilineTextComponent
|
||||||
|
import AvatarNode
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AccountContext
|
||||||
|
import TelegramCore
|
||||||
|
import Markdown
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
|
final class VideoChatExpandedSpeakingToastComponent: Component {
|
||||||
|
let context: AccountContext
|
||||||
|
let peer: EnginePeer
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let action: (EnginePeer) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
peer: EnginePeer,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
action: @escaping (EnginePeer) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.peer = peer
|
||||||
|
self.strings = strings
|
||||||
|
self.theme = theme
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: VideoChatExpandedSpeakingToastComponent, rhs: VideoChatExpandedSpeakingToastComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.peer != rhs.peer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: HighlightTrackingButton {
|
||||||
|
private let background = ComponentView<Empty>()
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private var avatarNode: AvatarNode?
|
||||||
|
|
||||||
|
private var component: VideoChatExpandedSpeakingToastComponent?
|
||||||
|
private var isUpdating: Bool = false
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
if let component = self.component {
|
||||||
|
component.action(component.peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: VideoChatExpandedSpeakingToastComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
self.isUpdating = true
|
||||||
|
defer {
|
||||||
|
self.isUpdating = false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
let avatarLeftInset: CGFloat = 3.0
|
||||||
|
let avatarVerticalInset: CGFloat = 3.0
|
||||||
|
let avatarSpacing: CGFloat = 12.0
|
||||||
|
let rightInset: CGFloat = 16.0
|
||||||
|
let avatarWidth: CGFloat = 32.0
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
|
let bodyAttributes = MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white, additionalAttributes: [:])
|
||||||
|
let boldAttributes = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: .white, additionalAttributes: [:])
|
||||||
|
let titleText = addAttributesToStringWithRanges(component.strings.VoiceChat_ParticipantIsSpeaking(component.peer.displayTitle(strings: component.strings, displayOrder: presentationData.nameDisplayOrder))._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(titleText)
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - avatarLeftInset - avatarWidth - avatarSpacing - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let size = CGSize(width: avatarLeftInset + avatarWidth + avatarSpacing + titleSize.width + rightInset, height: avatarWidth + avatarVerticalInset * 2.0)
|
||||||
|
|
||||||
|
let _ = self.background.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(FilledRoundedRectangleComponent(
|
||||||
|
color: UIColor(white: 0.0, alpha: 0.9),
|
||||||
|
cornerRadius: size.height * 0.5,
|
||||||
|
smoothCorners: false
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: size
|
||||||
|
)
|
||||||
|
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
if let backgroundView = self.background.view {
|
||||||
|
if backgroundView.superview == nil {
|
||||||
|
backgroundView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(backgroundView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: avatarLeftInset + avatarWidth + avatarSpacing, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
titleView.layer.anchorPoint = CGPoint()
|
||||||
|
self.addSubview(titleView)
|
||||||
|
}
|
||||||
|
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||||
|
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarNode: AvatarNode
|
||||||
|
if let current = self.avatarNode {
|
||||||
|
avatarNode = current
|
||||||
|
} else {
|
||||||
|
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
|
||||||
|
self.avatarNode = avatarNode
|
||||||
|
self.addSubview(avatarNode.view)
|
||||||
|
avatarNode.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarSize = CGSize(width: avatarWidth, height: avatarWidth)
|
||||||
|
|
||||||
|
let clipStyle: AvatarNodeClipStyle
|
||||||
|
if case let .channel(channel) = component.peer, channel.flags.contains(.isForum) {
|
||||||
|
clipStyle = .roundedRect
|
||||||
|
} else {
|
||||||
|
clipStyle = .round
|
||||||
|
}
|
||||||
|
|
||||||
|
if component.peer.smallProfileImage != nil {
|
||||||
|
avatarNode.setPeerV2(
|
||||||
|
context: component.context,
|
||||||
|
theme: component.theme,
|
||||||
|
peer: component.peer,
|
||||||
|
authorOfMessage: nil,
|
||||||
|
overrideImage: nil,
|
||||||
|
emptyColor: nil,
|
||||||
|
clipStyle: .round,
|
||||||
|
synchronousLoad: false,
|
||||||
|
displayDimensions: avatarSize
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, clipStyle: clipStyle, synchronousLoad: false, displayDimensions: avatarSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: avatarVerticalInset), size: avatarSize)
|
||||||
|
transition.setPosition(view: avatarNode.view, position: avatarFrame.center)
|
||||||
|
transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
|
avatarNode.updateSize(size: avatarSize)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
@ -184,7 +184,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
case connecting
|
case connecting
|
||||||
case muted
|
case muted
|
||||||
case unmuted(pushToTalk: Bool)
|
case unmuted(pushToTalk: Bool)
|
||||||
case raiseHand
|
case raiseHand(isRaised: Bool)
|
||||||
case scheduled(state: ScheduledState)
|
case scheduled(state: ScheduledState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +226,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
private var disappearingBackgrounds: [UIImageView] = []
|
private var disappearingBackgrounds: [UIImageView] = []
|
||||||
private var progressIndicator: RadialStatusNode?
|
private var progressIndicator: RadialStatusNode?
|
||||||
private let title = ComponentView<Empty>()
|
private let title = ComponentView<Empty>()
|
||||||
|
private var subtitle: ComponentView<Empty>?
|
||||||
private let icon: VoiceChatActionButtonIconNode
|
private let icon: VoiceChatActionButtonIconNode
|
||||||
|
|
||||||
private var glowView: GlowView?
|
private var glowView: GlowView?
|
||||||
@ -322,6 +323,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
|
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
|
||||||
|
|
||||||
let titleText: String
|
let titleText: String
|
||||||
|
var subtitleText: String?
|
||||||
var isEnabled = true
|
var isEnabled = true
|
||||||
switch component.content {
|
switch component.content {
|
||||||
case .connecting:
|
case .connecting:
|
||||||
@ -331,8 +333,14 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
titleText = "Unmute"
|
titleText = "Unmute"
|
||||||
case let .unmuted(isPushToTalk):
|
case let .unmuted(isPushToTalk):
|
||||||
titleText = isPushToTalk ? "You are Live" : "Tap to Mute"
|
titleText = isPushToTalk ? "You are Live" : "Tap to Mute"
|
||||||
case .raiseHand:
|
case let .raiseHand(isRaised):
|
||||||
titleText = "Raise Hand"
|
if isRaised {
|
||||||
|
titleText = "You asked to speak"
|
||||||
|
subtitleText = "We let the speakers know"
|
||||||
|
} else {
|
||||||
|
titleText = "Muted by Admin"
|
||||||
|
subtitleText = "Tap if you want to speak"
|
||||||
|
}
|
||||||
case let .scheduled(state):
|
case let .scheduled(state):
|
||||||
switch state {
|
switch state {
|
||||||
case .start:
|
case .start:
|
||||||
@ -353,7 +361,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white))
|
text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white))
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 120.0, height: 100.0)
|
containerSize: CGSize(width: 180.0, height: 100.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
let size = CGSize(width: availableSize.width, height: availableSize.height)
|
let size = CGSize(width: availableSize.width, height: availableSize.height)
|
||||||
@ -470,7 +478,10 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
transition.setScale(view: disappearingBackground, scale: size.width / 116.0)
|
transition.setScale(view: disappearingBackground, scale: size.width / 116.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize)
|
var titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize)
|
||||||
|
if subtitleText != nil {
|
||||||
|
titleFrame.origin.y -= 5.0
|
||||||
|
}
|
||||||
if let titleView = self.title.view {
|
if let titleView = self.title.view {
|
||||||
if titleView.superview == nil {
|
if titleView.superview == nil {
|
||||||
titleView.isUserInteractionEnabled = false
|
titleView.isUserInteractionEnabled = false
|
||||||
@ -481,6 +492,47 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0)
|
alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let subtitleText {
|
||||||
|
let subtitle: ComponentView<Empty>
|
||||||
|
var subtitleTransition = transition
|
||||||
|
if let current = self.subtitle {
|
||||||
|
subtitle = current
|
||||||
|
} else {
|
||||||
|
subtitleTransition = subtitleTransition.withAnimation(.none)
|
||||||
|
subtitle = ComponentView()
|
||||||
|
self.subtitle = subtitle
|
||||||
|
}
|
||||||
|
let subtitleSize = subtitle.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(13.0), textColor: .white))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 180.0, height: 100.0)
|
||||||
|
)
|
||||||
|
let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + 1.0), size: subtitleSize)
|
||||||
|
if let subtitleView = subtitle.view {
|
||||||
|
if subtitleView.superview == nil {
|
||||||
|
subtitleView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(subtitleView)
|
||||||
|
|
||||||
|
subtitleView.alpha = 0.0
|
||||||
|
transition.animateScale(view: subtitleView, from: 0.001, to: 1.0)
|
||||||
|
}
|
||||||
|
subtitleTransition.setPosition(view: subtitleView, position: subtitleFrame.center)
|
||||||
|
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
|
||||||
|
alphaTransition.setAlpha(view: subtitleView, alpha: component.isCollapsed ? 0.0 : 1.0)
|
||||||
|
}
|
||||||
|
} else if let subtitle = self.subtitle {
|
||||||
|
self.subtitle = nil
|
||||||
|
if let subtitleView = subtitle.view {
|
||||||
|
transition.setScale(view: subtitleView, scale: 0.001)
|
||||||
|
alphaTransition.setAlpha(view: subtitleView, alpha: 0.0, completion: { [weak subtitleView] _ in
|
||||||
|
subtitleView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.icon.view.superview == nil {
|
if self.icon.view.superview == nil {
|
||||||
self.icon.view.isUserInteractionEnabled = false
|
self.icon.view.isUserInteractionEnabled = false
|
||||||
self.addSubview(self.icon.view)
|
self.addSubview(self.icon.view)
|
||||||
|
@ -283,7 +283,7 @@ final class VideoChatParticipantAvatarComponent: Component {
|
|||||||
transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
avatarNode.updateSize(size: avatarSize)
|
avatarNode.updateSize(size: avatarSize)
|
||||||
|
|
||||||
let blobScale: CGFloat = 1.5
|
let blobScale: CGFloat = 2.0
|
||||||
|
|
||||||
if self.audioLevelDisposable == nil {
|
if self.audioLevelDisposable == nil {
|
||||||
struct Level {
|
struct Level {
|
||||||
|
@ -117,6 +117,13 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class EventCycleState {
|
||||||
|
var ignoreScrolling: Bool = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let call: PresentationGroupCall
|
let call: PresentationGroupCall
|
||||||
let participants: Participants?
|
let participants: Participants?
|
||||||
let speakingParticipants: Set<EnginePeer.Id>
|
let speakingParticipants: Set<EnginePeer.Id>
|
||||||
@ -132,6 +139,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
let updateIsMainParticipantPinned: (Bool) -> Void
|
let updateIsMainParticipantPinned: (Bool) -> Void
|
||||||
let updateIsExpandedUIHidden: (Bool) -> Void
|
let updateIsExpandedUIHidden: (Bool) -> Void
|
||||||
let openInviteMembers: () -> Void
|
let openInviteMembers: () -> Void
|
||||||
|
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
call: PresentationGroupCall,
|
call: PresentationGroupCall,
|
||||||
@ -148,7 +156,8 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
|
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
|
||||||
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
||||||
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
|
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
|
||||||
openInviteMembers: @escaping () -> Void
|
openInviteMembers: @escaping () -> Void,
|
||||||
|
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
|
||||||
) {
|
) {
|
||||||
self.call = call
|
self.call = call
|
||||||
self.participants = participants
|
self.participants = participants
|
||||||
@ -165,6 +174,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
|
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
|
||||||
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
|
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
|
||||||
self.openInviteMembers = openInviteMembers
|
self.openInviteMembers = openInviteMembers
|
||||||
|
self.visibleParticipantsUpdated = visibleParticipantsUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
|
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
|
||||||
@ -477,7 +487,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
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))
|
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 {
|
} else {
|
||||||
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height))
|
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.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX + layout.mainColumn.insets.left, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth - layout.mainColumn.insets.left - layout.mainColumn.insets.right, height: containerSize.height - layout.mainColumn.insets.top))
|
||||||
|
|
||||||
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: containerSize.height))
|
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))
|
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))
|
||||||
@ -599,6 +609,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
final class View: UIView, UIScrollViewDelegate {
|
final class View: UIView, UIScrollViewDelegate {
|
||||||
private let scrollViewClippingContainer: SolidRoundedCornersContainer
|
private let scrollViewClippingContainer: SolidRoundedCornersContainer
|
||||||
private let scrollView: ScrollView
|
private let scrollView: ScrollView
|
||||||
|
private let scrollViewBottomShadowView: UIImageView
|
||||||
|
|
||||||
private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer
|
private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer
|
||||||
private let separateVideoScrollView: ScrollView
|
private let separateVideoScrollView: ScrollView
|
||||||
@ -622,6 +633,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
private let expandedGridItemContainer: UIView
|
private let expandedGridItemContainer: UIView
|
||||||
private var expandedControlsView: ComponentView<Empty>?
|
private var expandedControlsView: ComponentView<Empty>?
|
||||||
private var expandedThumbnailsView: ComponentView<Empty>?
|
private var expandedThumbnailsView: ComponentView<Empty>?
|
||||||
|
private var expandedSpeakingToast: ComponentView<Empty>?
|
||||||
|
|
||||||
private var listItemViews: [EnginePeer.Id: ListItem] = [:]
|
private var listItemViews: [EnginePeer.Id: ListItem] = [:]
|
||||||
private let listItemViewContainer: UIView
|
private let listItemViewContainer: UIView
|
||||||
@ -635,9 +647,13 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
|
|
||||||
private var currentLoadMoreToken: String?
|
private var currentLoadMoreToken: String?
|
||||||
|
|
||||||
|
private var mainScrollViewEventCycleState: EventCycleState?
|
||||||
|
private var separateVideoScrollViewEventCycleState: EventCycleState?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.scrollViewClippingContainer = SolidRoundedCornersContainer()
|
self.scrollViewClippingContainer = SolidRoundedCornersContainer()
|
||||||
self.scrollView = ScrollView()
|
self.scrollView = ScrollView()
|
||||||
|
self.scrollViewBottomShadowView = UIImageView()
|
||||||
|
|
||||||
self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer()
|
self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer()
|
||||||
self.separateVideoScrollView = ScrollView()
|
self.separateVideoScrollView = ScrollView()
|
||||||
@ -687,6 +703,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
self.scrollViewClippingContainer.addSubview(self.scrollView)
|
self.scrollViewClippingContainer.addSubview(self.scrollView)
|
||||||
self.addSubview(self.scrollViewClippingContainer)
|
self.addSubview(self.scrollViewClippingContainer)
|
||||||
self.addSubview(self.scrollViewClippingContainer.cornersView)
|
self.addSubview(self.scrollViewClippingContainer.cornersView)
|
||||||
|
self.addSubview(self.scrollViewBottomShadowView)
|
||||||
|
|
||||||
self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView)
|
self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView)
|
||||||
self.addSubview(self.separateVideoScrollViewClippingContainer)
|
self.addSubview(self.separateVideoScrollViewClippingContainer)
|
||||||
@ -765,10 +782,46 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
if !self.ignoreScrolling {
|
if !self.ignoreScrolling {
|
||||||
|
if scrollView == self.scrollView {
|
||||||
|
if let eventCycleState = self.mainScrollViewEventCycleState {
|
||||||
|
if eventCycleState.ignoreScrolling {
|
||||||
|
self.ignoreScrolling = true
|
||||||
|
scrollView.contentOffset = CGPoint()
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if scrollView == self.separateVideoScrollView {
|
||||||
|
if let eventCycleState = self.separateVideoScrollViewEventCycleState {
|
||||||
|
if eventCycleState.ignoreScrolling {
|
||||||
|
self.ignoreScrolling = true
|
||||||
|
scrollView.contentOffset = CGPoint()
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.updateScrolling(transition: .immediate)
|
self.updateScrolling(transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||||
|
if scrollView == self.scrollView {
|
||||||
|
if let eventCycleState = self.mainScrollViewEventCycleState {
|
||||||
|
if eventCycleState.ignoreScrolling {
|
||||||
|
targetContentOffset.pointee.y = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if scrollView == self.separateVideoScrollView {
|
||||||
|
if let eventCycleState = self.separateVideoScrollViewEventCycleState {
|
||||||
|
if eventCycleState.ignoreScrolling {
|
||||||
|
targetContentOffset.pointee.y = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateScrolling(transition: ComponentTransition) {
|
private func updateScrolling(transition: ComponentTransition) {
|
||||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||||
return
|
return
|
||||||
@ -832,11 +885,18 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
var validGridItemIds: [VideoParticipantKey] = []
|
var validGridItemIds: [VideoParticipantKey] = []
|
||||||
var validGridItemIndices: [Int] = []
|
var validGridItemIndices: [Int] = []
|
||||||
|
|
||||||
|
var clippedScrollViewBounds = self.scrollView.bounds
|
||||||
|
clippedScrollViewBounds.origin.y += component.layout.mainColumn.insets.top
|
||||||
|
clippedScrollViewBounds.size.height -= component.layout.mainColumn.insets.top + component.layout.mainColumn.insets.bottom
|
||||||
|
|
||||||
let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
|
let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
|
||||||
|
let clippedVisibleGridItemRange: (minIndex: Int, maxIndex: Int)
|
||||||
if itemLayout.layout.videoColumn == nil {
|
if itemLayout.layout.videoColumn == nil {
|
||||||
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
|
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
|
||||||
|
clippedVisibleGridItemRange = itemLayout.visibleGridItemRange(for: clippedScrollViewBounds)
|
||||||
} else {
|
} else {
|
||||||
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
|
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
|
||||||
|
clippedVisibleGridItemRange = visibleGridItemRange
|
||||||
}
|
}
|
||||||
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
|
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
|
||||||
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
|
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
|
||||||
@ -852,6 +912,8 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
validGridItemIndices.append(index)
|
validGridItemIndices.append(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var visibleParticipants: [EnginePeer.Id] = []
|
||||||
|
|
||||||
for index in validGridItemIndices {
|
for index in validGridItemIndices {
|
||||||
let videoParticipant = self.gridParticipants[index]
|
let videoParticipant = self.gridParticipants[index]
|
||||||
@ -879,6 +941,10 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isItemExpanded || (index >= clippedVisibleGridItemRange.minIndex && index <= clippedVisibleGridItemRange.maxIndex) {
|
||||||
|
visibleParticipants.append(videoParticipant.key.id)
|
||||||
|
}
|
||||||
|
|
||||||
var suppressItemExpansionCollapseAnimation = false
|
var suppressItemExpansionCollapseAnimation = false
|
||||||
if isItemExpanded {
|
if isItemExpanded {
|
||||||
if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey {
|
if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey {
|
||||||
@ -1066,11 +1132,16 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
|
|
||||||
var validListItemIds: [EnginePeer.Id] = []
|
var validListItemIds: [EnginePeer.Id] = []
|
||||||
let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds)
|
let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds)
|
||||||
|
let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds)
|
||||||
if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex {
|
if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex {
|
||||||
for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex {
|
for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex {
|
||||||
let participant = self.listParticipants[i]
|
let participant = self.listParticipants[i]
|
||||||
validListItemIds.append(participant.peer.id)
|
validListItemIds.append(participant.peer.id)
|
||||||
|
|
||||||
|
if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex {
|
||||||
|
visibleParticipants.append(participant.peer.id)
|
||||||
|
}
|
||||||
|
|
||||||
var itemTransition = transition
|
var itemTransition = transition
|
||||||
let itemView: ListItem
|
let itemView: ListItem
|
||||||
if let current = self.listItemViews[participant.peer.id] {
|
if let current = self.listItemViews[participant.peer.id] {
|
||||||
@ -1087,9 +1158,15 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
if participant.peer.id == component.call.accountContext.account.peerId {
|
||||||
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
|
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
|
||||||
} else if component.speakingParticipants.contains(participant.peer.id) {
|
} else if component.speakingParticipants.contains(participant.peer.id) {
|
||||||
subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive)
|
if let volume = participant.volume, volume != 10000 {
|
||||||
|
subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive)
|
||||||
|
} else {
|
||||||
|
subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive)
|
||||||
|
}
|
||||||
|
} else if let about = participant.about, !about.isEmpty {
|
||||||
|
subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral)
|
||||||
} else {
|
} else {
|
||||||
subtitle = PeerListItemComponent.Subtitle(text: participant.about ?? "listening", color: .neutral)
|
subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral)
|
||||||
}
|
}
|
||||||
|
|
||||||
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
|
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
|
||||||
@ -1412,12 +1489,86 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let expandedVideoState = component.expandedVideoState, expandedVideoState.isMainParticipantPinned, let participants = component.participants, !component.speakingParticipants.isEmpty, let firstOther = component.speakingParticipants.first(where: { $0 != expandedVideoState.mainParticipant.id }), let speakingPeer = participants.participants.first(where: { $0.peer.id == firstOther })?.peer {
|
||||||
|
let expandedSpeakingToast: ComponentView<Empty>
|
||||||
|
var expandedSpeakingToastTransition = transition
|
||||||
|
if let current = self.expandedSpeakingToast {
|
||||||
|
expandedSpeakingToast = current
|
||||||
|
} else {
|
||||||
|
expandedSpeakingToastTransition = expandedSpeakingToastTransition.withAnimation(.none)
|
||||||
|
expandedSpeakingToast = ComponentView()
|
||||||
|
self.expandedSpeakingToast = expandedSpeakingToast
|
||||||
|
}
|
||||||
|
let expandedSpeakingToastSize = expandedSpeakingToast.update(
|
||||||
|
transition: expandedSpeakingToastTransition,
|
||||||
|
component: AnyComponent(VideoChatExpandedSpeakingToastComponent(
|
||||||
|
context: component.call.accountContext,
|
||||||
|
peer: EnginePeer(speakingPeer),
|
||||||
|
strings: component.strings,
|
||||||
|
theme: component.theme,
|
||||||
|
action: { [weak self] peer in
|
||||||
|
guard let self, let component = self.component, let participants = component.participants else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let participant = participants.participants.first(where: { $0.peer.id == peer.id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var key: VideoParticipantKey?
|
||||||
|
if participant.presentationDescription != nil {
|
||||||
|
key = VideoParticipantKey(id: peer.id, isPresentation: true)
|
||||||
|
} else if participant.videoDescription != nil {
|
||||||
|
key = VideoParticipantKey(id: peer.id, isPresentation: false)
|
||||||
|
}
|
||||||
|
if let key {
|
||||||
|
component.updateMainParticipant(key, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
|
||||||
|
)
|
||||||
|
let expandedSpeakingToastFrame = CGRect(origin: CGPoint(x: floor((itemLayout.expandedGrid.itemContainerFrame().size.width - expandedSpeakingToastSize.width) * 0.5), y: 44.0), size: expandedSpeakingToastSize)
|
||||||
|
if let expandedSpeakingToastView = expandedSpeakingToast.view {
|
||||||
|
var animateIn = false
|
||||||
|
if expandedSpeakingToastView.superview == nil {
|
||||||
|
animateIn = true
|
||||||
|
self.expandedGridItemContainer.addSubview(expandedSpeakingToastView)
|
||||||
|
}
|
||||||
|
expandedSpeakingToastTransition.setFrame(view: expandedSpeakingToastView, frame: expandedSpeakingToastFrame)
|
||||||
|
|
||||||
|
if animateIn {
|
||||||
|
alphaTransition.animateAlpha(view: expandedSpeakingToastView, from: 0.0, to: 1.0)
|
||||||
|
transition.animateScale(view: expandedSpeakingToastView, from: 0.6, to: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let expandedSpeakingToast = self.expandedSpeakingToast {
|
||||||
|
self.expandedSpeakingToast = nil
|
||||||
|
if let expandedSpeakingToastView = expandedSpeakingToast.view {
|
||||||
|
alphaTransition.setAlpha(view: expandedSpeakingToastView, alpha: 0.0, completion: { [weak expandedSpeakingToastView] _ in
|
||||||
|
expandedSpeakingToastView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
transition.setScale(view: expandedSpeakingToastView, scale: 0.6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let participants = component.participants, let loadMoreToken = participants.loadMoreToken, visibleListItemRange.maxIndex >= self.listParticipants.count - 5 {
|
if let participants = component.participants, let loadMoreToken = participants.loadMoreToken, visibleListItemRange.maxIndex >= self.listParticipants.count - 5 {
|
||||||
if self.currentLoadMoreToken != loadMoreToken {
|
if self.currentLoadMoreToken != loadMoreToken {
|
||||||
self.currentLoadMoreToken = loadMoreToken
|
self.currentLoadMoreToken = loadMoreToken
|
||||||
component.call.loadMoreMembers(token: loadMoreToken)
|
component.call.loadMoreMembers(token: loadMoreToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component.visibleParticipantsUpdated(Set(visibleParticipants))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEventCycleState(scrollView: UIScrollView, eventCycleState: EventCycleState?) {
|
||||||
|
if scrollView == self.scrollView {
|
||||||
|
self.mainScrollViewEventCycleState = eventCycleState
|
||||||
|
} else if scrollView == self.separateVideoScrollView {
|
||||||
|
self.separateVideoScrollViewEventCycleState = eventCycleState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
@ -1482,11 +1633,16 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||||
if let participants = component.participants {
|
if let participants = component.participants {
|
||||||
for participant in participants.participants {
|
for participant in participants.participants {
|
||||||
|
var isFullyMuted = false
|
||||||
|
if let muteState = participant.muteState, !muteState.canUnmute {
|
||||||
|
isFullyMuted = true
|
||||||
|
}
|
||||||
|
|
||||||
var hasVideo = false
|
var hasVideo = false
|
||||||
if participant.videoDescription != nil {
|
if participant.videoDescription != nil {
|
||||||
hasVideo = true
|
hasVideo = true
|
||||||
let videoParticipant = VideoParticipant(participant: participant, isPresentation: false)
|
let videoParticipant = VideoParticipant(participant: participant, isPresentation: false)
|
||||||
if participant.peer.id == component.call.accountContext.account.peerId || participant.peer.id == participants.myPeerId {
|
if participant.peer.id == participants.myPeerId {
|
||||||
gridParticipants.insert(videoParticipant, at: 0)
|
gridParticipants.insert(videoParticipant, at: 0)
|
||||||
} else {
|
} else {
|
||||||
gridParticipants.append(videoParticipant)
|
gridParticipants.append(videoParticipant)
|
||||||
@ -1495,14 +1651,14 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
if participant.presentationDescription != nil {
|
if participant.presentationDescription != nil {
|
||||||
hasVideo = true
|
hasVideo = true
|
||||||
let videoParticipant = VideoParticipant(participant: participant, isPresentation: true)
|
let videoParticipant = VideoParticipant(participant: participant, isPresentation: true)
|
||||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
if participant.peer.id == participants.myPeerId {
|
||||||
gridParticipants.insert(videoParticipant, at: 0)
|
gridParticipants.insert(videoParticipant, at: 0)
|
||||||
} else {
|
} else {
|
||||||
gridParticipants.append(videoParticipant)
|
gridParticipants.append(videoParticipant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasVideo || component.layout.videoColumn != nil {
|
if !hasVideo || component.layout.videoColumn != nil {
|
||||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
if participant.peer.id == participants.myPeerId && !isFullyMuted {
|
||||||
listParticipants.insert(participant, at: 0)
|
listParticipants.insert(participant, at: 0)
|
||||||
} else {
|
} else {
|
||||||
listParticipants.append(participant)
|
listParticipants.append(participant)
|
||||||
@ -1594,6 +1750,37 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
smoothCorners: false
|
smoothCorners: false
|
||||||
), transition: transition)
|
), transition: transition)
|
||||||
|
|
||||||
|
if self.scrollViewBottomShadowView.image == nil {
|
||||||
|
let height: CGFloat = 80.0
|
||||||
|
let baseGradientAlpha: CGFloat = 1.0
|
||||||
|
let numSteps = 8
|
||||||
|
let firstStep = 0
|
||||||
|
let firstLocation = 0.0
|
||||||
|
let colors = (0 ..< numSteps).map { i -> UIColor in
|
||||||
|
if i < firstStep {
|
||||||
|
return UIColor(white: 1.0, alpha: 1.0)
|
||||||
|
} else {
|
||||||
|
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||||
|
let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step)
|
||||||
|
return UIColor(white: 0.0, alpha: baseGradientAlpha * value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let locations = (0 ..< numSteps).map { i -> CGFloat in
|
||||||
|
if i < firstStep {
|
||||||
|
return 0.0
|
||||||
|
} else {
|
||||||
|
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||||
|
return (firstLocation + (1.0 - firstLocation) * step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrollViewBottomShadowView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })!.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(height - 1.0))
|
||||||
|
self.scrollViewBottomShadowView.tintColor = .black
|
||||||
|
}
|
||||||
|
let scrollViewBottomShadowOverflow: CGFloat = 30.0
|
||||||
|
let scrollViewBottomShadowFrame = CGRect(origin: CGPoint(x: itemLayout.scrollClippingFrame.minX, y: itemLayout.scrollClippingFrame.maxY - component.layout.mainColumn.insets.bottom - scrollViewBottomShadowOverflow), size: CGSize(width: itemLayout.scrollClippingFrame.width, height: component.layout.mainColumn.insets.bottom + scrollViewBottomShadowOverflow))
|
||||||
|
transition.setFrame(view: self.scrollViewBottomShadowView, frame: scrollViewBottomShadowFrame)
|
||||||
|
|
||||||
transition.setPosition(view: self.separateVideoScrollViewClippingContainer, position: itemLayout.separateVideoScrollClippingFrame.center)
|
transition.setPosition(view: self.separateVideoScrollViewClippingContainer, position: itemLayout.separateVideoScrollClippingFrame.center)
|
||||||
transition.setBounds(view: self.separateVideoScrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.separateVideoScrollClippingFrame.minX - itemLayout.separateVideoGridFrame.minX, y: itemLayout.separateVideoScrollClippingFrame.minY - itemLayout.separateVideoGridFrame.minY), size: itemLayout.separateVideoScrollClippingFrame.size))
|
transition.setBounds(view: self.separateVideoScrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.separateVideoScrollClippingFrame.minX - itemLayout.separateVideoGridFrame.minX, y: itemLayout.separateVideoScrollClippingFrame.minY - itemLayout.separateVideoGridFrame.minY), size: itemLayout.separateVideoScrollClippingFrame.size))
|
||||||
transition.setFrame(view: self.separateVideoScrollViewClippingContainer.cornersView, frame: itemLayout.separateVideoScrollClippingFrame)
|
transition.setFrame(view: self.separateVideoScrollViewClippingContainer.cornersView, frame: itemLayout.separateVideoScrollClippingFrame)
|
||||||
|
@ -41,15 +41,22 @@ final class VideoChatScreenComponent: Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PanGestureState {
|
private final class PanState {
|
||||||
var offsetFraction: CGFloat
|
var fraction: CGFloat
|
||||||
|
weak var scrollView: UIScrollView?
|
||||||
|
var startContentOffsetY: CGFloat = 0.0
|
||||||
|
var accumulatedOffset: CGFloat = 0.0
|
||||||
|
var dismissedTooltips: Bool = false
|
||||||
|
var didLockScrolling: Bool = false
|
||||||
|
var contentOffset: CGFloat?
|
||||||
|
|
||||||
init(offsetFraction: CGFloat) {
|
init(fraction: CGFloat, scrollView: UIScrollView?) {
|
||||||
self.offsetFraction = offsetFraction
|
self.fraction = fraction
|
||||||
|
self.scrollView = scrollView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class View: UIView {
|
final class View: UIView, UIGestureRecognizerDelegate {
|
||||||
let containerView: UIView
|
let containerView: UIView
|
||||||
|
|
||||||
var component: VideoChatScreenComponent?
|
var component: VideoChatScreenComponent?
|
||||||
@ -57,7 +64,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
weak var state: EmptyComponentState?
|
weak var state: EmptyComponentState?
|
||||||
var isUpdating: Bool = false
|
var isUpdating: Bool = false
|
||||||
|
|
||||||
private var panGestureState: PanGestureState?
|
private var verticalPanState: PanState?
|
||||||
var notifyDismissedInteractivelyOnPanGestureApply: Bool = false
|
var notifyDismissedInteractivelyOnPanGestureApply: Bool = false
|
||||||
var completionOnPanGestureApply: (() -> Void)?
|
var completionOnPanGestureApply: (() -> Void)?
|
||||||
|
|
||||||
@ -95,6 +102,9 @@ final class VideoChatScreenComponent: Component {
|
|||||||
var members: PresentationGroupCallMembers?
|
var members: PresentationGroupCallMembers?
|
||||||
var membersDisposable: Disposable?
|
var membersDisposable: Disposable?
|
||||||
|
|
||||||
|
var speakingParticipantPeers: [EnginePeer] = []
|
||||||
|
var visibleParticipants: Set<EnginePeer.Id> = Set()
|
||||||
|
|
||||||
let isPresentedValue = ValuePromise<Bool>(false, ignoreRepeated: true)
|
let isPresentedValue = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||||
var applicationStateDisposable: Disposable?
|
var applicationStateDisposable: Disposable?
|
||||||
|
|
||||||
@ -117,9 +127,11 @@ final class VideoChatScreenComponent: Component {
|
|||||||
|
|
||||||
self.addSubview(self.containerView)
|
self.addSubview(self.containerView)
|
||||||
|
|
||||||
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||||
|
panGestureRecognizer.delegate = self
|
||||||
|
self.addGestureRecognizer(panGestureRecognizer)
|
||||||
|
|
||||||
self.panGestureState = PanGestureState(offsetFraction: 1.0)
|
self.verticalPanState = PanState(fraction: 1.0, scrollView: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -139,37 +151,159 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func animateIn() {
|
func animateIn() {
|
||||||
self.panGestureState = PanGestureState(offsetFraction: 1.0)
|
self.verticalPanState = PanState(fraction: 1.0, scrollView: nil)
|
||||||
self.state?.updated(transition: .immediate)
|
self.state?.updated(transition: .immediate)
|
||||||
|
|
||||||
self.panGestureState = nil
|
self.verticalPanState = nil
|
||||||
self.state?.updated(transition: .spring(duration: 0.5))
|
self.state?.updated(transition: .spring(duration: 0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(completion: @escaping () -> Void) {
|
func animateOut(completion: @escaping () -> Void) {
|
||||||
self.panGestureState = PanGestureState(offsetFraction: 1.0)
|
self.verticalPanState = PanState(fraction: 1.0, scrollView: nil)
|
||||||
self.completionOnPanGestureApply = completion
|
self.completionOnPanGestureApply = completion
|
||||||
self.state?.updated(transition: .spring(duration: 0.5))
|
self.state?.updated(transition: .spring(duration: 0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureRecognizer is UITapGestureRecognizer {
|
||||||
|
if otherGestureRecognizer is UIPanGestureRecognizer {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureRecognizer is UIPanGestureRecognizer {
|
||||||
|
if let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||||
|
if otherGestureRecognizer.view is UIScrollView {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View {
|
||||||
|
if otherGestureRecognizer.view === participantsView {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||||
switch recognizer.state {
|
switch recognizer.state {
|
||||||
case .began, .changed:
|
case .began, .changed:
|
||||||
if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply {
|
if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply {
|
||||||
let translation = recognizer.translation(in: self)
|
let translation = recognizer.translation(in: self)
|
||||||
self.panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height)
|
let fraction = max(0.0, translation.y / self.bounds.height)
|
||||||
self.state?.updated(transition: .immediate)
|
if let verticalPanState = self.verticalPanState {
|
||||||
|
verticalPanState.fraction = fraction
|
||||||
|
} else {
|
||||||
|
var targetScrollView: UIScrollView?
|
||||||
|
if case .began = recognizer.state, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View {
|
||||||
|
if let hitResult = participantsView.hitTest(self.convert(recognizer.location(in: self), to: participantsView), with: nil) {
|
||||||
|
func findTargetScrollView(target: UIView, minParent: UIView) -> UIScrollView? {
|
||||||
|
if target === participantsView {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if let target = target as? UIScrollView {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
if let parent = target.superview {
|
||||||
|
return findTargetScrollView(target: parent, minParent: minParent)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetScrollView = findTargetScrollView(target: hitResult, minParent: participantsView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.verticalPanState = PanState(fraction: fraction, scrollView: targetScrollView)
|
||||||
|
if let targetScrollView {
|
||||||
|
self.verticalPanState?.contentOffset = targetScrollView.contentOffset.y
|
||||||
|
self.verticalPanState?.startContentOffsetY = recognizer.translation(in: self).y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let verticalPanState = self.verticalPanState {
|
||||||
|
/*if abs(verticalPanState.fraction) >= 0.1 && !verticalPanState.dismissedTooltips {
|
||||||
|
verticalPanState.dismissedTooltips = true
|
||||||
|
self.dismissAllTooltips()
|
||||||
|
}*/
|
||||||
|
|
||||||
|
if let scrollView = verticalPanState.scrollView {
|
||||||
|
let relativeTranslationY = recognizer.translation(in: self).y - verticalPanState.startContentOffsetY
|
||||||
|
let overflowY = scrollView.contentOffset.y - relativeTranslationY
|
||||||
|
|
||||||
|
if !verticalPanState.didLockScrolling {
|
||||||
|
if scrollView.contentOffset.y == 0.0 {
|
||||||
|
verticalPanState.didLockScrolling = true
|
||||||
|
}
|
||||||
|
if let previousContentOffset = verticalPanState.contentOffset, (previousContentOffset < 0.0) != (scrollView.contentOffset.y < 0.0) {
|
||||||
|
verticalPanState.didLockScrolling = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resetContentOffset = false
|
||||||
|
if verticalPanState.didLockScrolling {
|
||||||
|
verticalPanState.accumulatedOffset += -overflowY
|
||||||
|
|
||||||
|
if verticalPanState.accumulatedOffset < 0.0 {
|
||||||
|
verticalPanState.accumulatedOffset = 0.0
|
||||||
|
}
|
||||||
|
if scrollView.contentOffset.y < 0.0 {
|
||||||
|
resetContentOffset = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
verticalPanState.accumulatedOffset += -overflowY
|
||||||
|
verticalPanState.accumulatedOffset = max(0.0, verticalPanState.accumulatedOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if verticalPanState.accumulatedOffset > 0.0 || resetContentOffset {
|
||||||
|
scrollView.contentOffset = CGPoint()
|
||||||
|
|
||||||
|
if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View {
|
||||||
|
let eventCycleState = VideoChatParticipantsComponent.EventCycleState()
|
||||||
|
eventCycleState.ignoreScrolling = true
|
||||||
|
participantsView.setEventCycleState(scrollView: scrollView, eventCycleState: eventCycleState)
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak scrollView, weak participantsView] in
|
||||||
|
guard let participantsView, let scrollView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
participantsView.setEventCycleState(scrollView: scrollView, eventCycleState: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verticalPanState.contentOffset = scrollView.contentOffset.y
|
||||||
|
verticalPanState.startContentOffsetY = recognizer.translation(in: self).y
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .cancelled, .ended:
|
case .cancelled, .ended:
|
||||||
if !self.bounds.height.isZero {
|
if !self.bounds.height.isZero, let verticalPanState = self.verticalPanState {
|
||||||
let translation = recognizer.translation(in: self)
|
let translation = recognizer.translation(in: self)
|
||||||
let panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height)
|
verticalPanState.fraction = max(0.0, translation.y / self.bounds.height)
|
||||||
|
|
||||||
|
let effectiveFraction: CGFloat
|
||||||
|
if verticalPanState.scrollView != nil {
|
||||||
|
effectiveFraction = verticalPanState.accumulatedOffset / self.bounds.height
|
||||||
|
} else {
|
||||||
|
effectiveFraction = verticalPanState.fraction
|
||||||
|
}
|
||||||
|
|
||||||
let velocity = recognizer.velocity(in: self)
|
let velocity = recognizer.velocity(in: self)
|
||||||
|
|
||||||
self.panGestureState = nil
|
self.verticalPanState = nil
|
||||||
if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 {
|
if effectiveFraction > 0.6 || (effectiveFraction > 0.0 && velocity.y >= 100.0) {
|
||||||
self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0)
|
self.verticalPanState = PanState(fraction: effectiveFraction < 0.0 ? -1.0 : 1.0, scrollView: nil)
|
||||||
self.notifyDismissedInteractivelyOnPanGestureApply = true
|
self.notifyDismissedInteractivelyOnPanGestureApply = true
|
||||||
if let controller = self.environment?.controller() as? VideoChatScreenV2Impl {
|
if let controller = self.environment?.controller() as? VideoChatScreenV2Impl {
|
||||||
controller.notifyDismissed()
|
controller.notifyDismissed()
|
||||||
@ -556,6 +690,39 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func onVisibleParticipantsUpdated(ids: Set<EnginePeer.Id>) {
|
||||||
|
if self.visibleParticipants == ids {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.visibleParticipants = ids
|
||||||
|
self.updateTitleSpeakingStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTitleSpeakingStatus() {
|
||||||
|
guard let titleView = self.title.view as? VideoChatTitleComponent.View else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.speakingParticipantPeers.isEmpty {
|
||||||
|
titleView.updateActivityStatus(value: nil, transition: .easeInOut(duration: 0.2))
|
||||||
|
} else {
|
||||||
|
var titleSpeakingStatusValue = ""
|
||||||
|
for participant in self.speakingParticipantPeers {
|
||||||
|
if !self.visibleParticipants.contains(participant.id) {
|
||||||
|
if !titleSpeakingStatusValue.isEmpty {
|
||||||
|
titleSpeakingStatusValue.append(", ")
|
||||||
|
}
|
||||||
|
titleSpeakingStatusValue.append(participant.compactDisplayTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if titleSpeakingStatusValue.isEmpty {
|
||||||
|
titleView.updateActivityStatus(value: nil, transition: .easeInOut(duration: 0.2))
|
||||||
|
} else {
|
||||||
|
titleView.updateActivityStatus(value: titleSpeakingStatusValue, transition: .easeInOut(duration: 0.2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
defer {
|
defer {
|
||||||
@ -585,7 +752,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
if self.members != members {
|
if self.members != members {
|
||||||
var members = members
|
var members = members
|
||||||
|
|
||||||
#if DEBUG && false
|
#if DEBUG && true
|
||||||
if let membersValue = members {
|
if let membersValue = members {
|
||||||
var participants = membersValue.participants
|
var participants = membersValue.participants
|
||||||
for i in 1 ... 20 {
|
for i in 1 ... 20 {
|
||||||
@ -640,25 +807,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
if let membersValue = members {
|
if let membersValue = members {
|
||||||
var participants = membersValue.participants
|
let participants = membersValue.participants
|
||||||
participants = participants.sorted(by: { lhs, rhs in
|
|
||||||
guard let lhsIndex = membersValue.participants.firstIndex(where: { $0.peer.id == lhs.peer.id }) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard let rhsIndex = membersValue.participants.firstIndex(where: { $0.peer.id == rhs.peer.id }) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if let lhsActivityRank = lhs.activityRank, let rhsActivityRank = rhs.activityRank {
|
|
||||||
if lhsActivityRank != rhsActivityRank {
|
|
||||||
return lhsActivityRank < rhsActivityRank
|
|
||||||
}
|
|
||||||
} else if (lhs.activityRank == nil) != (rhs.activityRank == nil) {
|
|
||||||
return lhs.activityRank != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return lhsIndex < rhsIndex
|
|
||||||
})
|
|
||||||
members = PresentationGroupCallMembers(
|
members = PresentationGroupCallMembers(
|
||||||
participants: participants,
|
participants: participants,
|
||||||
speakingParticipants: membersValue.speakingParticipants,
|
speakingParticipants: membersValue.speakingParticipants,
|
||||||
@ -746,6 +895,19 @@ final class VideoChatScreenComponent: Component {
|
|||||||
if !self.isUpdating {
|
if !self.isUpdating {
|
||||||
self.state?.updated(transition: .spring(duration: 0.4))
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var speakingParticipantPeers: [EnginePeer] = []
|
||||||
|
if let members, !members.speakingParticipants.isEmpty {
|
||||||
|
for participant in members.participants {
|
||||||
|
if members.speakingParticipants.contains(participant.peer.id) {
|
||||||
|
speakingParticipantPeers.append(EnginePeer(participant.peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.speakingParticipantPeers != speakingParticipantPeers {
|
||||||
|
self.speakingParticipantPeers = speakingParticipantPeers
|
||||||
|
self.updateTitleSpeakingStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -898,8 +1060,12 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var containerOffset: CGFloat = 0.0
|
var containerOffset: CGFloat = 0.0
|
||||||
if let panGestureState = self.panGestureState {
|
if let verticalPanState = self.verticalPanState {
|
||||||
containerOffset = panGestureState.offsetFraction * availableSize.height
|
if verticalPanState.scrollView != nil {
|
||||||
|
containerOffset = verticalPanState.accumulatedOffset
|
||||||
|
} else {
|
||||||
|
containerOffset = verticalPanState.fraction * availableSize.height
|
||||||
|
}
|
||||||
self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius
|
self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -907,7 +1073,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
guard let self, completed else {
|
guard let self, completed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.panGestureState == nil {
|
if self.verticalPanState == nil {
|
||||||
self.containerView.layer.cornerRadius = 0.0
|
self.containerView.layer.cornerRadius = 0.0
|
||||||
}
|
}
|
||||||
if self.notifyDismissedInteractivelyOnPanGestureApply {
|
if self.notifyDismissedInteractivelyOnPanGestureApply {
|
||||||
@ -1141,11 +1307,19 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonsSideInset: CGFloat = 42.0
|
let buttonsSideInset: CGFloat = 26.0
|
||||||
|
|
||||||
let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter
|
let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter
|
||||||
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
|
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
|
||||||
let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5))
|
|
||||||
|
let effectiveMaxActionMicrophoneButtonSpacing: CGFloat
|
||||||
|
if areButtonsCollapsed {
|
||||||
|
effectiveMaxActionMicrophoneButtonSpacing = 80.0
|
||||||
|
} else {
|
||||||
|
effectiveMaxActionMicrophoneButtonSpacing = maxActionMicrophoneButtonSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionMicrophoneButtonSpacing = min(effectiveMaxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5))
|
||||||
|
|
||||||
var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter))
|
var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter))
|
||||||
var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter))
|
var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter))
|
||||||
@ -1330,6 +1504,12 @@ final class VideoChatScreenComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openInviteMembers()
|
self.openInviteMembers()
|
||||||
|
},
|
||||||
|
visibleParticipantsUpdated: { [weak self] visibleParticipants in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.onVisibleParticipantsUpdated(ids: visibleParticipants)
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -1403,8 +1583,8 @@ final class VideoChatScreenComponent: Component {
|
|||||||
micButtonContent = .connecting
|
micButtonContent = .connecting
|
||||||
actionButtonMicrophoneState = .connecting
|
actionButtonMicrophoneState = .connecting
|
||||||
case .connected:
|
case .connected:
|
||||||
if let callState = callState.muteState {
|
if let muteState = callState.muteState {
|
||||||
if callState.canUnmute {
|
if muteState.canUnmute {
|
||||||
if self.isPushToTalkActive {
|
if self.isPushToTalkActive {
|
||||||
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
|
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
|
||||||
actionButtonMicrophoneState = .unmuted
|
actionButtonMicrophoneState = .unmuted
|
||||||
@ -1413,7 +1593,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
actionButtonMicrophoneState = .muted
|
actionButtonMicrophoneState = .muted
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
micButtonContent = .raiseHand
|
micButtonContent = .raiseHand(isRaised: callState.raisedHand)
|
||||||
actionButtonMicrophoneState = .raiseHand
|
actionButtonMicrophoneState = .raiseHand
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1741,9 +1921,11 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
|
|||||||
}
|
}
|
||||||
self.isAnimatingDismiss = false
|
self.isAnimatingDismiss = false
|
||||||
self.superDismiss()
|
self.superDismiss()
|
||||||
|
completion?()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
self.superDismiss()
|
self.superDismiss()
|
||||||
|
completion?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,8 +248,8 @@ extension VideoChatScreenComponent.View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let context = component.call.accountContext
|
let context = component.call.accountContext
|
||||||
environment.controller()?.dismiss(completion: { [weak navigationController] in
|
controller.dismiss(completion: { [weak navigationController] in
|
||||||
Queue.mainQueue().after(0.3) {
|
Queue.mainQueue().after(0.1) {
|
||||||
guard let navigationController else {
|
guard let navigationController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import ComponentFlow
|
|||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import HierarchyTrackingLayer
|
import HierarchyTrackingLayer
|
||||||
|
import ChatTitleActivityNode
|
||||||
|
|
||||||
final class VideoChatTitleComponent: Component {
|
final class VideoChatTitleComponent: Component {
|
||||||
let title: String
|
let title: String
|
||||||
@ -43,12 +44,17 @@ final class VideoChatTitleComponent: Component {
|
|||||||
final class View: UIView {
|
final class View: UIView {
|
||||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||||
private let title = ComponentView<Empty>()
|
private let title = ComponentView<Empty>()
|
||||||
private var status: ComponentView<Empty>?
|
private let status = ComponentView<Empty>()
|
||||||
private var recordingImageView: UIImageView?
|
private var recordingImageView: UIImageView?
|
||||||
|
|
||||||
|
private var activityStatusNode: ChatTitleActivityNode?
|
||||||
|
|
||||||
private var component: VideoChatTitleComponent?
|
private var component: VideoChatTitleComponent?
|
||||||
private var isUpdating: Bool = false
|
private var isUpdating: Bool = false
|
||||||
|
|
||||||
|
private var currentActivityStatus: String?
|
||||||
|
private var currentSize: CGSize?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||||
|
|
||||||
@ -81,6 +87,64 @@ final class VideoChatTitleComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateActivityStatus(value: String?, transition: ComponentTransition) {
|
||||||
|
if self.currentActivityStatus == value {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.currentActivityStatus = value
|
||||||
|
|
||||||
|
guard let currentSize = self.currentSize, let statusView = self.status.view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let alphaTransition: ComponentTransition
|
||||||
|
if transition.animation.isImmediate {
|
||||||
|
alphaTransition = .immediate
|
||||||
|
} else {
|
||||||
|
alphaTransition = .easeInOut(duration: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let value {
|
||||||
|
let activityStatusNode: ChatTitleActivityNode
|
||||||
|
if let current = self.activityStatusNode {
|
||||||
|
activityStatusNode = current
|
||||||
|
} else {
|
||||||
|
activityStatusNode = ChatTitleActivityNode()
|
||||||
|
self.activityStatusNode = activityStatusNode
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = activityStatusNode.transitionToState(.recordingVoice(NSAttributedString(string: value, font: Font.regular(13.0), textColor: UIColor(rgb: 0x34c759)), UIColor(rgb: 0x34c759)), animation: .none)
|
||||||
|
let activityStatusSize = activityStatusNode.updateLayout(CGSize(width: currentSize.width, height: 100.0), alignment: .center)
|
||||||
|
let activityStatusFrame = CGRect(origin: CGPoint(x: floor((currentSize.width - activityStatusSize.width) * 0.5), y: statusView.center.y - activityStatusSize.height * 0.5), size: activityStatusSize)
|
||||||
|
|
||||||
|
let activityStatusNodeView = activityStatusNode.view
|
||||||
|
activityStatusNodeView.center = activityStatusFrame.center
|
||||||
|
activityStatusNodeView.bounds = CGRect(origin: CGPoint(), size: activityStatusFrame.size)
|
||||||
|
if activityStatusNodeView.superview == nil {
|
||||||
|
self.addSubview(activityStatusNode.view)
|
||||||
|
ComponentTransition.immediate.setTransform(view: activityStatusNodeView, transform: CATransform3DMakeTranslation(0.0, -10.0, 0.0))
|
||||||
|
activityStatusNodeView.alpha = 0.0
|
||||||
|
}
|
||||||
|
transition.setTransform(view: activityStatusNodeView, transform: CATransform3DIdentity)
|
||||||
|
alphaTransition.setAlpha(view: activityStatusNodeView, alpha: 1.0)
|
||||||
|
|
||||||
|
transition.setTransform(view: statusView, transform: CATransform3DMakeTranslation(0.0, 10.0, 0.0))
|
||||||
|
alphaTransition.setAlpha(view: statusView, alpha: 0.0)
|
||||||
|
} else {
|
||||||
|
if let activityStatusNode = self.activityStatusNode {
|
||||||
|
self.activityStatusNode = nil
|
||||||
|
let activityStatusNodeView = activityStatusNode.view
|
||||||
|
transition.setTransform(view: activityStatusNodeView, transform: CATransform3DMakeTranslation(0.0, -10.0, 0.0))
|
||||||
|
alphaTransition.setAlpha(view: activityStatusNodeView, alpha: 0.0, completion: { [weak activityStatusNodeView] _ in
|
||||||
|
activityStatusNodeView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setTransform(view: statusView, transform: CATransform3DIdentity)
|
||||||
|
alphaTransition.setAlpha(view: statusView, alpha: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func update(component: VideoChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
func update(component: VideoChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
defer {
|
defer {
|
||||||
@ -100,19 +164,12 @@ final class VideoChatTitleComponent: Component {
|
|||||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
let status: ComponentView<Empty>
|
|
||||||
if let current = self.status {
|
|
||||||
status = current
|
|
||||||
} else {
|
|
||||||
status = ComponentView()
|
|
||||||
self.status = status
|
|
||||||
}
|
|
||||||
let statusComponent: AnyComponent<Empty>
|
let statusComponent: AnyComponent<Empty>
|
||||||
statusComponent = AnyComponent(MultilineTextComponent(
|
statusComponent = AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(string: component.status, font: Font.regular(13.0), textColor: UIColor(white: 1.0, alpha: 0.5)))
|
text: .plain(NSAttributedString(string: component.status, font: Font.regular(13.0), textColor: UIColor(white: 1.0, alpha: 0.5)))
|
||||||
))
|
))
|
||||||
|
|
||||||
let statusSize = status.update(
|
let statusSize = self.status.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: statusComponent,
|
component: statusComponent,
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -131,7 +188,7 @@ final class VideoChatTitleComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize)
|
let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize)
|
||||||
if let statusView = status.view {
|
if let statusView = self.status.view {
|
||||||
if statusView.superview == nil {
|
if statusView.superview == nil {
|
||||||
self.addSubview(statusView)
|
self.addSubview(statusView)
|
||||||
}
|
}
|
||||||
@ -165,6 +222,8 @@ final class VideoChatTitleComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.currentSize = size
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -324,13 +324,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
|||||||
self.closeAction?()
|
self.closeAction?()
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
if !"".isEmpty {
|
||||||
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
|
if #available(iOS 16.0, *) {
|
||||||
pipVideoCallViewController.view.addSubview(self.pipView)
|
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
|
||||||
self.pipView.frame = pipVideoCallViewController.view.bounds
|
pipVideoCallViewController.view.addSubview(self.pipView)
|
||||||
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
self.pipView.frame = pipVideoCallViewController.view.bounds
|
||||||
self.pipView.translatesAutoresizingMaskIntoConstraints = true
|
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
self.pipVideoCallViewController = pipVideoCallViewController
|
self.pipView.translatesAutoresizingMaskIntoConstraints = true
|
||||||
|
self.pipVideoCallViewController = pipVideoCallViewController
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let blurFilter = makeBlurFilter() {
|
if let blurFilter = makeBlurFilter() {
|
||||||
|
@ -1659,7 +1659,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
let loopVideo = updatedVideoFile.isAnimated
|
let loopVideo = updatedVideoFile.isAnimated
|
||||||
|
|
||||||
let videoContent: UniversalVideoContent
|
let videoContent: UniversalVideoContent
|
||||||
if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming {
|
if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile) {
|
||||||
videoContent = HLSVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, loopVideo: loopVideo)
|
videoContent = HLSVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, loopVideo: loopVideo)
|
||||||
} else {
|
} else {
|
||||||
videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in
|
videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in
|
||||||
|
Loading…
x
Reference in New Issue
Block a user