diff --git a/src/meson.build b/src/meson.build index 9dcebb7..6a27d2d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -40,10 +40,9 @@ sources = [ 'transcript/layouts/sender-annotation-layout.vala', 'transcript/layouts/text-bubble-layout.vala', + 'models/attachment.vala', 'models/conversation.vala', 'models/message.vala', - - ] executable('kordophone', diff --git a/src/models/attachment.vala b/src/models/attachment.vala new file mode 100644 index 0000000..05357ac --- /dev/null +++ b/src/models/attachment.vala @@ -0,0 +1,74 @@ +public class AttributionInfo : Object { + // Picture width + public int64 width; + + // Picture height + public int64 height; + + public static AttributionInfo from_variant(Variant variant) { + var attribution_info = new AttributionInfo(); + + VariantDict dict = new VariantDict(variant); + attribution_info.width = dict.lookup_value("width", VariantType.INT32)?.get_int32() ?? 0; + attribution_info.height = dict.lookup_value("height", VariantType.INT32)?.get_int32() ?? 0; + + return attribution_info; + } +} + +public class AttachmentMetadata : Object { + public AttributionInfo? attribution_info = null; + + public static AttachmentMetadata from_variant(Variant variant) { + var metadata = new AttachmentMetadata(); + + VariantDict dict = new VariantDict(variant); + metadata.attribution_info = AttributionInfo.from_variant(dict.lookup_value("attribution_info", VariantType.DICTIONARY)); + + return metadata; + } +} + +public class Attachment : Object { + public string guid; + public string path; + public string preview_path; + public bool downloaded; + public bool preview_downloaded; + public AttachmentMetadata? metadata; + + public Attachment(string guid, AttachmentMetadata? metadata) { + this.guid = guid; + this.metadata = metadata; + } + + public Attachment.from_variant(Variant variant) { + VariantIter iter; + variant.get("a{sv}", out iter); + + string key; + Variant val; + while (iter.next("{sv}", out key, out val)) { + switch (key) { + case "guid": + this.guid = val.get_string(); + break; + case "path": + this.path = val.get_string(); + break; + case "preview_path": + this.preview_path = val.get_string(); + break; + case "downloaded": + this.downloaded = val.get_boolean(); + break; + case "preview_downloaded": + this.preview_downloaded = val.get_boolean(); + break; + case "metadata": + this.metadata = AttachmentMetadata.from_variant(val); + break; + } + } + } +} diff --git a/src/models/message.vala b/src/models/message.vala index e8ab948..8590e63 100644 --- a/src/models/message.vala +++ b/src/models/message.vala @@ -6,6 +6,8 @@ public class Message : Object public string text { get; set; default = ""; } public DateTime date { get; set; default = new DateTime.now_local(); } public string sender { get; set; default = null; } + + public Attachment[] attachments { get; set; default = {}; } public bool from_me { get { @@ -25,5 +27,18 @@ public class Message : Object text = message_data["text"].get_string(); sender = message_data["sender"].get_string(); date = new DateTime.from_unix_utc(message_data["date"].get_int64()); + + // Attachments + var attachments_variant = message_data["attachments"]; + var attachments = new Gee.ArrayList(); + if (attachments_variant != null) { + for (int i = 0; i < attachments_variant.n_children(); i++) { + var attachment_variant = attachments_variant.get_child_value(i); + var attachment = new Attachment.from_variant(attachment_variant); + attachments.add(attachment); + } + } + + this.attachments = attachments.to_array(); } } \ No newline at end of file diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index 16aceb1..0cb85b6 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -52,5 +52,24 @@ namespace DBusService { [DBus (name = "MessagesUpdated")] public signal void messages_updated(string conversation_id); + + [DBus (name = "GetAttachmentInfo")] + public abstract RepositoryAttachmentInfoStruct get_attachment_info(string attachment_id) throws DBusError, IOError; + + [DBus (name = "DownloadAttachment")] + public abstract void download_attachment(string attachment_id, bool preview) throws DBusError, IOError; + + [DBus (name = "AttachmentDownloadCompleted")] + public signal void attachment_download_completed(string attachment_id); + + [DBus (name = "AttachmentDownloadFailed")] + public signal void attachment_download_failed(string attachment_id, string error_message); + } + + public struct RepositoryAttachmentInfoStruct { + public string attr1; + public string attr2; + public bool attr3; + public bool attr4; } } diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index 8198155..2cf0e9b 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -4,7 +4,7 @@ - @@ -13,9 +13,9 @@ - + - - - - - - @@ -58,7 +58,24 @@ - + + + @@ -66,16 +83,56 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -90,6 +147,6 @@ - + diff --git a/src/service/repository.vala b/src/service/repository.vala index 421ca94..f52e938 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -4,6 +4,7 @@ using Gee; public class Repository : DBusServiceProxy { public signal void conversations_updated(); public signal void messages_updated(string conversation_guid); + public signal void attachment_downloaded(string attachment_guid); public static Repository get_instance() { if (instance == null) { @@ -36,6 +37,10 @@ public class Repository : DBusServiceProxy { messages_updated(conversation_guid); }); + this.dbus_repository.attachment_download_completed.connect((attachment_guid) => { + attachment_downloaded(attachment_guid); + }); + conversations_updated(); } catch (GLib.Error e) { warning("Failed to connect to repository: %s", e.message); @@ -87,4 +92,12 @@ public class Repository : DBusServiceProxy { dbus_repository.sync_conversation(conversation_guid); } + + public void download_attachment(string attachment_guid, bool preview) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + dbus_repository.download_attachment(attachment_guid, preview); + } } diff --git a/src/transcript/layouts/image-bubble-layout.vala b/src/transcript/layouts/image-bubble-layout.vala index 53cab58..2e25641 100644 --- a/src/transcript/layouts/image-bubble-layout.vala +++ b/src/transcript/layouts/image-bubble-layout.vala @@ -7,8 +7,6 @@ private class ImageBubbleLayout : BubbleLayout private Graphene.Size image_size; private Gdk.Texture? cached_texture = null; - private const float max_image_width = 300.0f; - private const float max_image_height = 400.0f; public ImageBubbleLayout(string image_path, bool from_me, Widget parent, float max_width, Graphene.Size? image_size = null) { base(parent, max_width); @@ -29,18 +27,13 @@ private class ImageBubbleLayout : BubbleLayout // Try to load the image to get its dimensions 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(); - // Calculate scaled dimensions while maintaining aspect ratio - var scale_factor = float.min( - max_image_width / original_width, - max_image_height / original_height - ); - scale_factor = float.min(scale_factor, 1.0f); // Don't scale up - - this.image_size = Graphene.Size() { width = original_width * scale_factor, height = original_height * scale_factor }; + this.image_size = Graphene.Size() { width = original_width, height = original_height }; } catch (Error e) { // Fallback dimensions if image can't be loaded warning("Failed to load image %s: %s", image_path, e.message); @@ -65,12 +58,8 @@ private class ImageBubbleLayout : BubbleLayout } public override float get_height() { - float aspect_ratio = image_size.width / image_size.height; - if (image_size.width > max_width) { - return max_width / aspect_ratio; - } - - return image_size.height; + var scale_factor = float.min(max_width / image_size.width, 1.0f); + return image_size.height * scale_factor; } public override float get_width() { diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 5aa393d..e7482c2 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -108,6 +108,30 @@ private class TranscriptDrawingArea : Widget text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; items.add(text_bubble); + // Check for attachments. For each one, add an image layout bubble + foreach (var attachment in message.attachments) { + if (attachment.metadata != null) { + var image_size = Graphene.Size() { + width = attachment.metadata.attribution_info.width, + height = attachment.metadata.attribution_info.height + }; + + var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); + 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 (!attachment.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; last_date = date; diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index 4078749..9d22574 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -44,6 +44,7 @@ public class TranscriptView : Adw.Bin private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea(); private ScrolledWindow scrolled_window = new ScrolledWindow(); private ulong messages_changed_handler_id = 0; + private bool needs_reload = false; public TranscriptView() { container = new Adw.ToolbarView(); @@ -56,6 +57,36 @@ public class TranscriptView : Adw.Bin var header_bar = new Adw.HeaderBar(); header_bar.set_title_widget(title_label); container.add_top_bar(header_bar); + + Repository.get_instance().attachment_downloaded.connect((attachment_guid) => { + debug("Attachment downloaded: %s", attachment_guid); + + // See if this attachment is part of this transcript. + bool contains_attachment = false; + foreach (var message in _model.messages) { + foreach (var attachment in message.attachments) { + if (attachment.guid == attachment_guid) { + contains_attachment = true; + break; + } + } + } + + if (contains_attachment && !needs_reload) { + debug("Queueing reload of messages for attachment download"); + + needs_reload = true; + GLib.Idle.add(() => { + if (needs_reload) { + debug("Reloading messages for attachment download"); + model.load_messages(); + needs_reload = false; + } + + return false; + }, GLib.Priority.HIGH); + } + }); } private void reload_messages() {