Implements attachment uploading
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user