Implements attachment uploading
This commit is contained in:
@@ -12,11 +12,12 @@ enum AttachmentState
|
|||||||
{
|
{
|
||||||
case staged
|
case staged
|
||||||
case uploading(progress: Double)
|
case uploading(progress: Double)
|
||||||
case uploaded
|
case uploaded(String) // fileTransferGUID
|
||||||
case failed
|
case failed
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OutgoingAttachment: Identifiable, Hashable
|
@Observable
|
||||||
|
class OutgoingAttachment: Identifiable, Hashable
|
||||||
{
|
{
|
||||||
let id: String
|
let id: String
|
||||||
let originalURL: URL
|
let originalURL: URL
|
||||||
|
|||||||
@@ -47,12 +47,11 @@ struct MessageEntryView: View
|
|||||||
Button("Send") {
|
Button("Send") {
|
||||||
viewModel.sendDraft(to: selectedConversation)
|
viewModel.sendDraft(to: selectedConversation)
|
||||||
}
|
}
|
||||||
.disabled(viewModel.draftText.isEmpty)
|
.disabled(viewModel.draftText.isEmpty && viewModel.uploadedAttachmentGUIDs.isEmpty)
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
}
|
}
|
||||||
.padding(10.0)
|
.padding(10.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
.onChange(of: selectedConversation) { oldValue, newValue in
|
.onChange(of: selectedConversation) { oldValue, newValue in
|
||||||
if oldValue?.id != newValue?.id {
|
if oldValue?.id != newValue?.id {
|
||||||
viewModel.draftText = ""
|
viewModel.draftText = ""
|
||||||
@@ -69,20 +68,61 @@ struct MessageEntryView: View
|
|||||||
var attachments: [OutgoingAttachment] = []
|
var attachments: [OutgoingAttachment] = []
|
||||||
var isDropTargeted: Bool = false
|
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?) {
|
func sendDraft(to convo: Display.Conversation?) {
|
||||||
guard let convo else { return }
|
guard let convo else { return }
|
||||||
guard !draftText.isEmpty else { return }
|
guard !(draftText.isEmpty && uploadedAttachmentGUIDs.isEmpty) else { return }
|
||||||
|
|
||||||
let messageText = self.draftText
|
let messageText = self.draftText
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
let transferGuids = self.uploadedAttachmentGUIDs
|
||||||
|
|
||||||
self.draftText = ""
|
self.draftText = ""
|
||||||
|
self.attachments = []
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
let xpc = XPCClient()
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await xpc.sendMessage(conversationId: convo.id, message: messageText)
|
try await client.sendMessage(
|
||||||
|
conversationId: convo.id,
|
||||||
|
message: messageText,
|
||||||
|
transferGuids: transferGuids
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
print("Sending error: \(error)")
|
print("Sending error: \(error)")
|
||||||
}
|
}
|
||||||
@@ -123,7 +163,37 @@ struct MessageEntryView: View
|
|||||||
|
|
||||||
private func stageAndAppend(url: URL) {
|
private func stageAndAppend(url: URL) {
|
||||||
guard let att = AttachmentManager.shared.stageIfImage(url: url) else { return }
|
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
|
let attachment: OutgoingAttachment
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -133,10 +133,14 @@ final class XPCClient
|
|||||||
return results
|
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] = [:]
|
var args: [String: xpc_object_t] = [:]
|
||||||
args["conversation_id"] = xpcString(conversationId)
|
args["conversation_id"] = xpcString(conversationId)
|
||||||
args["text"] = xpcString(message)
|
args["text"] = xpcString(message)
|
||||||
|
|
||||||
|
if !transferGuids.isEmpty {
|
||||||
|
args["attachment_guids"] = xpcStringArray(transferGuids)
|
||||||
|
}
|
||||||
|
|
||||||
let req = makeRequest(method: "SendMessage", arguments: args)
|
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 }
|
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)
|
let req = makeRequest(method: "DownloadAttachment", arguments: args)
|
||||||
_ = try await sendSync(req)
|
_ = 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 {
|
public func openAttachmentFileHandle(attachmentId: String, preview: Bool) async throws -> FileHandle {
|
||||||
var args: [String: xpc_object_t] = [:]
|
var args: [String: xpc_object_t] = [:]
|
||||||
@@ -274,6 +288,15 @@ extension XPCClient
|
|||||||
return s.withCString { ptr in xpc_string_create(ptr) }
|
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? {
|
private func sendSync(_ request: xpc_object_t) async throws -> xpc_object_t? {
|
||||||
try await withCheckedThrowingContinuation { continuation in
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
let conn: xpc_connection_t? = self.connectionQueue.sync { self.connection }
|
let conn: xpc_connection_t? = self.connectionQueue.sync { self.connection }
|
||||||
|
|||||||
Reference in New Issue
Block a user