Files
openclaw/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift
Peter Steinberger f95ca90378 fix(ios): persist voice notes through offline delivery (#101236)
* fix(ios): persist voice note delivery state

* fix(ios): pin chats during attachment staging

* fix(ios): keep voice notes on captured chat

* fix(ios): keep voice notes durable through staging

* fix(ios): harden voice note lifecycle

* test(ios): satisfy strict concurrency in voice note proof

* fix(ios): keep legacy attachment sends live

* fix(ios): retain legacy drafts during outbox restore

* fix(ios): pin voice note recipient presentation

* fix(ios): preserve confirmed voice note duration

* chore(i18n): refresh voice note source inventory

* chore: leave release notes to release automation
2026-07-07 01:21:16 +01:00

121 lines
3.9 KiB
Swift

import Foundation
import OpenClawKit
import UniformTypeIdentifiers
#if canImport(AppKit)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif
extension OpenClawChatViewModel {
/// Stages a recorded m4a voice note and removes its temporary file.
public func addVoiceNoteAttachment(fileURL: URL, durationSeconds: Double) async {
self.beginAttachmentStaging()
defer {
try? FileManager.default.removeItem(at: fileURL)
self.endAttachmentStaging()
}
let data: Data
do {
data = try await Task.detached(priority: .userInitiated) {
try Data(contentsOf: fileURL)
}.value
} catch {
self.errorText = String(localized: "Could not attach voice note: \(error.localizedDescription)")
return
}
guard data.count <= Self.maxAttachmentBytes else {
self.errorText = String(localized: "Voice note exceeds the 5 MB attachment limit")
return
}
let normalizedDuration = durationSeconds.isFinite
? min(max(0, durationSeconds), OpenClawVoiceNoteRecorder.maximumDurationSeconds)
: 0
self.attachments.append(
OpenClawPendingAttachment(
url: nil,
data: data,
fileName: fileURL.lastPathComponent,
mimeType: "audio/mp4",
preview: nil,
durationSeconds: normalizedDuration))
}
func loadAttachments(urls: [URL]) async {
for url in urls {
do {
let data = try await Task.detached { try Data(contentsOf: url) }.value
await self.addImageAttachment(
url: url,
data: data,
fileName: url.lastPathComponent,
mimeType: Self.mimeType(for: url) ?? "application/octet-stream")
} catch {
await MainActor.run { self.errorText = error.localizedDescription }
}
}
}
static func mimeType(for url: URL) -> String? {
let ext = url.pathExtension
guard !ext.isEmpty else { return nil }
return (UTType(filenameExtension: ext) ?? .data).preferredMIMEType
}
func addImageAttachment(url: URL?, data: Data, fileName: String, mimeType: String) async {
let uti: UTType = {
if let url {
return UTType(filenameExtension: url.pathExtension) ?? .data
}
return UTType(mimeType: mimeType) ?? .data
}()
guard uti.conforms(to: .image) else {
self.errorText = "Only image attachments are supported right now"
return
}
let processed: Data
do {
processed = try await Task.detached(priority: .userInitiated) {
try ChatImageProcessor.processForUpload(data: data)
}.value
} catch {
self.errorText = "Could not process \(fileName): \(error.localizedDescription)"
return
}
if processed.count > Self.maxAttachmentBytes {
self.errorText = "Attachment \(fileName) exceeds 5 MB limit after resizing"
return
}
let outputFileName: String = {
let baseName = (fileName as NSString).deletingPathExtension
return baseName.isEmpty ? "image.jpg" : "\(baseName).jpg"
}()
let preview = Self.previewImage(data: processed)
self.attachments.append(
OpenClawPendingAttachment(
url: url,
data: processed,
fileName: outputFileName,
mimeType: "image/jpeg",
preview: preview))
}
static func previewImage(data: Data) -> OpenClawPlatformImage? {
#if canImport(AppKit)
NSImage(data: data)
#elseif canImport(UIKit)
UIImage(data: data)
#else
nil
#endif
}
}