Merge commit '0077a070f2e6a9cb831b6e017260edd0f29d91e8'

This commit is contained in:
Isaac 2025-05-29 00:23:47 +08:00
commit 98dc32fe4c
9 changed files with 186 additions and 45 deletions

View File

@ -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<Double>?
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<Double>?,
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<Double>?
public init(
duration: Int32,
duration: Double,
frames: [UIImage],
framesUpdateTimestamp: Double,
trimRange: Range<Double>?
@ -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")

View File

@ -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)

View File

@ -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 {

View File

@ -279,6 +279,14 @@ public extension Peer {
}
}
var displayForumAsTabs: Bool {
if let channel = self as? TelegramChannel, isForum {
return channel.flags.contains(.displayForumAsTabs)
} else {
return false
}
}
var isForumOrMonoForum: Bool {
if let channel = self as? TelegramChannel {
return channel.flags.contains(.isForum) || channel.flags.contains(.isMonoforum)
@ -460,7 +468,7 @@ public func peerViewMonoforumMainPeer(_ view: PeerView) -> Peer? {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum), let linkedMonoforumId = channel.linkedMonoforumId {
return view.peers[linkedMonoforumId]
} else {
return peer
return nil
}
} else {
return nil

View File

@ -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<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
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<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
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)
}
}
}

View File

@ -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
@ -439,7 +439,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
@ -494,7 +494,7 @@ extension ChatControllerImpl {
})
} else {
let proceed = {
self.withAudioRecorder({ audioRecorder in
self.withAudioRecorder(resuming: true, { audioRecorder in
audioRecorder.resume()
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
@ -505,24 +505,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()
}
})
}
}
@ -607,13 +617,43 @@ extension ChatControllerImpl {
self.present(tooltipController, in: .window(.root))
}
private func withAudioRecorder(_ f: (ManagedAudioRecorder) -> Void) {
private func withAudioRecorder(resuming: Bool, _ f: (ManagedAudioRecorder) -> Void) {
if let audioRecorder = self.audioRecorderValue {
f(audioRecorder)
} else if let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview {
self.requestAudioRecorder(beginWithTone: false, existingDraft: audio)
if let audioRecorder = self.audioRecorderValue {
f(audioRecorder)
if !resuming {
self.recorderDataDisposable.set(
(audioRecorder.takenRecordedData()
|> deliverOnMainQueue).startStrict(
next: { [weak self] data in
if let strongSelf = self, let data = data {
let audioWaveform = audio.waveform
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInterfaceState {
$0.withUpdatedMediaDraftState(.audio(
ChatInterfaceMediaDraftState.Audio(
resource: audio.resource,
fileSize: Int32(data.compressedData.count),
duration: data.duration,
waveform: audioWaveform,
trimRange: data.trimRange,
resumeData: data.resumeData
)
))
}.updatedInputTextPanelState { panelState in
return panelState.withUpdatedMediaRecordingState(nil)
}
})
strongSelf.updateDownButtonVisibility()
}
})
)
}
}
}
}
@ -622,7 +662,7 @@ extension ChatControllerImpl {
if let videoRecorder = self.videoRecorderValue {
videoRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
} else {
self.withAudioRecorder({ audioRecorder in
self.withAudioRecorder(resuming: false, { audioRecorder in
audioRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
})
}

View File

@ -119,7 +119,7 @@ final class PlayButtonNode: ASDisplayNode {
transition.updateFrame(node: self.backgroundNode, frame: buttonSize.centered(in: CGRect(origin: .zero, size: size)))
self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 1.0 - UIScreenPixel), size: CGSize(width: 21.0, height: 21.0))
self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 1.0 - UIScreenPixel), size: CGSize(width: 21.0, height: 21.0))
transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: 18.0, y: 3.0), size: CGSize(width: 35.0, height: 20.0)))
transition.updateAlpha(node: self.durationLabel, alpha: buttonSize.width > 27.0 ? 1.0 : 0.0)
@ -509,16 +509,17 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
}))
}
let minDuration = max(1.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 {

View File

@ -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 {

View File

@ -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 {