Private
Public Access
1
0

Implements attachments display in transcript

This commit is contained in:
2025-06-06 20:03:02 -07:00
parent 1a2dad08a5
commit 54790d1d70
9 changed files with 252 additions and 31 deletions

View File

@@ -40,10 +40,9 @@ sources = [
'transcript/layouts/sender-annotation-layout.vala', 'transcript/layouts/sender-annotation-layout.vala',
'transcript/layouts/text-bubble-layout.vala', 'transcript/layouts/text-bubble-layout.vala',
'models/attachment.vala',
'models/conversation.vala', 'models/conversation.vala',
'models/message.vala', 'models/message.vala',
] ]
executable('kordophone', executable('kordophone',

View File

@@ -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;
}
}
}
}

View File

@@ -6,6 +6,8 @@ public class Message : Object
public string text { get; set; default = ""; } public string text { get; set; default = ""; }
public DateTime date { get; set; default = new DateTime.now_local(); } public DateTime date { get; set; default = new DateTime.now_local(); }
public string sender { get; set; default = null; } public string sender { get; set; default = null; }
public Attachment[] attachments { get; set; default = {}; }
public bool from_me { public bool from_me {
get { get {
@@ -25,5 +27,18 @@ public class Message : Object
text = message_data["text"].get_string(); text = message_data["text"].get_string();
sender = message_data["sender"].get_string(); sender = message_data["sender"].get_string();
date = new DateTime.from_unix_utc(message_data["date"].get_int64()); date = new DateTime.from_unix_utc(message_data["date"].get_int64());
// Attachments
var attachments_variant = message_data["attachments"];
var attachments = new Gee.ArrayList<Attachment>();
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();
} }
} }

View File

@@ -52,5 +52,24 @@ namespace DBusService {
[DBus (name = "MessagesUpdated")] [DBus (name = "MessagesUpdated")]
public signal void messages_updated(string conversation_id); 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;
} }
} }

View File

@@ -4,7 +4,7 @@
<interface name="net.buzzert.kordophone.Repository"> <interface name="net.buzzert.kordophone.Repository">
<method name="GetVersion"> <method name="GetVersion">
<arg type="s" name="version" direction="out" /> <arg type="s" name="version" direction="out" />
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Returns the version of the client daemon."/> value="Returns the version of the client daemon."/>
</method> </method>
@@ -13,9 +13,9 @@
<method name="GetConversations"> <method name="GetConversations">
<arg type="i" name="limit" direction="in"/> <arg type="i" name="limit" direction="in"/>
<arg type="i" name="offset" direction="in"/> <arg type="i" name="offset" direction="in"/>
<arg type="aa{sv}" direction="out" name="conversations"> <arg type="aa{sv}" direction="out" name="conversations">
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Array of dictionaries. Each dictionary has keys: value="Array of dictionaries. Each dictionary has keys:
'id' (string): Unique identifier 'id' (string): Unique identifier
'display_name' (string): Display name 'display_name' (string): Display name
@@ -28,28 +28,28 @@
</method> </method>
<method name="SyncConversationList"> <method name="SyncConversationList">
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Initiates a background sync of the conversation list with the server."/> value="Initiates a background sync of the conversation list with the server."/>
</method> </method>
<method name="SyncAllConversations"> <method name="SyncAllConversations">
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Initiates a background sync of all conversations with the server."/> value="Initiates a background sync of all conversations with the server."/>
</method> </method>
<method name="SyncConversation"> <method name="SyncConversation">
<arg type="s" name="conversation_id" direction="in"/> <arg type="s" name="conversation_id" direction="in"/>
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Initiates a background sync of a single conversation with the server."/> value="Initiates a background sync of a single conversation with the server."/>
</method> </method>
<signal name="ConversationsUpdated"> <signal name="ConversationsUpdated">
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Emitted when the list of conversations is updated."/> value="Emitted when the list of conversations is updated."/>
</signal> </signal>
<method name="DeleteAllConversations"> <method name="DeleteAllConversations">
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Deletes all conversations from the database."/> value="Deletes all conversations from the database."/>
</method> </method>
@@ -58,7 +58,24 @@
<method name="GetMessages"> <method name="GetMessages">
<arg type="s" name="conversation_id" direction="in"/> <arg type="s" name="conversation_id" direction="in"/>
<arg type="s" name="last_message_id" direction="in"/> <arg type="s" name="last_message_id" direction="in"/>
<arg type="aa{sv}" direction="out" name="messages"/> <arg type="aa{sv}" direction="out" name="messages">
<annotation name="org.freedesktop.DBus.DocString"
value="Array of dictionaries. Each dictionary has keys:
'id' (string): Unique message identifier
'text' (string): Message body text
'date' (int64): Message timestamp
'sender' (string): Sender display name
'attachments' (array of dictionaries): List of attachments
'guid' (string): Attachment GUID
'path' (string): Attachment path
'preview_path' (string): Preview attachment path
'downloaded' (boolean): Whether the attachment is downloaded
'preview_downloaded' (boolean): Whether the preview is downloaded
'metadata' (dictionary, optional): Attachment metadata
'attribution_info' (dictionary, optional): Attribution info
'width' (int32, optional): Width
'height' (int32, optional): Height"/>
</arg>
</method> </method>
<method name="SendMessage"> <method name="SendMessage">
@@ -66,16 +83,56 @@
<arg type="s" name="text" direction="in"/> <arg type="s" name="text" direction="in"/>
<arg type="s" name="outgoing_message_id" direction="out"/> <arg type="s" name="outgoing_message_id" direction="out"/>
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Sends a message to the server. Returns the outgoing message ID."/> value="Sends a message to the server. Returns the outgoing message ID."/>
</method> </method>
<signal name="MessagesUpdated"> <signal name="MessagesUpdated">
<arg type="s" name="conversation_id" direction="in"/> <arg type="s" name="conversation_id" direction="in"/>
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Emitted when the list of messages is updated."/> value="Emitted when the list of messages is updated."/>
</signal> </signal>
<!-- Attachments -->
<method name="GetAttachmentInfo">
<arg type="s" name="attachment_id" direction="in"/>
<arg type="(ssbb)" name="attachment_info" direction="out"/>
<annotation name="org.freedesktop.DBus.DocString"
value="Returns attachment info:
- path: string
- preview_path: string
- downloaded: boolean
- preview_downloaded: boolean
"/>
</method>
<method name="DownloadAttachment">
<arg type="s" name="attachment_id" direction="in"/>
<arg type="b" name="preview" direction="in"/>
<annotation name="org.freedesktop.DBus.DocString"
value="Initiates download of the specified attachment if not already downloaded.
Arguments:
attachment_id: the attachment GUID
preview: whether to download the preview (true) or full attachment (false)
"/>
</method>
<signal name="AttachmentDownloadCompleted">
<arg type="s" name="attachment_id"/>
<annotation name="org.freedesktop.DBus.DocString"
value="Emitted when an attachment download completes successfully."/>
</signal>
<signal name="AttachmentDownloadFailed">
<arg type="s" name="attachment_id"/>
<arg type="s" name="error_message"/>
<annotation name="org.freedesktop.DBus.DocString"
value="Emitted when an attachment download fails."/>
</signal>
</interface> </interface>
<interface name="net.buzzert.kordophone.Settings"> <interface name="net.buzzert.kordophone.Settings">
@@ -90,6 +147,6 @@
</method> </method>
<!-- emitted when anything changes --> <!-- emitted when anything changes -->
<signal name="ConfigChanged"/> <signal name="ConfigChanged"/>
</interface> </interface>
</node> </node>

