diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 1448c0f424..1f1e17ba77 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -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)) } else { 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) } 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)) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift new file mode 100644 index 0000000000..8170418a48 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift @@ -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() + private let title = ComponentView() + 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, 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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift index 8ecf340e20..d7db6cc15f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift @@ -184,7 +184,7 @@ final class VideoChatMicButtonComponent: Component { case connecting case muted case unmuted(pushToTalk: Bool) - case raiseHand + case raiseHand(isRaised: Bool) case scheduled(state: ScheduledState) } @@ -226,6 +226,7 @@ final class VideoChatMicButtonComponent: Component { private var disappearingBackgrounds: [UIImageView] = [] private var progressIndicator: RadialStatusNode? private let title = ComponentView() + private var subtitle: ComponentView? private let icon: VoiceChatActionButtonIconNode private var glowView: GlowView? @@ -322,6 +323,7 @@ final class VideoChatMicButtonComponent: Component { let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) let titleText: String + var subtitleText: String? var isEnabled = true switch component.content { case .connecting: @@ -331,8 +333,14 @@ final class VideoChatMicButtonComponent: Component { titleText = "Unmute" case let .unmuted(isPushToTalk): titleText = isPushToTalk ? "You are Live" : "Tap to Mute" - case .raiseHand: - titleText = "Raise Hand" + case let .raiseHand(isRaised): + 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): switch state { case .start: @@ -353,7 +361,7 @@ final class VideoChatMicButtonComponent: Component { text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white)) )), 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) @@ -470,7 +478,10 @@ final class VideoChatMicButtonComponent: Component { 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 titleView.superview == nil { titleView.isUserInteractionEnabled = false @@ -481,6 +492,47 @@ final class VideoChatMicButtonComponent: Component { alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0) } + if let subtitleText { + let subtitle: ComponentView + 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 { self.icon.view.isUserInteractionEnabled = false self.addSubview(self.icon.view) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index 2369a08d5c..834fc03318 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -283,7 +283,7 @@ final class VideoChatParticipantAvatarComponent: Component { transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) avatarNode.updateSize(size: avatarSize) - let blobScale: CGFloat = 1.5 + let blobScale: CGFloat = 2.0 if self.audioLevelDisposable == nil { struct Level { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index c5c197f6e2..dda1b187c8 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -117,6 +117,13 @@ final class VideoChatParticipantsComponent: Component { } } + final class EventCycleState { + var ignoreScrolling: Bool = false + + init() { + } + } + let call: PresentationGroupCall let participants: Participants? let speakingParticipants: Set @@ -132,6 +139,7 @@ final class VideoChatParticipantsComponent: Component { let updateIsMainParticipantPinned: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void let openInviteMembers: () -> Void + let visibleParticipantsUpdated: (Set) -> Void init( call: PresentationGroupCall, @@ -148,7 +156,8 @@ final class VideoChatParticipantsComponent: Component { updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void, updateIsExpandedUIHidden: @escaping (Bool) -> Void, - openInviteMembers: @escaping () -> Void + openInviteMembers: @escaping () -> Void, + visibleParticipantsUpdated: @escaping (Set) -> Void ) { self.call = call self.participants = participants @@ -165,6 +174,7 @@ final class VideoChatParticipantsComponent: Component { self.updateIsMainParticipantPinned = updateIsMainParticipantPinned self.updateIsExpandedUIHidden = updateIsExpandedUIHidden self.openInviteMembers = openInviteMembers + self.visibleParticipantsUpdated = visibleParticipantsUpdated } 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)) } else { self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height)) - self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth, height: containerSize.height - layout.mainColumn.insets.top - layout.mainColumn.insets.bottom)) + self.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.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 { private let scrollViewClippingContainer: SolidRoundedCornersContainer private let scrollView: ScrollView + private let scrollViewBottomShadowView: UIImageView private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer private let separateVideoScrollView: ScrollView @@ -622,6 +633,7 @@ final class VideoChatParticipantsComponent: Component { private let expandedGridItemContainer: UIView private var expandedControlsView: ComponentView? private var expandedThumbnailsView: ComponentView? + private var expandedSpeakingToast: ComponentView? private var listItemViews: [EnginePeer.Id: ListItem] = [:] private let listItemViewContainer: UIView @@ -635,9 +647,13 @@ final class VideoChatParticipantsComponent: Component { private var currentLoadMoreToken: String? + private var mainScrollViewEventCycleState: EventCycleState? + private var separateVideoScrollViewEventCycleState: EventCycleState? + override init(frame: CGRect) { self.scrollViewClippingContainer = SolidRoundedCornersContainer() self.scrollView = ScrollView() + self.scrollViewBottomShadowView = UIImageView() self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer() self.separateVideoScrollView = ScrollView() @@ -687,6 +703,7 @@ final class VideoChatParticipantsComponent: Component { self.scrollViewClippingContainer.addSubview(self.scrollView) self.addSubview(self.scrollViewClippingContainer) self.addSubview(self.scrollViewClippingContainer.cornersView) + self.addSubview(self.scrollViewBottomShadowView) self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView) self.addSubview(self.separateVideoScrollViewClippingContainer) @@ -765,10 +782,46 @@ final class VideoChatParticipantsComponent: Component { func scrollViewDidScroll(_ scrollView: UIScrollView) { 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) } } + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + 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) { guard let component = self.component, let itemLayout = self.itemLayout else { return @@ -832,11 +885,18 @@ final class VideoChatParticipantsComponent: Component { var validGridItemIds: [VideoParticipantKey] = [] 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 clippedVisibleGridItemRange: (minIndex: Int, maxIndex: Int) if itemLayout.layout.videoColumn == nil { visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds) + clippedVisibleGridItemRange = itemLayout.visibleGridItemRange(for: clippedScrollViewBounds) } else { visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds) + clippedVisibleGridItemRange = visibleGridItemRange } if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex { for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex { @@ -852,6 +912,8 @@ final class VideoChatParticipantsComponent: Component { validGridItemIndices.append(index) } } + + var visibleParticipants: [EnginePeer.Id] = [] for index in validGridItemIndices { 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 if isItemExpanded { if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey { @@ -1066,11 +1132,16 @@ final class VideoChatParticipantsComponent: Component { var validListItemIds: [EnginePeer.Id] = [] let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds) + let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds) if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex { for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex { let participant = self.listParticipants[i] validListItemIds.append(participant.peer.id) + if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex { + visibleParticipants.append(participant.peer.id) + } + var itemTransition = transition let itemView: ListItem 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 { subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent) } 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 { - subtitle = PeerListItemComponent.Subtitle(text: participant.about ?? "listening", color: .neutral) + subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral) } let rightAccessoryComponent: AnyComponent = 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 + 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 self.currentLoadMoreToken != loadMoreToken { self.currentLoadMoreToken = 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, transition: ComponentTransition) -> CGSize { @@ -1482,11 +1633,16 @@ final class VideoChatParticipantsComponent: Component { var listParticipants: [GroupCallParticipantsContext.Participant] = [] if let participants = component.participants { for participant in participants.participants { + var isFullyMuted = false + if let muteState = participant.muteState, !muteState.canUnmute { + isFullyMuted = true + } + var hasVideo = false if participant.videoDescription != nil { hasVideo = true 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) } else { gridParticipants.append(videoParticipant) @@ -1495,14 +1651,14 @@ final class VideoChatParticipantsComponent: Component { if participant.presentationDescription != nil { hasVideo = 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) } else { gridParticipants.append(videoParticipant) } } 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) } else { listParticipants.append(participant) @@ -1594,6 +1750,37 @@ final class VideoChatParticipantsComponent: Component { smoothCorners: false ), 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.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) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 906f913916..85227b283c 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -41,15 +41,22 @@ final class VideoChatScreenComponent: Component { return true } - private struct PanGestureState { - var offsetFraction: CGFloat + private final class PanState { + 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) { - self.offsetFraction = offsetFraction + init(fraction: CGFloat, scrollView: UIScrollView?) { + self.fraction = fraction + self.scrollView = scrollView } } - final class View: UIView { + final class View: UIView, UIGestureRecognizerDelegate { let containerView: UIView var component: VideoChatScreenComponent? @@ -57,7 +64,7 @@ final class VideoChatScreenComponent: Component { weak var state: EmptyComponentState? var isUpdating: Bool = false - private var panGestureState: PanGestureState? + private var verticalPanState: PanState? var notifyDismissedInteractivelyOnPanGestureApply: Bool = false var completionOnPanGestureApply: (() -> Void)? @@ -95,6 +102,9 @@ final class VideoChatScreenComponent: Component { var members: PresentationGroupCallMembers? var membersDisposable: Disposable? + var speakingParticipantPeers: [EnginePeer] = [] + var visibleParticipants: Set = Set() + let isPresentedValue = ValuePromise(false, ignoreRepeated: true) var applicationStateDisposable: Disposable? @@ -117,9 +127,11 @@ final class VideoChatScreenComponent: Component { 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) { @@ -139,37 +151,159 @@ final class VideoChatScreenComponent: Component { } func animateIn() { - self.panGestureState = PanGestureState(offsetFraction: 1.0) + self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) self.state?.updated(transition: .immediate) - self.panGestureState = nil + self.verticalPanState = nil self.state?.updated(transition: .spring(duration: 0.5)) } func animateOut(completion: @escaping () -> Void) { - self.panGestureState = PanGestureState(offsetFraction: 1.0) + self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) self.completionOnPanGestureApply = completion 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) { switch recognizer.state { case .began, .changed: if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply { let translation = recognizer.translation(in: self) - self.panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height) - self.state?.updated(transition: .immediate) + let fraction = max(0.0, translation.y / self.bounds.height) + 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: - if !self.bounds.height.isZero { + if !self.bounds.height.isZero, let verticalPanState = self.verticalPanState { 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) - self.panGestureState = nil - if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 { - self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0) + self.verticalPanState = nil + if effectiveFraction > 0.6 || (effectiveFraction > 0.0 && velocity.y >= 100.0) { + self.verticalPanState = PanState(fraction: effectiveFraction < 0.0 ? -1.0 : 1.0, scrollView: nil) self.notifyDismissedInteractivelyOnPanGestureApply = true if let controller = self.environment?.controller() as? VideoChatScreenV2Impl { controller.notifyDismissed() @@ -556,6 +690,39 @@ final class VideoChatScreenComponent: Component { } } + private func onVisibleParticipantsUpdated(ids: Set) { + 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, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -585,7 +752,7 @@ final class VideoChatScreenComponent: Component { if self.members != members { var members = members - #if DEBUG && false + #if DEBUG && true if let membersValue = members { var participants = membersValue.participants for i in 1 ... 20 { @@ -640,25 +807,7 @@ final class VideoChatScreenComponent: Component { #endif if let membersValue = members { - var 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 - }) + let participants = membersValue.participants members = PresentationGroupCallMembers( participants: participants, speakingParticipants: membersValue.speakingParticipants, @@ -746,6 +895,19 @@ final class VideoChatScreenComponent: Component { if !self.isUpdating { 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 - if let panGestureState = self.panGestureState { - containerOffset = panGestureState.offsetFraction * availableSize.height + if let verticalPanState = self.verticalPanState { + if verticalPanState.scrollView != nil { + containerOffset = verticalPanState.accumulatedOffset + } else { + containerOffset = verticalPanState.fraction * availableSize.height + } self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius } @@ -907,7 +1073,7 @@ final class VideoChatScreenComponent: Component { guard let self, completed else { return } - if self.panGestureState == nil { + if self.verticalPanState == nil { self.containerView.layer.cornerRadius = 0.0 } 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 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 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 } self.openInviteMembers() + }, + visibleParticipantsUpdated: { [weak self] visibleParticipants in + guard let self else { + return + } + self.onVisibleParticipantsUpdated(ids: visibleParticipants) } )), environment: {}, @@ -1403,8 +1583,8 @@ final class VideoChatScreenComponent: Component { micButtonContent = .connecting actionButtonMicrophoneState = .connecting case .connected: - if let callState = callState.muteState { - if callState.canUnmute { + if let muteState = callState.muteState { + if muteState.canUnmute { if self.isPushToTalkActive { micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive) actionButtonMicrophoneState = .unmuted @@ -1413,7 +1593,7 @@ final class VideoChatScreenComponent: Component { actionButtonMicrophoneState = .muted } } else { - micButtonContent = .raiseHand + micButtonContent = .raiseHand(isRaised: callState.raisedHand) actionButtonMicrophoneState = .raiseHand } } else { @@ -1741,9 +1921,11 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo } self.isAnimatingDismiss = false self.superDismiss() + completion?() }) } else { self.superDismiss() + completion?() } } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index 6ee08d0a90..dd81e23a91 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -248,8 +248,8 @@ extension VideoChatScreenComponent.View { } let context = component.call.accountContext - environment.controller()?.dismiss(completion: { [weak navigationController] in - Queue.mainQueue().after(0.3) { + controller.dismiss(completion: { [weak navigationController] in + Queue.mainQueue().after(0.1) { guard let navigationController else { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift index 0f13e9d815..6b36289463 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift @@ -5,6 +5,7 @@ import ComponentFlow import MultilineTextComponent import TelegramPresentationData import HierarchyTrackingLayer +import ChatTitleActivityNode final class VideoChatTitleComponent: Component { let title: String @@ -43,12 +44,17 @@ final class VideoChatTitleComponent: Component { final class View: UIView { private let hierarchyTrackingLayer: HierarchyTrackingLayer private let title = ComponentView() - private var status: ComponentView? + private let status = ComponentView() private var recordingImageView: UIImageView? + + private var activityStatusNode: ChatTitleActivityNode? private var component: VideoChatTitleComponent? private var isUpdating: Bool = false + private var currentActivityStatus: String? + private var currentSize: CGSize? + override init(frame: CGRect) { 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, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -100,19 +164,12 @@ final class VideoChatTitleComponent: Component { containerSize: CGSize(width: availableSize.width, height: 100.0) ) - let status: ComponentView - if let current = self.status { - status = current - } else { - status = ComponentView() - self.status = status - } let statusComponent: AnyComponent statusComponent = AnyComponent(MultilineTextComponent( 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, component: statusComponent, 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) - if let statusView = status.view { + if let statusView = self.status.view { if statusView.superview == nil { self.addSubview(statusView) } @@ -165,6 +222,8 @@ final class VideoChatTitleComponent: Component { } } + self.currentSize = size + return size } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 28fcc277ad..9474675578 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -324,13 +324,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.closeAction?() } - if #available(iOS 16.0, *) { - let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() - pipVideoCallViewController.view.addSubview(self.pipView) - self.pipView.frame = pipVideoCallViewController.view.bounds - self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - self.pipView.translatesAutoresizingMaskIntoConstraints = true - self.pipVideoCallViewController = pipVideoCallViewController + if !"".isEmpty { + if #available(iOS 16.0, *) { + let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() + pipVideoCallViewController.view.addSubview(self.pipView) + self.pipView.frame = pipVideoCallViewController.view.bounds + self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.pipView.translatesAutoresizingMaskIntoConstraints = true + self.pipVideoCallViewController = pipVideoCallViewController + } } if let blurFilter = makeBlurFilter() { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index d37ea5fcd8..707944d151 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1659,7 +1659,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let loopVideo = updatedVideoFile.isAnimated 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) } 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