diff --git a/src/models/attachment.vala b/src/models/attachment.vala index 05357ac..24e9596 100644 --- a/src/models/attachment.vala +++ b/src/models/attachment.vala @@ -29,6 +29,20 @@ public class AttachmentMetadata : Object { } } +public class AttachmentInfo : Object { + public string? path; + public string? preview_path; + public bool? downloaded; + public bool? preview_downloaded; + + public AttachmentInfo(string? path, string? preview_path, bool? downloaded, bool? preview_downloaded) { + this.path = path; + this.preview_path = preview_path; + this.downloaded = downloaded; + this.preview_downloaded = preview_downloaded; + } +} + public class Attachment : Object { public string guid; public string path; diff --git a/src/service/repository.vala b/src/service/repository.vala index 9c939c4..77c13c5 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -135,4 +135,13 @@ public class Repository : DBusServiceProxy { return dbus_repository.upload_attachment(filename); } + + public AttachmentInfo get_attachment_info(string attachment_guid) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + var info = dbus_repository.get_attachment_info(attachment_guid); + return new AttachmentInfo(info.attr1, info.attr2, info.attr3, info.attr4); + } } diff --git a/src/transcript/layouts/image-bubble-layout.vala b/src/transcript/layouts/image-bubble-layout.vala index 09f5f4b..c957d86 100644 --- a/src/transcript/layouts/image-bubble-layout.vala +++ b/src/transcript/layouts/image-bubble-layout.vala @@ -26,6 +26,7 @@ private class ImageBubbleLayout : BubbleLayout { public string image_path; public bool is_downloaded; + public string? attachment_guid; private Graphene.Size image_size; private Gdk.Texture? cached_texture = null; diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 68fa7a9..6abb32c 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -16,8 +16,9 @@ private class TranscriptDrawingArea : Widget } } - public signal void on_text_bubble_hover(VisibleTextLayout? text_bubble); - public signal void on_text_bubble_click(VisibleTextLayout? text_bubble); + public signal void on_text_bubble_hover(VisibleLayout? text_bubble); + public signal void on_text_bubble_click(VisibleLayout? text_bubble); + public signal void on_image_bubble_activate(string attachment_guid); private ArrayList _messages = new ArrayList(); private ArrayList _chat_items = new ArrayList(); @@ -29,7 +30,7 @@ private class TranscriptDrawingArea : Widget private Gdk.Rectangle? _click_bounding_box = null; private EventControllerMotion _motion_controller = new EventControllerMotion(); - private ArrayList _visible_text_layouts = new ArrayList(); + private ArrayList _visible_text_layouts = new ArrayList(); private const bool debug_viewport = false; private uint? _tick_callback_id = null; @@ -41,8 +42,8 @@ private class TranscriptDrawingArea : Widget weak TranscriptDrawingArea self = this; _click_gesture.button = 0; - _click_gesture.begin.connect(() => { - self.on_click(self._click_gesture.get_current_button()); + _click_gesture.pressed.connect((n_press, x, y) => { + self.on_click(self._click_gesture.get_current_button(), n_press); }); add_controller(_click_gesture); @@ -157,8 +158,8 @@ private class TranscriptDrawingArea : Widget // Skip drawing if this item is not in the viewport float height_offset = 0.0f; if (viewport_rect.intersection(rect, null)) { - if (chat_item is TextBubbleLayout) { - _visible_text_layouts.add(VisibleTextLayout(chat_item as TextBubbleLayout, rect)); + if (chat_item is BubbleLayout) { + _visible_text_layouts.add(VisibleLayout(chat_item as BubbleLayout, rect)); } snapshot.save(); @@ -199,9 +200,10 @@ private class TranscriptDrawingArea : Widget animation_tick(); } - private VisibleTextLayout? get_text_bubble_at(double x, double y) { + private VisibleLayout? get_visible_layout_at(double x, double y) { + var point = Graphene.Point() { x = (float)x, y = (float)y }; foreach (var layout in _visible_text_layouts) { - if (layout.rect.contains_point(Graphene.Point() { x = (float)x, y = (float)y })) { + if (layout.rect.contains_point(point)) { return layout; } } @@ -209,16 +211,29 @@ private class TranscriptDrawingArea : Widget return null; } + private VisibleLayout? get_text_bubble_at(double x, double y) { + var layout = get_visible_layout_at(x, y); + if (layout != null && layout.bubble is TextBubbleLayout) { + return layout; + } + + return null; + } + private void on_mouse_motion(double x, double y) { - VisibleTextLayout? hovered_text_bubble = get_text_bubble_at(x, y); + VisibleLayout? hovered_text_bubble = get_text_bubble_at(x, y); on_text_bubble_hover(hovered_text_bubble); } - private void on_click(uint button) { + private void on_click(uint button, int n_press) { if (button == Gdk.BUTTON_SECONDARY) { on_right_click(); } else if (button == Gdk.BUTTON_PRIMARY) { on_left_click(); + + if (n_press == 2) { + on_double_click(); + } } } @@ -230,6 +245,17 @@ private class TranscriptDrawingArea : Widget } } + private void on_double_click() { + Gdk.Rectangle? bounding_box = null; + if (_click_gesture.get_bounding_box(out bounding_box)) { + var double_clicked_bubble = get_visible_layout_at(bounding_box.x, bounding_box.y); + if (double_clicked_bubble != null && double_clicked_bubble.bubble is ImageBubbleLayout) { + var image_bubble = double_clicked_bubble.bubble as ImageBubbleLayout; + on_image_bubble_activate(image_bubble.attachment_guid); + } + } + } + private void on_right_click() { var menu_model = new Menu(); menu_model.append("Copy", "transcript.copy"); @@ -346,6 +372,8 @@ private class TranscriptDrawingArea : Widget var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); image_layout.id = @"image-$(attachment.guid)"; + image_layout.attachment_guid = attachment.guid; + if (animate) { start_animation(image_layout.id); } @@ -410,12 +438,12 @@ private class ChatItemAnimation } } -public struct VisibleTextLayout { - public weak TextBubbleLayout text_bubble; +public struct VisibleLayout { + public weak BubbleLayout bubble; public Graphene.Rect rect; - public VisibleTextLayout(TextBubbleLayout text_bubble, Graphene.Rect rect) { - this.text_bubble = text_bubble; + public VisibleLayout(BubbleLayout bubble, Graphene.Rect rect) { + this.bubble = bubble; this.rect = rect; } } \ No newline at end of file diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index 3c7336c..cd431bc 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -51,8 +51,8 @@ public class TranscriptView : Adw.Bin private TextView _hovering_text_view = new TextView(); private bool _queued_url_open = false; - private VisibleTextLayout? hovered_text_bubble = null; - private VisibleTextLayout? locked_text_bubble = null; + private VisibleLayout? hovered_text_bubble = null; + private VisibleLayout? locked_text_bubble = null; public TranscriptView() { container = new Adw.ToolbarView(); @@ -123,6 +123,10 @@ public class TranscriptView : Adw.Bin } }); + transcript_drawing_area.on_image_bubble_activate.connect((attachment_guid) => { + self.open_attachment(attachment_guid); + }); + Repository.get_instance().attachment_downloaded.connect((attachment_guid) => { debug("Attachment downloaded: %s", attachment_guid); @@ -154,6 +158,40 @@ public class TranscriptView : Adw.Bin }); } + delegate void OpenPath(string path); + private ulong attachment_downloaded_handler_id = 0; + private void open_attachment(string attachment_guid) { + OpenPath open_path = (path) => { + try { + GLib.AppInfo.launch_default_for_uri("file://" + path, null); + } catch (GLib.Error e) { + warning("Failed to open image %s: %s", path, e.message); + } + }; + + try { + var attachment_info = Repository.get_instance().get_attachment_info(attachment_guid); + if (attachment_info.downloaded == true) { + // We already have it, so open it. + open_path(attachment_info.path); + } else { + // We need to download this, then open it once the downloaded signal is emitted. + Repository.get_instance().download_attachment(attachment_guid, false); + + // TODO: Should probably indicate progress here. + + attachment_downloaded_handler_id = Repository.get_instance().attachment_downloaded.connect((guid) => { + if (guid == attachment_guid) { + open_path(attachment_info.path); + Repository.get_instance().disconnect(attachment_downloaded_handler_id); + } + }); + } + } catch (GLib.Error e) { + warning("Failed to get attachment info: %s", e.message); + } + } + private void on_hovering_text_view_clicked(TextIter location, TextMark end) { lock_text_bubble(hovered_text_bubble); @@ -213,23 +251,24 @@ public class TranscriptView : Adw.Bin scrolled_window.vadjustment.page_size = 0; } - private void lock_text_bubble(VisibleTextLayout? visible_text_layout) { + private void lock_text_bubble(VisibleLayout? visible_text_layout) { if (visible_text_layout != null) { locked_text_bubble = visible_text_layout; configure_hovering_text_view(visible_text_layout); } } - private void configure_hovering_text_view(VisibleTextLayout? visible_text_layout) { + private void configure_hovering_text_view(VisibleLayout? visible_text_layout) { hovered_text_bubble = visible_text_layout; if (locked_text_bubble == null) { - _hovering_text_view_origin = visible_text_layout.text_bubble.get_text_origin(); + TextBubbleLayout text_bubble = visible_text_layout.bubble as TextBubbleLayout; + _hovering_text_view_origin = text_bubble.get_text_origin(); _hovering_text_view.buffer.text = ""; Gtk.TextIter start_iter; _hovering_text_view.buffer.get_start_iter(out start_iter); - _hovering_text_view.buffer.insert_markup(ref start_iter, visible_text_layout.text_bubble.message.markup, -1); + _hovering_text_view.buffer.insert_markup(ref start_iter, text_bubble.message.markup, -1); overlay.queue_allocate(); } }