From f3e59b99519e4b088270c57e66f4647bcc5962ad Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 12 Jun 2025 19:26:49 -0700 Subject: [PATCH] Adds ui support for attachments, results not yet connected to daemon --- src/application/main-window.vala | 7 +- src/meson.build | 1 + src/resources/style.css | 17 ++ src/transcript/attachment-preview.vala | 93 ++++++++++ src/transcript/transcript-container-view.vala | 172 +++++++++++++++++- 5 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 src/transcript/attachment-preview.vala diff --git a/src/application/main-window.vala b/src/application/main-window.vala index e53a6c9..63e0cc4 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -53,7 +53,10 @@ public class MainWindow : Adw.ApplicationWindow } } - private void on_transcript_send(string message) { + private void on_transcript_send(TranscriptContainerView view) { + var body = view.message_body; + var attachment_guids = view.attachment_guids; + if (transcript_container_view.transcript_view.model == null) { GLib.warning("No conversation selected"); return; @@ -66,7 +69,7 @@ public class MainWindow : Adw.ApplicationWindow } try { - Repository.get_instance().send_message(selected_conversation, message); + Repository.get_instance().send_message(selected_conversation, body); } catch (Error e) { GLib.warning("Failed to send message: %s", e.message); } diff --git a/src/meson.build b/src/meson.build index 6a27d2d..f1ec3f0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -28,6 +28,7 @@ sources = [ 'conversation-list/conversation-list-model.vala', 'conversation-list/conversation-row.vala', + 'transcript/attachment-preview.vala', 'transcript/message-list-model.vala', 'transcript/transcript-container-view.vala', 'transcript/transcript-drawing-area.vala', diff --git a/src/resources/style.css b/src/resources/style.css index 58b7ebb..5b04e69 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -30,5 +30,22 @@ font-size: 1.1rem; } +.attachment-preview-row { + background-color: alpha(@window_bg_color, 0.3); + border-radius: 8px; + padding: 8px; +} +.attachment-preview { + border-radius: 8px; + overflow: hidden; + border: 1px solid alpha(@borders, 0.5); +} +.attachment-preview.completed { + border-color: @success_color; +} + +.attachment-image { + border-radius: 8px; +} diff --git a/src/transcript/attachment-preview.vala b/src/transcript/attachment-preview.vala new file mode 100644 index 0000000..d9568af --- /dev/null +++ b/src/transcript/attachment-preview.vala @@ -0,0 +1,93 @@ +using Gtk; +using Gdk; + +class AttachmentPreview : Gtk.Box { + public signal void remove_requested(); + + private File file; + private string upload_guid; + private string? attachment_guid = null; + private bool is_completed = false; + + private Overlay overlay; + private Image picture; + private Spinner spinner; + private Button remove_button; + + public AttachmentPreview(File file, string upload_guid) { + Object(orientation: Orientation.VERTICAL, spacing: 0); + this.file = file; + this.upload_guid = upload_guid; + + setup_ui(); + load_image(); + } + + private void setup_ui() { + set_size_request(100, 100); + add_css_class("attachment-preview"); + + overlay = new Overlay(); + overlay.set_size_request(100, 100); + append(overlay); + + // Image preview + picture = new Image(); + overlay.set_child(picture); + + // Loading spinner + spinner = new Spinner(); + spinner.set_halign(Align.CENTER); + spinner.set_valign(Align.CENTER); + spinner.set_size_request(24, 24); + spinner.start(); + overlay.add_overlay(spinner); + + // Remove button + remove_button = new Button(); + remove_button.set_icon_name("window-close-symbolic"); + remove_button.add_css_class("circular"); + remove_button.add_css_class("destructive-action"); + remove_button.set_halign(Align.END); + remove_button.set_valign(Align.START); + remove_button.set_margin_top(4); + remove_button.set_margin_end(4); + remove_button.set_size_request(20, 20); + remove_button.clicked.connect(() => { + remove_requested(); + }); + overlay.add_overlay(remove_button); + } + + private void load_image() { + try { + picture.set_from_file(file.get_path()); + } catch (Error e) { + warning("Failed to load image preview: %s", e.message); + + // Show a placeholder icon if image loading fails + var icon = new Image.from_icon_name("image-x-generic"); + icon.set_pixel_size(48); + overlay.set_child(icon); + } + } + + public void set_completed(string attachment_guid) { + this.attachment_guid = attachment_guid; + this.is_completed = true; + + spinner.stop(); + spinner.set_visible(false); + + // Optionally change visual state to indicate completion + add_css_class("completed"); + } + + public string? get_attachment_guid() { + return attachment_guid; + } + + public bool get_is_completed() { + return is_completed; + } +} \ No newline at end of file diff --git a/src/transcript/transcript-container-view.vala b/src/transcript/transcript-container-view.vala index 9a17edb..9b26d06 100644 --- a/src/transcript/transcript-container-view.vala +++ b/src/transcript/transcript-container-view.vala @@ -1,29 +1,67 @@ using Gtk; using Adw; +using Gee; -class TranscriptContainerView : Adw.Bin { +class TranscriptContainerView : Adw.Bin +{ public TranscriptView transcript_view; - public Entry message_entry; - public signal void on_send(string message); + public signal void on_send(TranscriptContainerView view); private Box container; private Button send_button; + private FlowBox attachment_flow_box; + + private Entry message_entry; + private HashSet pending_uploads; + private HashMap attachment_previews; + private ArrayList completed_attachments; + + public string message_body { + get { + return message_entry.text; + } + } + + public ArrayList attachment_guids { + owned get { + var attachment_guids = new ArrayList(); + completed_attachments.foreach((attachment) => { + attachment_guids.add(attachment.attachment_guid); + return true; + }); + + return attachment_guids; + } + } public TranscriptContainerView () { container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); set_child (container); + + pending_uploads = new HashSet(); + attachment_previews = new HashMap(); + completed_attachments = new ArrayList(); // Create message list view transcript_view = new TranscriptView(); transcript_view.set_vexpand(true); container.append(transcript_view); + // Create attachment preview row (initially hidden) + setup_attachment_row(); + // Create bottom box for input var input_box = new Box(Orientation.HORIZONTAL, 6); input_box.add_css_class("message-input-box"); input_box.set_valign(Align.END); input_box.set_spacing(6); container.append(input_box); + + // Setup drag and drop + setup_drag_and_drop(); + + // Connect to repository signals + Repository.get_instance().attachment_uploaded.connect(on_attachment_uploaded); // Create message entry message_entry = new Entry(); @@ -42,16 +80,138 @@ class TranscriptContainerView : Adw.Bin { send_button.clicked.connect(on_request_send); input_box.append(send_button); } + + private void setup_attachment_row() { + attachment_flow_box = new FlowBox(); + attachment_flow_box.set_max_children_per_line(6); + attachment_flow_box.set_row_spacing(6); + attachment_flow_box.set_column_spacing(6); + attachment_flow_box.halign = Align.START; + attachment_flow_box.add_css_class("attachment-preview-row"); + container.append(attachment_flow_box); + } + + private void setup_drag_and_drop() { + var drop_target = new DropTarget(typeof(File), Gdk.DragAction.COPY); + drop_target.drop.connect(on_file_dropped); + this.add_controller(drop_target); + } + + private bool on_file_dropped(Value val, double x, double y) { + if (!val.holds(typeof(File))) { + return false; + } + + var file = (File)val.get_object(); + if (file == null) { + return false; + } + + // Check if it's an image file + try { + var file_info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); + string content_type = file_info.get_content_type(); + + if (!content_type.has_prefix("image/")) { + return false; + } + + upload_file(file); + return true; + } catch (Error e) { + warning("Failed to get file info: %s", e.message); + return false; + } + } + + private void upload_file(File file) { + try { + string upload_guid = Repository.get_instance().upload_attachment(file.get_path()); + pending_uploads.add(upload_guid); + + var preview = new AttachmentPreview(file, upload_guid); + preview.remove_requested.connect(() => { + remove_attachment(upload_guid); + }); + + attachment_previews[upload_guid] = preview; + attachment_flow_box.append(preview); + + update_attachment_row_visibility(); + } catch (Error e) { + warning("Failed to upload attachment: %s", e.message); + } + } + + private void on_attachment_uploaded(string upload_guid, string attachment_guid) { + if (attachment_previews.has_key(upload_guid)) { + var preview = attachment_previews[upload_guid]; + preview.set_completed(attachment_guid); + completed_attachments.add(new UploadedAttachment(upload_guid, attachment_guid)); + pending_uploads.remove(upload_guid); + update_send_button_sensitivity(); + } + } + + private void remove_attachment(string upload_guid) { + if (attachment_previews.has_key(upload_guid)) { + var preview = attachment_previews[upload_guid]; + attachment_flow_box.remove(preview); + attachment_previews.unset(upload_guid); + + completed_attachments.foreach((attachment) => { + if (attachment.upload_guid == upload_guid) { + completed_attachments.remove(attachment); + return false; + } + + return true; + }); + + update_attachment_row_visibility(); + update_send_button_sensitivity(); + } + } + + private void update_attachment_row_visibility() { + bool has_attachments = attachment_previews.size > 0; + attachment_flow_box.set_visible(has_attachments); + } private void on_text_changed() { - send_button.set_sensitive(message_entry.text.length > 0); + update_send_button_sensitivity(); + } + + private void update_send_button_sensitivity() { + send_button.set_sensitive(message_entry.text.length > 0 && pending_uploads.size == 0); } private void on_request_send() { - if (message_entry.text.length > 0) { - on_send(message_entry.text); + if (message_entry.text.length > 0 && pending_uploads.size == 0) { + on_send(this); + + // Clear the message entry message_entry.text = ""; + + // Clear the attachment previews + attachment_flow_box.remove_all(); + attachment_previews.clear(); + completed_attachments.clear(); + pending_uploads.clear(); + + update_send_button_sensitivity(); } } } +class UploadedAttachment +{ + public string upload_guid; + public string attachment_guid; + + public UploadedAttachment(string upload_guid, string attachment_guid) + { + this.upload_guid = upload_guid; + this.attachment_guid = attachment_guid; + } +} \ No newline at end of file