Private
Public Access
1
0

Implements attachment uploading

This commit is contained in:
2025-09-03 22:38:26 -07:00
parent b2f8abfbff
commit 46755a07ef
3 changed files with 106 additions and 11 deletions

View File

@@ -12,11 +12,12 @@ enum AttachmentState
{
case staged
case uploading(progress: Double)
case uploaded
case uploaded(String) // fileTransferGUID
case failed
}
struct OutgoingAttachment: Identifiable, Hashable
@Observable
class OutgoingAttachment: Identifiable, Hashable
{
let id: String
let originalURL: URL

View File

@@ -47,12 +47,11 @@ struct MessageEntryView: View
Button("Send") {
viewModel.sendDraft(to: selectedConversation)
}
.disabled(viewModel.draftText.isEmpty)
.disabled(viewModel.draftText.isEmpty && viewModel.uploadedAttachmentGUIDs.isEmpty)
.keyboardShortcut(.defaultAction)
}
.padding(10.0)
}
.onChange(of: selectedConversation) { oldValue, newValue in
if oldValue?.id != newValue?.id {
viewModel.draftText = ""
@@ -69,20 +68,61 @@ struct MessageEntryView: View
var attachments: [OutgoingAttachment] = []
var isDropTargeted: Bool = false
var uploadedAttachmentGUIDs: Set<String> {
attachments.reduce(Set<String>()) { partialResult, attachment in
switch attachment.state {
case .uploaded(let guid): partialResult.union([ guid ])
default: partialResult
}
}
}
private let client = XPCClient()
private var uploadGuidToAttachmentId: [String: String] = [:]
private var signalTask: Task<Void, Never>? = nil
init() {
signalTask = Task { [weak self] in
guard let self else { return }
for await signal in client.eventStream() {
switch signal {
case .attachmentUploaded(let uploadGuid, let attachmentGuid):
// Mark local attachment as uploaded when the daemon confirms
await MainActor.run {
if let localId = self.uploadGuidToAttachmentId[uploadGuid],
let idx = self.attachments.firstIndex(where: { $0.id == localId }) {
self.attachments[idx].state = .uploaded(attachmentGuid)
self.uploadGuidToAttachmentId.removeValue(forKey: uploadGuid)
}
}
default:
break
}
}
}
}
deinit { signalTask?.cancel() }
func sendDraft(to convo: Display.Conversation?) {
guard let convo else { return }
guard !draftText.isEmpty else { return }
guard !(draftText.isEmpty && uploadedAttachmentGUIDs.isEmpty) else { return }
let messageText = self.draftText
.trimmingCharacters(in: .whitespacesAndNewlines)
let transferGuids = self.uploadedAttachmentGUIDs
self.draftText = ""
self.attachments = []
Task {
let xpc = XPCClient()
do {
try await xpc.sendMessage(conversationId: convo.id, message: messageText)
try await client.sendMessage(
conversationId: convo.id,
message: messageText,
transferGuids: transferGuids
)
} catch {
print("Sending error: \(error)")
}
@@ -123,7 +163,37 @@ struct MessageEntryView: View
private func stageAndAppend(url: URL) {
guard let att = AttachmentManager.shared.stageIfImage(url: url) else { return }
Task { @MainActor in attachments.append(att) }
Task { @MainActor in
attachments.append(att)
startUploadIfNeeded(for: att.id)
}
}
private func startUploadIfNeeded(for attachmentId: String) {
guard let idx = attachments.firstIndex(where: { $0.id == attachmentId }) else { return }
let attachment = attachments[idx]
switch attachment.state {
case .staged, .failed:
attachments[idx].state = .uploading(progress: 0.0)
Task {
do {
let guid = try await client.uploadAttachment(path: attachment.stagedURL.path)
await MainActor.run {
self.uploadGuidToAttachmentId[guid] = attachmentId
}
} catch {
await MainActor.run {
if let i = self.attachments.firstIndex(where: { $0.id == attachmentId }) {
self.attachments[i].state = .failed
}
}
}
}
default:
break
}
}
}
@@ -173,7 +243,8 @@ struct MessageEntryView: View
}
}
struct AttachmentThumbnail: View {
struct AttachmentThumbnail: View
{
let attachment: OutgoingAttachment
var body: some View {

View File

@@ -133,10 +133,14 @@ final class XPCClient
return results
}
public func sendMessage(conversationId: String, message: String) async throws {
public func sendMessage(conversationId: String, message: String, transferGuids: Set<String>) async throws {
var args: [String: xpc_object_t] = [:]
args["conversation_id"] = xpcString(conversationId)
args["text"] = xpcString(message)
if !transferGuids.isEmpty {
args["attachment_guids"] = xpcStringArray(transferGuids)
}
let req = makeRequest(method: "SendMessage", arguments: args)
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError }
@@ -150,6 +154,16 @@ final class XPCClient
let req = makeRequest(method: "DownloadAttachment", arguments: args)
_ = try await sendSync(req)
}
public func uploadAttachment(path: String) async throws -> String {
var args: [String: xpc_object_t] = [:]
args["path"] = xpcString(path)
let req = makeRequest(method: "UploadAttachment", arguments: args)
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError }
guard let guid: String = reply["upload_guid"] else { throw Error.encodingError }
return guid
}
public func openAttachmentFileHandle(attachmentId: String, preview: Bool) async throws -> FileHandle {
var args: [String: xpc_object_t] = [:]
@@ -274,6 +288,15 @@ extension XPCClient
return s.withCString { ptr in xpc_string_create(ptr) }
}
private func xpcStringArray(_ set: Set<String>) -> xpc_object_t {
let array = xpc_array_create(nil, 0)
for str in set {
xpc_array_append_value(array, xpcString(str))
}
return array
}
private func sendSync(_ request: xpc_object_t) async throws -> xpc_object_t? {
try await withCheckedThrowingContinuation { continuation in
let conn: xpc_connection_t? = self.connectionQueue.sync { self.connection }