From 40c4378bdeecb1b19e1d6224c6f3fb52280e8c8a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 14 Jan 2024 10:54:16 +0400 Subject: [PATCH] Video message recording improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../Sources/MediaScrubberComponent.swift | 67 ++++++-- .../Sources/VideoMessageCameraScreen.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 27 ++++ ...essageContextControllerContentSource.swift | 150 ++++++++++-------- .../ChatRecordingPreviewInputPanelNode.swift | 5 + .../Sources/ManagedAudioRecorder.swift | 19 ++- 7 files changed, 185 insertions(+), 88 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c341d0e8aa..18fee0796e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10893,3 +10893,6 @@ Sorry for the inconvenience."; "Chat.TapToPlayVideoMessageOnceTooltip" = "Tap to set this message to **Play Once**"; "Chat.PlayVideoMessageOnceTooltip" = "The recipient will be able to play it only once."; + +"Conversation.DiscardRecordedVoiceMessageDescription" = "Are you sure you want to discard\nyour voice message?"; +"Conversation.DiscardRecordedVoiceMessageAction" = "Discard"; diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index 891bfa7edf..aa0a996ba5 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -556,8 +556,6 @@ public final class MediaScrubberComponent: Component { transition: transition ) - let _ = leftHandleFrame - let _ = rightHandleFrame let _ = ghostLeftHandleFrame let _ = ghostRightHandleFrame @@ -585,12 +583,15 @@ public final class MediaScrubberComponent: Component { transition.setFrame(view: self.ghostTrimView, frame: ghostTrimViewFrame) transition.setAlpha(view: self.ghostTrimView, alpha: ghostTrimVisible ? 0.75 : 0.0) -// var containerLeftEdge = leftHandleFrame.maxX -// var containerRightEdge = rightHandleFrame.minX -// if self.isAudioSelected && component.duration > 0.0 { -// containerLeftEdge = ghostLeftHandleFrame.maxX -// containerRightEdge = ghostRightHandleFrame.minX -// } + if case .videoMessage = component.style { + for (_ , trackView) in self.trackViews { + trackView.updateOpaqueEdges( + left: leftHandleFrame.minX, + right: rightHandleFrame.maxX, + transition: transition + ) + } + } let isDraggingTracks = self.trackViews.values.contains(where: { $0.isDragging }) let isCursorHidden = isDraggingTracks || self.trimView.isPanningTrimHandle || self.ghostTrimView.isPanningTrimHandle @@ -738,7 +739,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let (track, _, _) = self.params else { + guard let (track, _, _, _) = self.params else { return } self.onSelection(track.id) @@ -787,9 +788,42 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega private var params: ( track: MediaScrubberComponent.Track, isSelected: Bool, + availableSize: CGSize, duration: Double )? + private var leftOpaqueEdge: CGFloat? + private var rightOpaqueEdge: CGFloat? + func updateOpaqueEdges( + left: CGFloat, + right: CGFloat, + transition: Transition + ) { + self.leftOpaqueEdge = left + self.rightOpaqueEdge = right + + if let params = self.params { + self.updateThumbnailContainers( + scrubberSize: CGSize(width: params.availableSize.width, height: 33.0), + availableSize: params.availableSize, + transition: transition + ) + } + } + + private func updateThumbnailContainers( + scrubberSize: CGSize, + availableSize: CGSize, + transition: Transition + ) { + let containerLeftEdge: CGFloat = self.leftOpaqueEdge ?? 0.0 + let containerRightEdge: CGFloat = self.rightOpaqueEdge ?? availableSize.width + + transition.setFrame(view: self.videoTransparentFramesContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: scrubberSize.width, height: scrubberSize.height))) + transition.setFrame(view: self.videoOpaqueFramesContainer, frame: CGRect(origin: CGPoint(x: containerLeftEdge, y: 0.0), size: CGSize(width: containerRightEdge - containerLeftEdge, height: scrubberSize.height))) + transition.setBounds(view: self.videoOpaqueFramesContainer, bounds: CGRect(origin: CGPoint(x: containerLeftEdge, y: 0.0), size: CGSize(width: containerRightEdge - containerLeftEdge, height: scrubberSize.height))) + } + func update( context: AccountContext, style: MediaScrubberComponent.Style, @@ -800,7 +834,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega transition: Transition ) -> CGSize { let previousParams = self.params - self.params = (track, isSelected, duration) + self.params = (track, isSelected, availableSize, duration) let fullTrackHeight: CGFloat let framesCornerRadius: CGFloat @@ -968,13 +1002,12 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega } } } - - let containerLeftEdge: CGFloat = 0.0 - let containerRightEdge: CGFloat = availableSize.width - - transition.setFrame(view: self.videoTransparentFramesContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: scrubberSize.width, height: scrubberSize.height))) - transition.setFrame(view: self.videoOpaqueFramesContainer, frame: CGRect(origin: CGPoint(x: containerLeftEdge, y: 0.0), size: CGSize(width: containerRightEdge - containerLeftEdge, height: scrubberSize.height))) - transition.setBounds(view: self.videoOpaqueFramesContainer, bounds: CGRect(origin: CGPoint(x: containerLeftEdge, y: 0.0), size: CGSize(width: containerRightEdge - containerLeftEdge, height: scrubberSize.height))) + + self.updateThumbnailContainers( + scrubberSize: scrubberSize, + availableSize: availableSize, + transition: transition + ) var frameAspectRatio = 0.66 if let image = frames.first, image.size.height > 0.0 { diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 3e9fb39a50..825a83a6b3 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -1219,7 +1219,7 @@ public class VideoMessageCameraScreen: ViewController { self.updatedPresentationData = updatedPresentationData self.inputPanelFrame = inputPanelFrame self.allowLiveUpload = peerId.namespace != Namespaces.Peer.SecretChat - self.viewOnceAvailable = peerId.namespace == Namespaces.Peer.CloudUser + self.viewOnceAvailable = peerId.namespace == Namespaces.Peer.CloudUser && peerId != context.account.peerId self.completion = completion self.recordingStatus = RecordingStatus(micLevel: self.micLevelValue.get(), duration: self.durationValue.get()) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 74eb0bf136..3f1ef78082 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -689,6 +689,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } + if strongSelf.presentRecordedVoiceMessageDiscardAlert(action: action, performAction: false) { + return false + } + return true } @@ -743,6 +747,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.openViewOnceMediaMessage(message) return false } + } else if file.isVideo { + if !displayVoiceMessageDiscardAlert() { + return false + } } } if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { @@ -17359,6 +17367,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } + func presentRecordedVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool { + if let _ = self.presentationInterfaceState.recordedMediaPreview { + alertAction?() + Queue.mainQueue().after(delay ? 0.2 : 0.0) { + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageAction, action: { [weak self] in + self?.stopMediaRecorder() + Queue.mainQueue().after(0.1) { + action() + } + })]), in: .window(.root)) + } + + return true + } else if performAction { + action() + } + return false + } + func presentAutoremoveSetup() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index 2e636e7392..c14268f35a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -169,6 +169,7 @@ final class ChatViewOnceMessageContextExtractedContentSource: ContextExtractedCo return nil } + let context = self.context self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension()) let isIncoming = self.message.effectivelyIncoming(self.context.account.peerId) @@ -196,6 +197,7 @@ final class ChatViewOnceMessageContextExtractedContentSource: ContextExtractedCo var tooltipSourceRect: CGRect = .zero if let sourceNode { + let videoWidth = min(404.0, chatNode.frame.width - 2.0) var bubbleWidth: CGFloat = 0.0 if (isIncoming || "".isEmpty) { @@ -231,7 +233,7 @@ final class ChatViewOnceMessageContextExtractedContentSource: ContextExtractedCo if let messageNode = node as? ChatMessageItemView, let copyContentNode = messageNode.getMessageContextSourceNode(stableId: self.message.stableId) { if isVideo { - self.initialAppearanceOffset = CGPoint(x: 0.0, y: width - 20.0 - copyContentNode.frame.height) + self.initialAppearanceOffset = CGPoint(x: 0.0, y: min(videoWidth, width - 20.0) - copyContentNode.frame.height) } messageNode.frame.origin.y = sourceRect.origin.y @@ -253,79 +255,99 @@ final class ChatViewOnceMessageContextExtractedContentSource: ContextExtractedCo } let mappedParentRect = chatNode.view.convert(chatNode.bounds, to: nil) - tooltipSourceRect = CGRect(x: mappedParentRect.minX + (isIncoming ? 22.0 : chatNode.frame.width - bubbleWidth + 10.0), y: floorToScreenPixels((chatNode.frame.height - 75.0) / 2.0) - 43.0, width: 44.0, height: 44.0) + if isVideo { + tooltipSourceRect = CGRect(x: mappedParentRect.minX + (isIncoming ? videoWidth / 2.0 : chatNode.frame.width - videoWidth / 2.0), y: floorToScreenPixels((chatNode.frame.height - videoWidth) / 2.0) + 8.0, width: 0.0, height: 0.0) + } else { + tooltipSourceRect = CGRect(x: mappedParentRect.minX + (isIncoming ? 22.0 : chatNode.frame.width - bubbleWidth + 10.0), y: floorToScreenPixels((chatNode.frame.height - 75.0) / 2.0) - 43.0, width: 44.0, height: 44.0) + } } - if !isVideo { - let displayTooltip = { [weak self] in - guard let self else { - return - } - let absoluteFrame = tooltipSourceRect - let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY), size: CGSize()) - - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - var tooltipText: String? - if isIncoming { + let displayTooltip = { [weak self] in + guard let self else { + return + } + let absoluteFrame = tooltipSourceRect + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY), size: CGSize()) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var tooltipText: String? + if isIncoming { + if isVideo { + tooltipText = presentationData.strings.Chat_PlayOnceVideoMessageTooltip + } else { tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageTooltip - } else if let peer = self.message.peers[self.message.id.peerId] { - let peerName = EnginePeer(peer).compactDisplayTitle - tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageYourTooltip(peerName).string } - - if let tooltipText { - let tooltipController = TooltipScreen( - account: self.context.account, - sharedContext: self.context.sharedContext, - text: .markdown(text: tooltipText), - balancedTextLayout: true, - constrainWidth: 240.0, - style: .customBlur(UIColor(rgb: 0x18181a), 0.0), - arrowStyle: .small, - icon: nil, - location: .point(location, .bottom), - displayDuration: .custom(3.0), - inset: 8.0, - cornerRadius: 11.0, - shouldDismissOnTouch: { _, _ in - return .ignore - } - ) - self.tooltipController = tooltipController - self.present(tooltipController) + } else if let peer = self.message.peers[self.message.id.peerId] { + let peerName = EnginePeer(peer).compactDisplayTitle + if isVideo { + tooltipText = presentationData.strings.Chat_PlayOnceVideoMessageYourTooltip(peerName).string + } else { + tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageYourTooltip(peerName).string } } - if isIncoming { - let _ = (ApplicationSpecificNotice.getIncomingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in - guard let self else { - return + if let tooltipText { + let tooltipController = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .markdown(text: tooltipText), + balancedTextLayout: true, + constrainWidth: 240.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: nil, + location: .point(location, .bottom), + displayDuration: .custom(3.0), + inset: 8.0, + cornerRadius: 11.0, + shouldDismissOnTouch: { _, _ in + return .ignore } - if counter >= 2 { - return - } - Queue.mainQueue().after(0.3) { - displayTooltip() - } - let _ = ApplicationSpecificNotice.incrementIncomingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager).startStandalone() - }) - } else { - let _ = (ApplicationSpecificNotice.getOutgoingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in - guard let self else { - return - } - if counter >= 2 { - return - } - Queue.mainQueue().after(0.3) { - displayTooltip() - } - let _ = ApplicationSpecificNotice.incrementOutgoingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager).startStandalone() - }) + ) + self.tooltipController = tooltipController + self.present(tooltipController) } } + + let tooltipStateSignal: Signal + let updateTooltipState: () -> Void + if isVideo { + if isIncoming { + tooltipStateSignal = ApplicationSpecificNotice.getIncomingVideoMessagePlayOnceTip(accountManager: context.sharedContext.accountManager) + updateTooltipState = { + let _ = ApplicationSpecificNotice.incrementIncomingVideoMessagePlayOnceTip(accountManager: context.sharedContext.accountManager).startStandalone() + } + } else { + tooltipStateSignal = ApplicationSpecificNotice.getOutgoingVideoMessagePlayOnceTip(accountManager: context.sharedContext.accountManager) + updateTooltipState = { + let _ = ApplicationSpecificNotice.incrementOutgoingVideoMessagePlayOnceTip(accountManager: context.sharedContext.accountManager).startStandalone() + } + } + } else { + if isIncoming { + tooltipStateSignal = ApplicationSpecificNotice.getIncomingVoiceMessagePlayOnceTip(accountManager: context.sharedContext.accountManager) + updateTooltipState = { + let _ = ApplicationSpecificNotice.incrementIncomingVoiceMessagePlayOnceTip(accountManager: context.sharedContext.accountManager).startStandalone() + } + } else { + tooltipStateSignal = ApplicationSpecificNotice.getOutgoingVoiceMessagePlayOnceTip(accountManager: context.sharedContext.accountManager) + updateTooltipState = { + let _ = ApplicationSpecificNotice.incrementOutgoingVoiceMessagePlayOnceTip(accountManager: context.sharedContext.accountManager).startStandalone() + } + } + } + + let _ = (tooltipStateSignal + |> deliverOnMainQueue).startStandalone(next: { counter in + if counter >= 2 { + return + } + Queue.mainQueue().after(0.3) { + displayTooltip() + } + updateTooltipState() + }) + return result } diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index a73363e27b..d12c41f77d 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -415,9 +415,14 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { prevTextInputPanelNode.viewOnceButton.isHidden = true prevTextInputPanelNode.viewOnce = false + self.recordMoreButton.isEnabled = false self.viewOnceButton.layer.animatePosition(from: prevTextInputPanelNode.viewOnceButton.position, to: self.viewOnceButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in prevTextInputPanelNode.viewOnceButton.isHidden = false prevTextInputPanelNode.viewOnceButton.update(isSelected: false, animated: false) + + Queue.mainQueue().after(0.3) { + self.recordMoreButton.isEnabled = true + } }) self.recordMoreButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) diff --git a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift index a78fc96e3f..cb8f91145c 100644 --- a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift +++ b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift @@ -150,6 +150,7 @@ final class ManagedAudioRecorderContext { private let beganWithTone: (Bool) -> Void private var paused = true + private var manuallyPaused = false private let queue: Queue private let mediaManager: MediaManager @@ -413,9 +414,11 @@ final class ManagedAudioRecorderContext { return Signal { subscriber in queue.async { if let strongSelf = self { - strongSelf.hasAudioSession = false - strongSelf.stop() - strongSelf.recordingState.set(.stopped) + if !strongSelf.manuallyPaused { + strongSelf.hasAudioSession = false + strongSelf.stop() + strongSelf.recordingState.set(.stopped) + } subscriber.putCompletion() } } @@ -450,13 +453,17 @@ final class ManagedAudioRecorderContext { func pause() { assert(self.queue.isCurrent()) - self.paused = true + self.manuallyPaused = true } func resume() { assert(self.queue.isCurrent()) - self.paused = false + if self.manuallyPaused { + self.manuallyPaused = false + } else if self.paused { + self.start() + } } func stop() { @@ -500,7 +507,7 @@ final class ManagedAudioRecorderContext { free(buffer.mData) } - if !self.processSamples || self.paused { + if !self.processSamples || self.manuallyPaused { return }