diff --git a/gtk/src/service/repository.vala b/gtk/src/service/repository.vala index 7931404..7db73ce 100644 --- a/gtk/src/service/repository.vala +++ b/gtk/src/service/repository.vala @@ -144,4 +144,35 @@ public class Repository : DBusServiceProxy { var info = dbus_repository.get_attachment_info(attachment_guid); return new AttachmentInfo(info.attr1, info.attr2, info.attr3, info.attr4); } + + public int open_attachment_fd(string attachment_guid, bool preview) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + var connection = Bus.get_sync(BusType.SESSION); + UnixFDList? out_fd_list = null; + var result = connection.call_with_unix_fd_list_sync( + DBUS_NAME, + DBUS_PATH, + "net.buzzert.kordophone.Repository", + "OpenAttachmentFd", + new Variant("(sb)", attachment_guid, preview), + new VariantType("(h)"), + DBusCallFlags.NONE, + 120000, + null, + out out_fd_list, + null + ); + + int fd_handle = -1; + result.get("(h)", out fd_handle); + + if (out_fd_list == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Missing UnixFDList from OpenAttachmentFd"); + } + + return out_fd_list.get(fd_handle); + } } diff --git a/gtk/src/transcript/layouts/image-bubble-layout.vala b/gtk/src/transcript/layouts/image-bubble-layout.vala index c957d86..2e4f235 100644 --- a/gtk/src/transcript/layouts/image-bubble-layout.vala +++ b/gtk/src/transcript/layouts/image-bubble-layout.vala @@ -13,30 +13,52 @@ private class SizeCache return instance; } - public Graphene.Size? get_size(string image_path) { - return size_cache.get(image_path); + public Graphene.Size? get_size(string attachment_guid) { + return size_cache.get(attachment_guid); } - public void set_size(string image_path, Graphene.Size size) { - size_cache.set(image_path, size); + public void set_size(string attachment_guid, Graphene.Size size) { + size_cache.set(attachment_guid, size); + } +} + +private class TextureCache +{ + private static TextureCache instance = null; + private HashMap texture_cache = new HashMap(); + + public static TextureCache get_instance() { + if (instance == null) { + instance = new TextureCache(); + } + return instance; + } + + public Gdk.Texture? get_texture(string attachment_guid) { + return texture_cache.get(attachment_guid); + } + + public void set_texture(string attachment_guid, Gdk.Texture texture) { + texture_cache.set(attachment_guid, texture); } } private class ImageBubbleLayout : BubbleLayout { - public string image_path; + public string attachment_guid; public bool is_downloaded; - public string? attachment_guid; private Graphene.Size image_size; private Gdk.Texture? cached_texture = null; + private bool preview_download_queued = false; - public ImageBubbleLayout(string image_path, bool from_me, Widget parent, float max_width, Graphene.Size? image_size = null) { + public ImageBubbleLayout(string attachment_guid, bool from_me, Widget parent, float max_width, Graphene.Size? image_size = null) { base(parent, max_width); this.from_me = from_me; - this.image_path = image_path; + this.attachment_guid = attachment_guid; this.is_downloaded = false; + this.cached_texture = TextureCache.get_instance().get_texture(attachment_guid); // Calculate image dimensions for layout calculate_image_dimensions(image_size); @@ -48,26 +70,25 @@ private class ImageBubbleLayout : BubbleLayout return; } - var cached_size = SizeCache.get_instance().get_size(image_path); + var cached_size = SizeCache.get_instance().get_size(attachment_guid); if (cached_size != null) { this.image_size = cached_size; return; } - // Try to load the image to get its dimensions + this.image_size = Graphene.Size() { width = 200.0f, height = 150.0f }; + } + + private void queue_preview_download_if_needed() { + if (is_downloaded || preview_download_queued || attachment_guid == "") { + return; + } + try { - warning("No image size provided, loading image to get dimensions"); - - var texture = Gdk.Texture.from_filename(image_path); - var original_width = (float)texture.get_width(); - var original_height = (float)texture.get_height(); - - this.image_size = Graphene.Size() { width = original_width, height = original_height }; - SizeCache.get_instance().set_size(image_path, this.image_size); - } catch (Error e) { - // Fallback dimensions if image can't be loaded - warning("Failed to load image %s: %s", image_path, e.message); - this.image_size = Graphene.Size() { width = 200.0f, height = 150.0f }; + Repository.get_instance().download_attachment(attachment_guid, true); + preview_download_queued = true; + } catch (GLib.Error e) { + warning("Failed to queue preview download for %s: %s", attachment_guid, e.message); } } @@ -81,9 +102,22 @@ private class ImageBubbleLayout : BubbleLayout } try { - cached_texture = Gdk.Texture.from_filename(image_path); + int fd = Repository.get_instance().open_attachment_fd(attachment_guid, true); + var stream = new UnixInputStream(fd, true); + var pixbuf = new Gdk.Pixbuf.from_stream(stream, null); + cached_texture = Gdk.Texture.for_pixbuf(pixbuf); + + if (cached_texture != null) { + TextureCache.get_instance().set_texture(attachment_guid, cached_texture); + this.image_size = Graphene.Size() { + width = (float)cached_texture.get_width(), + height = (float)cached_texture.get_height() + }; + SizeCache.get_instance().set_size(attachment_guid, this.image_size); + parent.queue_allocate(); + } } catch (Error e) { - warning("Failed to load image %s: %s", image_path, e.message); + warning("Failed to load preview image for %s: %s", attachment_guid, e.message); } } @@ -110,6 +144,7 @@ private class ImageBubbleLayout : BubbleLayout } public override void draw_content(Snapshot snapshot) { + queue_preview_download_if_needed(); load_image_if_needed(); snapshot.save(); @@ -137,4 +172,4 @@ private class ImageBubbleLayout : BubbleLayout public override void copy(Gdk.Clipboard clipboard) { clipboard.set_texture(cached_texture); } -} \ No newline at end of file +} diff --git a/gtk/src/transcript/message-list-model.vala b/gtk/src/transcript/message-list-model.vala index b4cf6ec..5f9dbae 100644 --- a/gtk/src/transcript/message-list-model.vala +++ b/gtk/src/transcript/message-list-model.vala @@ -62,44 +62,65 @@ public class MessageListModel : Object, ListModel } } - public void load_messages() { + public void load_messages(bool force_full_reload = false) { var previous_messages = new HashSet(); previous_messages.add_all(_messages); try { bool first_load = _messages.size == 0; + string last_message_id = (first_load || force_full_reload) ? "" : _messages.get(_messages.size - 1).guid; - Message[] messages = Repository.get_instance().get_messages(conversation.guid); - - // Clear existing set - uint old_count = _messages.size; - _messages.clear(); - participants.clear(); - - // Notify of removal - if (old_count > 0) { - items_changed(0, old_count, 0); + Message[] messages = Repository.get_instance().get_messages(conversation.guid, last_message_id); + + bool fallback_full_reload = first_load || force_full_reload; + if (!first_load && messages.length > 0 && previous_messages.contains(messages[0])) { + fallback_full_reload = true; } - - // Process each conversation - uint position = 0; - - for (int i = 0; i < messages.length; i++) { - var message = messages[i]; - participants.add(message.sender); - if (!first_load && !previous_messages.contains(message)) { - // This is a new message according to the UI, schedule an animation for it. - message.should_animate = true; + if (fallback_full_reload) { + uint old_count = _messages.size; + _messages.clear(); + participants.clear(); + + if (old_count > 0) { + items_changed(0, old_count, 0); } - _messages.add(message); - position++; - } - - // Notify of additions - if (position > 0) { - items_changed(0, 0, position); + uint position = 0; + for (int i = 0; i < messages.length; i++) { + var message = messages[i]; + participants.add(message.sender); + + if (!first_load && !previous_messages.contains(message)) { + message.should_animate = true; + } + + _messages.add(message); + position++; + } + + if (position > 0) { + items_changed(0, 0, position); + } + } else { + uint old_count = _messages.size; + uint appended = 0; + + for (int i = 0; i < messages.length; i++) { + var message = messages[i]; + if (previous_messages.contains(message)) { + continue; + } + + participants.add(message.sender); + message.should_animate = true; + _messages.add(message); + appended++; + } + + if (appended > 0) { + items_changed(old_count, 0, appended); + } } } catch (Error e) { warning("Failed to load messages: %s", e.message); @@ -134,4 +155,4 @@ public class MessageListModel : Object, ListModel public Object? get_item(uint position) { return _messages.get((int)position); } -} \ No newline at end of file +} diff --git a/gtk/src/transcript/transcript-drawing-area.vala b/gtk/src/transcript/transcript-drawing-area.vala index df05ba6..aac1a05 100644 --- a/gtk/src/transcript/transcript-drawing-area.vala +++ b/gtk/src/transcript/transcript-drawing-area.vala @@ -363,49 +363,23 @@ private class TranscriptDrawingArea : Widget // Check for attachments. For each one, add an image layout bubble foreach (var attachment in message.attachments) { - string preview_path = attachment.preview_path; - bool preview_downloaded = attachment.preview_downloaded; - - if (preview_path == null || preview_path == "") { - try { - var attachment_info = Repository.get_instance().get_attachment_info(attachment.guid); - if (attachment_info.preview_path != null) { - preview_path = attachment_info.preview_path; - } - preview_downloaded = attachment_info.preview_downloaded == true; - } catch (GLib.Error e) { - warning("Failed to load attachment info for %s: %s", attachment.guid, e.message); - } - } - Graphene.Size? image_size = null; - if (attachment.metadata != null) { + if (attachment.metadata != null && attachment.metadata.attribution_info != null) { image_size = Graphene.Size() { width = attachment.metadata.attribution_info.width, height = attachment.metadata.attribution_info.height }; } - var image_layout = new ImageBubbleLayout(preview_path, message.from_me, this, max_width, image_size); + var image_layout = new ImageBubbleLayout(attachment.guid, 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); } - image_layout.is_downloaded = preview_downloaded; + image_layout.is_downloaded = attachment.preview_downloaded; items.add(image_layout); - - // If the attachment isn't downloaded, queue a download since we are going to be showing it here. - // TODO: Probably would be better if we only did this for stuff in the viewport. - if (!preview_downloaded) { - try { - Repository.get_instance().download_attachment(attachment.guid, true); - } catch (GLib.Error e) { - warning("Wasn't able to message daemon about queuing attachment download: %s", e.message); - } - } } last_sender = message.sender; diff --git a/gtk/src/transcript/transcript-view.vala b/gtk/src/transcript/transcript-view.vala index cd431bc..306b4ac 100644 --- a/gtk/src/transcript/transcript-view.vala +++ b/gtk/src/transcript/transcript-view.vala @@ -148,7 +148,7 @@ public class TranscriptView : Adw.Bin GLib.Idle.add(() => { if (needs_reload) { debug("Reloading messages for attachment download"); - model.load_messages(); + model.load_messages(true); needs_reload = false; }