mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-07 08:01:10 +00:00
Merge commit '0077a070f2e6a9cb831b6e017260edd0f29d91e8'
This commit is contained in:
commit
98dc32fe4c
@ -282,7 +282,7 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||||||
public struct Audio: Codable, Equatable {
|
public struct Audio: Codable, Equatable {
|
||||||
public let resource: LocalFileMediaResource
|
public let resource: LocalFileMediaResource
|
||||||
public let fileSize: Int32
|
public let fileSize: Int32
|
||||||
public let duration: Int32
|
public let duration: Double
|
||||||
public let waveform: AudioWaveform
|
public let waveform: AudioWaveform
|
||||||
public let trimRange: Range<Double>?
|
public let trimRange: Range<Double>?
|
||||||
public let resumeData: Data?
|
public let resumeData: Data?
|
||||||
@ -290,7 +290,7 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||||||
public init(
|
public init(
|
||||||
resource: LocalFileMediaResource,
|
resource: LocalFileMediaResource,
|
||||||
fileSize: Int32,
|
fileSize: Int32,
|
||||||
duration: Int32,
|
duration: Double,
|
||||||
waveform: AudioWaveform,
|
waveform: AudioWaveform,
|
||||||
trimRange: Range<Double>?,
|
trimRange: Range<Double>?,
|
||||||
resumeData: Data?
|
resumeData: Data?
|
||||||
@ -310,7 +310,12 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||||||
self.resource = LocalFileMediaResource(decoder: PostboxDecoder(buffer: MemoryBuffer(data: resourceData.data)))
|
self.resource = LocalFileMediaResource(decoder: PostboxDecoder(buffer: MemoryBuffer(data: resourceData.data)))
|
||||||
|
|
||||||
self.fileSize = try container.decode(Int32.self, forKey: "s")
|
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 waveformData = try container.decode(Data.self, forKey: "wd")
|
||||||
let waveformPeak = try container.decode(Int32.self, forKey: "wp")
|
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(PostboxEncoder().encodeObjectToRawData(self.resource), forKey: "r")
|
||||||
try container.encode(self.fileSize, forKey: "s")
|
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.samples, forKey: "wd")
|
||||||
try container.encode(self.waveform.peak, forKey: "wp")
|
try container.encode(self.waveform.peak, forKey: "wp")
|
||||||
|
|
||||||
@ -368,13 +373,13 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct Video: Codable, Equatable {
|
public struct Video: Codable, Equatable {
|
||||||
public let duration: Int32
|
public let duration: Double
|
||||||
public let frames: [UIImage]
|
public let frames: [UIImage]
|
||||||
public let framesUpdateTimestamp: Double
|
public let framesUpdateTimestamp: Double
|
||||||
public let trimRange: Range<Double>?
|
public let trimRange: Range<Double>?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
duration: Int32,
|
duration: Double,
|
||||||
frames: [UIImage],
|
frames: [UIImage],
|
||||||
framesUpdateTimestamp: Double,
|
framesUpdateTimestamp: Double,
|
||||||
trimRange: Range<Double>?
|
trimRange: Range<Double>?
|
||||||
@ -388,7 +393,11 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
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.frames = []
|
||||||
self.framesUpdateTimestamp = try container.decode(Double.self, forKey: "fu")
|
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") {
|
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 {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
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")
|
try container.encode(self.framesUpdateTimestamp, forKey: "fu")
|
||||||
if let trimRange = self.trimRange {
|
if let trimRange = self.trimRange {
|
||||||
try container.encode(trimRange.lowerBound, forKey: "tl")
|
try container.encode(trimRange.lowerBound, forKey: "tl")
|
||||||
|
@ -271,13 +271,25 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
|
|||||||
return
|
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(
|
let controller = TooltipScreen(
|
||||||
account: context.account,
|
account: context.account,
|
||||||
sharedContext: context.sharedContext,
|
sharedContext: context.sharedContext,
|
||||||
text: .plain(text: text),
|
text: .plain(text: text),
|
||||||
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
|
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
|
||||||
icon: .image(icon),
|
icon: .image(icon),
|
||||||
location: .point(sourceRect, .bottom),
|
location: .point(sourceRect, position),
|
||||||
displayDuration: .custom(2.0),
|
displayDuration: .custom(2.0),
|
||||||
shouldDismissOnTouch: { _, _ in
|
shouldDismissOnTouch: { _, _ in
|
||||||
return .dismiss(consume: false)
|
return .dismiss(consume: false)
|
||||||
|
@ -157,7 +157,7 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
|||||||
|
|
||||||
private func ensureHasTimer() {
|
private func ensureHasTimer() {
|
||||||
if self.updateTimer == nil {
|
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()
|
self?.updateTimestamp()
|
||||||
}, queue: Queue.mainQueue())
|
}, queue: Queue.mainQueue())
|
||||||
self.updateTimer = timer
|
self.updateTimer = timer
|
||||||
@ -182,7 +182,12 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
|||||||
duration = trimRange.upperBound - trimRange.lowerBound
|
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)
|
let timestamp = Int32(duration)
|
||||||
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
|
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
|
||||||
} else {
|
} else {
|
||||||
|
@ -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 {
|
var isForumOrMonoForum: Bool {
|
||||||
if let channel = self as? TelegramChannel {
|
if let channel = self as? TelegramChannel {
|
||||||
return channel.flags.contains(.isForum) || channel.flags.contains(.isMonoforum)
|
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 {
|
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum), let linkedMonoforumId = channel.linkedMonoforumId {
|
||||||
return view.peers[linkedMonoforumId]
|
return view.peers[linkedMonoforumId]
|
||||||
} else {
|
} else {
|
||||||
return peer
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -206,6 +206,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
|
|||||||
case multipleStoriesTooltip = 79
|
case multipleStoriesTooltip = 79
|
||||||
case voiceMessagesPauseSuggestion = 80
|
case voiceMessagesPauseSuggestion = 80
|
||||||
case videoMessagesPauseSuggestion = 81
|
case videoMessagesPauseSuggestion = 81
|
||||||
|
case voiceMessagesResumeTrimWarning = 82
|
||||||
|
|
||||||
var key: ValueBoxKey {
|
var key: ValueBoxKey {
|
||||||
let v = ValueBoxKey(length: 4)
|
let v = ValueBoxKey(length: 4)
|
||||||
@ -579,6 +580,10 @@ private struct ApplicationSpecificNoticeKeys {
|
|||||||
static func videoMessagesPauseSuggestion() -> NoticeEntryKey {
|
static func videoMessagesPauseSuggestion() -> NoticeEntryKey {
|
||||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.videoMessagesPauseSuggestion.key)
|
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 {
|
public struct ApplicationSpecificNotice {
|
||||||
@ -2522,4 +2527,31 @@ public struct ApplicationSpecificNotice {
|
|||||||
return Int(previousValue)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -325,7 +325,7 @@ extension ChatControllerImpl {
|
|||||||
ChatInterfaceMediaDraftState.Audio(
|
ChatInterfaceMediaDraftState.Audio(
|
||||||
resource: resource!,
|
resource: resource!,
|
||||||
fileSize: Int32(data.compressedData.count),
|
fileSize: Int32(data.compressedData.count),
|
||||||
duration: Int32(data.duration),
|
duration: data.duration,
|
||||||
waveform: audioWaveform,
|
waveform: audioWaveform,
|
||||||
trimRange: data.trimRange,
|
trimRange: data.trimRange,
|
||||||
resumeData: data.resumeData
|
resumeData: data.resumeData
|
||||||
@ -439,7 +439,7 @@ extension ChatControllerImpl {
|
|||||||
$0.updatedInterfaceState {
|
$0.updatedInterfaceState {
|
||||||
$0.withUpdatedMediaDraftState(.video(
|
$0.withUpdatedMediaDraftState(.video(
|
||||||
ChatInterfaceMediaDraftState.Video(
|
ChatInterfaceMediaDraftState.Video(
|
||||||
duration: Int32(data.duration),
|
duration: data.duration,
|
||||||
frames: data.frames,
|
frames: data.frames,
|
||||||
framesUpdateTimestamp: data.framesUpdateTimestamp,
|
framesUpdateTimestamp: data.framesUpdateTimestamp,
|
||||||
trimRange: data.trimRange
|
trimRange: data.trimRange
|
||||||
@ -494,7 +494,7 @@ extension ChatControllerImpl {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let proceed = {
|
let proceed = {
|
||||||
self.withAudioRecorder({ audioRecorder in
|
self.withAudioRecorder(resuming: true, { audioRecorder in
|
||||||
audioRecorder.resume()
|
audioRecorder.resume()
|
||||||
|
|
||||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||||
@ -505,24 +505,34 @@ extension ChatControllerImpl {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO:localize
|
let _ = (ApplicationSpecificNotice.getVoiceMessagesResumeTrimWarning(accountManager: self.context.sharedContext.accountManager)
|
||||||
if let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let _ = audio.trimRange {
|
|> deliverOnMainQueue).start(next: { [weak self] count in
|
||||||
self.present(
|
guard let self else {
|
||||||
textAlertController(
|
return
|
||||||
context: self.context,
|
}
|
||||||
title: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Title,
|
if count > 0 {
|
||||||
text: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Text,
|
proceed()
|
||||||
actions: [
|
return
|
||||||
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}),
|
}
|
||||||
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Proceed, action: {
|
if let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange, trimRange.lowerBound > 0.1 || trimRange.upperBound < audio.duration {
|
||||||
proceed()
|
self.present(
|
||||||
})
|
textAlertController(
|
||||||
]
|
context: self.context,
|
||||||
), in: .window(.root)
|
title: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Title,
|
||||||
)
|
text: self.presentationData.strings.Chat_TrimVoiceMessageToResume_Text,
|
||||||
} else {
|
actions: [
|
||||||
proceed()
|
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))
|
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 {
|
if let audioRecorder = self.audioRecorderValue {
|
||||||
f(audioRecorder)
|
f(audioRecorder)
|
||||||
} else if let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview {
|
} else if let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview {
|
||||||
self.requestAudioRecorder(beginWithTone: false, existingDraft: audio)
|
self.requestAudioRecorder(beginWithTone: false, existingDraft: audio)
|
||||||
if let audioRecorder = self.audioRecorderValue {
|
if let audioRecorder = self.audioRecorderValue {
|
||||||
f(audioRecorder)
|
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 {
|
if let videoRecorder = self.videoRecorderValue {
|
||||||
videoRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
|
videoRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
|
||||||
} else {
|
} else {
|
||||||
self.withAudioRecorder({ audioRecorder in
|
self.withAudioRecorder(resuming: false, { audioRecorder in
|
||||||
audioRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
|
audioRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ final class PlayButtonNode: ASDisplayNode {
|
|||||||
|
|
||||||
transition.updateFrame(node: self.backgroundNode, frame: buttonSize.centered(in: CGRect(origin: .zero, size: size)))
|
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.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)
|
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(
|
let (leftHandleFrame, rightHandleFrame) = self.trimView.update(
|
||||||
style: .voiceMessage,
|
style: .voiceMessage,
|
||||||
theme: interfaceState.theme,
|
theme: interfaceState.theme,
|
||||||
visualInsets: .zero,
|
visualInsets: .zero,
|
||||||
scrubberSize: waveformBackgroundFrame.size,
|
scrubberSize: waveformBackgroundFrame.size,
|
||||||
duration: Double(audio.duration),
|
duration: audio.duration,
|
||||||
startPosition: audio.trimRange?.lowerBound ?? 0.0,
|
startPosition: audio.trimRange?.lowerBound ?? 0.0,
|
||||||
endPosition: audio.trimRange?.upperBound ?? Double(audio.duration),
|
endPosition: audio.trimRange?.upperBound ?? Double(audio.duration),
|
||||||
position: 0.0,
|
position: 0.0,
|
||||||
minDuration: 2.0,
|
minDuration: minDuration,
|
||||||
maxDuration: Double(audio.duration),
|
maxDuration: Double(audio.duration),
|
||||||
transition: .immediate
|
transition: .immediate
|
||||||
)
|
)
|
||||||
@ -530,7 +531,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||||||
if !updatedEnd {
|
if !updatedEnd {
|
||||||
self.mediaPlayer?.seek(timestamp: start, play: true)
|
self.mediaPlayer?.seek(timestamp: start, play: true)
|
||||||
} else {
|
} 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
|
self.playButtonNode.durationLabel.isScrubbing = false
|
||||||
Queue.mainQueue().after(0.1) {
|
Queue.mainQueue().after(0.1) {
|
||||||
@ -547,6 +548,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.trimView.frame = waveformBackgroundFrame
|
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)
|
let playButtonSize = CGSize(width: max(0.0, rightHandleFrame.minX - leftHandleFrame.maxX), height: waveformBackgroundFrame.height)
|
||||||
self.playButtonNode.update(size: playButtonSize, transition: transition)
|
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 {
|
if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange {
|
||||||
let _ = (mediaPlayer.status
|
let _ = (mediaPlayer.status
|
||||||
|
|> map(Optional.init)
|
||||||
|
|> timeout(0.3, queue: Queue.mainQueue(), alternate: .single(nil))
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
guard let self, let mediaPlayer = self.mediaPlayer else {
|
guard let self, let mediaPlayer = self.mediaPlayer else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if let status {
|
||||||
if case .playing = status.status {
|
if case .playing = status.status {
|
||||||
mediaPlayer.pause()
|
mediaPlayer.pause()
|
||||||
} else if status.timestamp <= trimRange.lowerBound {
|
} else if status.timestamp <= trimRange.lowerBound {
|
||||||
mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true)
|
mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true)
|
||||||
|
} else {
|
||||||
|
mediaPlayer.play()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mediaPlayer.play()
|
mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -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)
|
let _ = (context.engine.messages.checkStoriesUploadAvailability(target: .myStories)
|
||||||
|> deliverOnMainQueue).start(next: { availability in
|
|> deliverOnMainQueue).start(next: { availability in
|
||||||
if case let .available(remainingCount) = availability {
|
if case let .available(remainingCount) = availability {
|
||||||
|
@ -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))
|
backgroundFrame = CGRect(origin: CGPoint(x: rect.midX - backgroundWidth / 2.0, y: rect.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
|
||||||
case .right:
|
case .right:
|
||||||
backgroundFrame = CGRect(origin: CGPoint(x: rect.minX - backgroundWidth - bottomInset, y: rect.midY - backgroundHeight / 2.0), size: CGSize(width: backgroundWidth, height: backgroundHeight))
|
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 {
|
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))
|
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)
|
let arrowBounds = CGRect(origin: .zero, size: arrowSize)
|
||||||
self.arrowNode.frame = arrowBounds
|
self.arrowNode.frame = arrowBounds
|
||||||
self.arrowGradientNode?.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)
|
startPoint = CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)
|
||||||
case .right:
|
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)
|
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)
|
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)
|
targetPoint = CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)
|
||||||
case .right:
|
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)
|
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)
|
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 top
|
||||||
case right
|
case right
|
||||||
case bottom
|
case bottom
|
||||||
|
case left
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ArrowStyle {
|
public enum ArrowStyle {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user