View File

@@ -4,6 +4,7 @@ using Gee;
public class Repository : DBusServiceProxy { public class Repository : DBusServiceProxy {
public signal void conversations_updated(); public signal void conversations_updated();
public signal void messages_updated(string conversation_guid); public signal void messages_updated(string conversation_guid);
public signal void attachment_downloaded(string attachment_guid);
public static Repository get_instance() { public static Repository get_instance() {
if (instance == null) { if (instance == null) {
@@ -36,6 +37,10 @@ public class Repository : DBusServiceProxy {
messages_updated(conversation_guid); messages_updated(conversation_guid);
}); });
this.dbus_repository.attachment_download_completed.connect((attachment_guid) => {
attachment_downloaded(attachment_guid);
});
conversations_updated(); conversations_updated();
} catch (GLib.Error e) { } catch (GLib.Error e) {
warning("Failed to connect to repository: %s", e.message); warning("Failed to connect to repository: %s", e.message);
@@ -87,4 +92,12 @@ public class Repository : DBusServiceProxy {
dbus_repository.sync_conversation(conversation_guid); 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);
}
} }

View File

@@ -7,8 +7,6 @@ private class ImageBubbleLayout : BubbleLayout
private Graphene.Size image_size; private Graphene.Size image_size;
private Gdk.Texture? cached_texture = null; 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) { public ImageBubbleLayout(string image_path, bool from_me, Widget parent, float max_width, Graphene.Size? image_size = null) {
base(parent, max_width); base(parent, max_width);
@@ -29,18 +27,13 @@ private class ImageBubbleLayout : BubbleLayout
// Try to load the image to get its dimensions // Try to load the image to get its dimensions
try { try {
warning("No image size provided, loading image to get dimensions");
var texture = Gdk.Texture.from_filename(image_path); var texture = Gdk.Texture.from_filename(image_path);
var original_width = (float)texture.get_width(); var original_width = (float)texture.get_width();
var original_height = (float)texture.get_height(); var original_height = (float)texture.get_height();
// Calculate scaled dimensions while maintaining aspect ratio this.image_size = Graphene.Size() { width = original_width, height = original_height };
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 };
} catch (Error e) { } catch (Error e) {
// Fallback dimensions if image can't be loaded // Fallback dimensions if image can't be loaded
warning("Failed to load image %s: %s", image_path, e.message); warning("Failed to load image %s: %s", image_path, e.message);
@@ -65,12 +58,8 @@ private class ImageBubbleLayout : BubbleLayout
} }
public override float get_height() { public override float get_height() {
float aspect_ratio = image_size.width / image_size.height; var scale_factor = float.min(max_width / image_size.width, 1.0f);
if (image_size.width > max_width) { return image_size.height * scale_factor;
return max_width / aspect_ratio;
}
return image_size.height;
} }
public override float get_width() { public override float get_width() {

View File

@@ -108,6 +108,30 @@ private class TranscriptDrawingArea : Widget
text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f;
items.add(text_bubble); 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_sender = message.sender;
last_date = date; last_date = date;

View File

@@ -44,6 +44,7 @@ public class TranscriptView : Adw.Bin
private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea(); private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea();
private ScrolledWindow scrolled_window = new ScrolledWindow(); private ScrolledWindow scrolled_window = new ScrolledWindow();
private ulong messages_changed_handler_id = 0; private ulong messages_changed_handler_id = 0;
private bool needs_reload = false;
public TranscriptView() { public TranscriptView() {
container = new Adw.ToolbarView(); container = new Adw.ToolbarView();
@@ -56,6 +57,36 @@ public class TranscriptView : Adw.Bin
var header_bar = new Adw.HeaderBar(); var header_bar = new Adw.HeaderBar();
header_bar.set_title_widget(title_label); header_bar.set_title_widget(title_label);
container.add_top_bar(header_bar); 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() { private void reload_messages() {