diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index 084a592c11..5ca920aa8d 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -282,7 +282,7 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { public struct Audio: Codable, Equatable { public let resource: LocalFileMediaResource public let fileSize: Int32 - public let duration: Int32 + public let duration: Double public let waveform: AudioWaveform public let trimRange: Range? public let resumeData: Data? @@ -290,7 +290,7 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { public init( resource: LocalFileMediaResource, fileSize: Int32, - duration: Int32, + duration: Double, waveform: AudioWaveform, trimRange: Range?, resumeData: Data? @@ -310,7 +310,12 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { self.resource = LocalFileMediaResource(decoder: PostboxDecoder(buffer: MemoryBuffer(data: resourceData.data))) self.fileSize = try container.decode(Int32.self, forKey: "s") - self.duration = try container.decode(Int32.self, forKey: "d") + + if let doubleValue = try container.decodeIfPresent(Double.self, forKey: "dd") { + self.duration = doubleValue + } else { + self.duration = Double(try container.decode(Int32.self, forKey: "d")) + } let waveformData = try container.decode(Data.self, forKey: "wd") let waveformPeak = try container.decode(Int32.self, forKey: "wp") @@ -330,7 +335,7 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { try container.encode(PostboxEncoder().encodeObjectToRawData(self.resource), forKey: "r") try container.encode(self.fileSize, forKey: "s") - try container.encode(self.duration, forKey: "d") + try container.encode(self.duration, forKey: "dd") try container.encode(self.waveform.samples, forKey: "wd") try container.encode(self.waveform.peak, forKey: "wp") @@ -368,13 +373,13 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { } public struct Video: Codable, Equatable { - public let duration: Int32 + public let duration: Double public let frames: [UIImage] public let framesUpdateTimestamp: Double public let trimRange: Range? public init( - duration: Int32, + duration: Double, frames: [UIImage], framesUpdateTimestamp: Double, trimRange: Range? @@ -388,7 +393,11 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) - self.duration = try container.decode(Int32.self, forKey: "d") + if let doubleValue = try container.decodeIfPresent(Double.self, forKey: "dd") { + self.duration = doubleValue + } else { + self.duration = Double(try container.decode(Int32.self, forKey: "d")) + } self.frames = [] self.framesUpdateTimestamp = try container.decode(Double.self, forKey: "fu") if let trimLowerBound = try container.decodeIfPresent(Double.self, forKey: "tl"), let trimUpperBound = try container.decodeIfPresent(Double.self, forKey: "tu") { @@ -401,7 +410,7 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - try container.encode(self.duration, forKey: "d") + try container.encode(self.duration, forKey: "dd") try container.encode(self.framesUpdateTimestamp, forKey: "fu") if let trimRange = self.trimRange { try container.encode(trimRange.lowerBound, forKey: "tl") diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index 4b293eb13f..0cfdafbf55 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -271,13 +271,25 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext { return } + var position: TooltipScreen.ArrowPosition = .bottom + if let layout = self.controller?.currentlyAppliedLayout, let orientation = layout.metrics.orientation { + switch orientation { + case .landscapeLeft: + position = .left + case .landscapeRight: + position = .right + default: + break + } + } + let controller = TooltipScreen( account: context.account, sharedContext: context.sharedContext, text: .plain(text: text), style: .customBlur(UIColor(rgb: 0x18181a), 0.0), icon: .image(icon), - location: .point(sourceRect, .bottom), + location: .point(sourceRect, position), displayDuration: .custom(2.0), shouldDismissOnTouch: { _, _ in return .dismiss(consume: false) diff --git a/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift index 210da5b91d..c82180a741 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift @@ -157,7 +157,7 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode { private func ensureHasTimer() { if self.updateTimer == nil { - let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + let timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in self?.updateTimestamp() }, queue: Queue.mainQueue()) self.updateTimer = timer @@ -182,7 +182,12 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode { duration = trimRange.upperBound - trimRange.lowerBound } - if self.showDurationIfNotStarted && (timestamp < .ulpOfOne || self.isScrubbing) { + var isPlaying = false + if case .playing = statusValue.status { + isPlaying = true + } + + if self.showDurationIfNotStarted && (timestamp < .ulpOfOne || self.isScrubbing) && !isPlaying { let timestamp = Int32(duration) self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) } else { diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 5c7b6162d6..904f2a2e26 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -206,6 +206,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case multipleStoriesTooltip = 79 case voiceMessagesPauseSuggestion = 80 case videoMessagesPauseSuggestion = 81 + case voiceMessagesResumeTrimWarning = 82 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -579,6 +580,10 @@ private struct ApplicationSpecificNoticeKeys { static func videoMessagesPauseSuggestion() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.videoMessagesPauseSuggestion.key) } + + static func voiceMessagesResumeTrimWarning() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.voiceMessagesResumeTrimWarning.key) + } } public struct ApplicationSpecificNotice { @@ -2522,4 +2527,31 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } + + public static func getVoiceMessagesResumeTrimWarning(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.voiceMessagesResumeTrimWarning())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementVoiceMessagesResumeTrimWarning(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.voiceMessagesResumeTrimWarning())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.voiceMessagesResumeTrimWarning(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 49ba1bb019..5bccd7f854 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -325,7 +325,7 @@ extension ChatControllerImpl { ChatInterfaceMediaDraftState.Audio( resource: resource!, fileSize: Int32(data.compressedData.count), - duration: Int32(data.duration), + duration: data.duration, waveform: audioWaveform, trimRange: data.trimRange, resumeData: data.resumeData @@ -434,7 +434,7 @@ extension ChatControllerImpl { $0.updatedInterfaceState { $0.withUpdatedMediaDraftState(.video( ChatInterfaceMediaDraftState.Video( - duration: Int32(data.duration), + duration: data.duration, frames: data.frames, framesUpdateTimestamp: data.framesUpdateTimestamp, trimRange: data.trimRange @@ -500,24 +500,34 @@ extension ChatControllerImpl { }) } - //TODO:localize - if let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let _ = audio.trimRange { - self.present( - textAlertController( - context: self.context, - title: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Title, - text: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Text, - actions: [ - TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Proceed, action: { - proceed() - }) - ] - ), in: .window(.root) - ) - } else { - proceed() - } + let _ = (ApplicationSpecificNotice.getVoiceMessagesResumeTrimWarning(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] count in + guard let self else { + return + } + if count > 0 { + proceed() + return + } + if let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange, trimRange.lowerBound > 0.1 || trimRange.upperBound < audio.duration { + self.present( + textAlertController( + context: self.context, + title: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Title, + text: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Text, + actions: [ + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Proceed, action: { + proceed() + let _ = ApplicationSpecificNotice.incrementVoiceMessagesResumeTrimWarning(accountManager: self.context.sharedContext.accountManager).start() + }) + ] + ), in: .window(.root) + ) + } else { + proceed() + } + }) } } diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index 15e13c963b..1374a6d3db 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -509,16 +509,17 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { })) } + let minDuration = max(2.0, 56.0 * audio.duration / waveformBackgroundFrame.size.width) let (leftHandleFrame, rightHandleFrame) = self.trimView.update( style: .voiceMessage, theme: interfaceState.theme, visualInsets: .zero, scrubberSize: waveformBackgroundFrame.size, - duration: Double(audio.duration), + duration: audio.duration, startPosition: audio.trimRange?.lowerBound ?? 0.0, endPosition: audio.trimRange?.upperBound ?? Double(audio.duration), position: 0.0, - minDuration: 2.0, + minDuration: minDuration, maxDuration: Double(audio.duration), transition: .immediate ) @@ -530,7 +531,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { if !updatedEnd { self.mediaPlayer?.seek(timestamp: start, play: true) } else { - self.mediaPlayer?.seek(timestamp: end - 1.0, play: true) + self.mediaPlayer?.seek(timestamp: max(0.0, end - 1.0), play: true) } self.playButtonNode.durationLabel.isScrubbing = false Queue.mainQueue().after(0.1) { @@ -547,6 +548,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } self.trimView.frame = waveformBackgroundFrame + self.trimView.isHidden = audio.duration < 2.0 let playButtonSize = CGSize(width: max(0.0, rightHandleFrame.minX - leftHandleFrame.maxX), height: waveformBackgroundFrame.height) self.playButtonNode.update(size: playButtonSize, transition: transition) @@ -823,18 +825,23 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange { let _ = (mediaPlayer.status + |> map(Optional.init) + |> timeout(0.3, queue: Queue.mainQueue(), alternate: .single(nil)) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] status in guard let self, let mediaPlayer = self.mediaPlayer else { return } - - if case .playing = status.status { - mediaPlayer.pause() - } else if status.timestamp <= trimRange.lowerBound { - mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true) + if let status { + if case .playing = status.status { + mediaPlayer.pause() + } else if status.timestamp <= trimRange.lowerBound { + mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true) + } else { + mediaPlayer.play() + } } else { - mediaPlayer.play() + mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true) } }) } else { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 391f885cfc..51494fcd2a 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -927,6 +927,15 @@ func openResolvedUrlImpl( } } + if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController { + for controller in navigationController.overlayControllers { + controller.dismiss() + } + for controller in navigationController.globalOverlayControllers { + controller.dismiss() + } + } + let _ = (context.engine.messages.checkStoriesUploadAvailability(target: .myStories) |> deliverOnMainQueue).start(next: { availability in if case let .available(remainingCount) = availability { diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index bc283f2462..510c3c53ac 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -738,6 +738,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { backgroundFrame = CGRect(origin: CGPoint(x: rect.midX - backgroundWidth / 2.0, y: rect.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight)) case .right: backgroundFrame = CGRect(origin: CGPoint(x: rect.minX - backgroundWidth - bottomInset, y: rect.midY - backgroundHeight / 2.0), size: CGSize(width: backgroundWidth, height: backgroundHeight)) + case .left: + backgroundFrame = CGRect(origin: CGPoint(x: rect.maxX + bottomInset, y: rect.midY - backgroundHeight / 2.0), size: CGSize(width: backgroundWidth, height: backgroundHeight)) + } if backgroundFrame.minX < sideInset { @@ -808,6 +811,17 @@ private final class TooltipScreenNode: ViewControllerTracingNode { transition.updateFrame(node: self.arrowContainer, frame: arrowFrame.offsetBy(dx: 8.0 - UIScreenPixel, dy: 0.0)) + let arrowBounds = CGRect(origin: .zero, size: arrowSize) + self.arrowNode.frame = arrowBounds + self.arrowGradientNode?.frame = arrowBounds + case .left: + let arrowCenterY = floorToScreenPixels(rect.midY - arrowSize.height / 2.0) + arrowFrame = CGRect(origin: CGPoint(x: -arrowSize.height, y: self.view.convert(CGPoint(x: 0.0, y: arrowCenterY), to: self.arrowContainer.supernode?.view).y), size: CGSize(width: arrowSize.height, height: arrowSize.width)) + + ContainedViewLayoutTransition.immediate.updateTransformRotation(node: self.arrowContainer, angle: CGFloat.pi / 2.0) + + transition.updateFrame(node: self.arrowContainer, frame: arrowFrame.offsetBy(dx: 3.0 - UIScreenPixel, dy: -19.0)) + let arrowBounds = CGRect(origin: .zero, size: arrowSize) self.arrowNode.frame = arrowBounds self.arrowGradientNode?.frame = arrowBounds @@ -1073,6 +1087,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { startPoint = CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0) case .right: startPoint = CGPoint(x: self.arrowContainer.frame.maxX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.minY - self.containerNode.bounds.height / 2.0) + case .left: + startPoint = CGPoint(x: self.arrowContainer.frame.minX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.minY - self.containerNode.bounds.height / 2.0) } self.containerNode.layer.animateSpring(from: NSValue(cgPoint: startPoint), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, damping: 105.0, additive: true) @@ -1123,6 +1139,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { targetPoint = CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0) case .right: targetPoint = CGPoint(x: self.arrowContainer.frame.maxX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.minY - self.containerNode.bounds.height / 2.0) + case .left: + targetPoint = CGPoint(x: self.arrowContainer.frame.minX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.minY - self.containerNode.bounds.height / 2.0) } self.containerNode.layer.animatePosition(from: CGPoint(), to: targetPoint, duration: 0.2, removeOnCompletion: false, additive: true) @@ -1179,6 +1197,7 @@ public final class TooltipScreen: ViewController { case top case right case bottom + case left } public enum ArrowStyle {