Private
Public Access
1
0

gtk: implement get_attachment_fd and texture/attachment cache, viewport loading

This commit is contained in:
2026-02-21 23:28:21 -08:00
parent f0ec6b8cb4
commit 7cf2724a75
5 changed files with 145 additions and 84 deletions

View File

@@ -144,4 +144,35 @@ public class Repository : DBusServiceProxy {
var info = dbus_repository.get_attachment_info(attachment_guid); var info = dbus_repository.get_attachment_info(attachment_guid);
return new AttachmentInfo(info.attr1, info.attr2, info.attr3, info.attr4); 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);
}
} }

View File

@@ -13,30 +13,52 @@ private class SizeCache
return instance; return instance;
} }
public Graphene.Size? get_size(string image_path) { public Graphene.Size? get_size(string attachment_guid) {
return size_cache.get(image_path); return size_cache.get(attachment_guid);
} }
public void set_size(string image_path, Graphene.Size size) { public void set_size(string attachment_guid, Graphene.Size size) {
size_cache.set(image_path, size); size_cache.set(attachment_guid, size);
}
}
private class TextureCache
{
private static TextureCache instance = null;
private HashMap<string, Gdk.Texture> texture_cache = new HashMap<string, Gdk.Texture>();
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 private class ImageBubbleLayout : BubbleLayout
{ {
public string image_path; public string attachment_guid;
public bool is_downloaded; public bool is_downloaded;
public string? attachment_guid;
private Graphene.Size image_size; private Graphene.Size image_size;
private Gdk.Texture? cached_texture = null; 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); base(parent, max_width);
this.from_me = from_me; this.from_me = from_me;
this.image_path = image_path; this.attachment_guid = attachment_guid;
this.is_downloaded = false; this.is_downloaded = false;
this.cached_texture = TextureCache.get_instance().get_texture(attachment_guid);
// Calculate image dimensions for layout // Calculate image dimensions for layout
calculate_image_dimensions(image_size); calculate_image_dimensions(image_size);
@@ -48,26 +70,25 @@ private class ImageBubbleLayout : BubbleLayout
return; 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) { if (cached_size != null) {
this.image_size = cached_size; this.image_size = cached_size;
return; 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 { try {
warning("No image size provided, loading image to get dimensions"); Repository.get_instance().download_attachment(attachment_guid, true);
preview_download_queued = true;
var texture = Gdk.Texture.from_filename(image_path); } catch (GLib.Error e) {
var original_width = (float)texture.get_width(); warning("Failed to queue preview download for %s: %s", attachment_guid, e.message);
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 };
} }
} }
@@ -81,9 +102,22 @@ private class ImageBubbleLayout : BubbleLayout
} }
try { 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) { } 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) { public override void draw_content(Snapshot snapshot) {
queue_preview_download_if_needed();
load_image_if_needed(); load_image_if_needed();
snapshot.save(); snapshot.save();

View File

@@ -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<Message>(); var previous_messages = new HashSet<Message>();
previous_messages.add_all(_messages); previous_messages.add_all(_messages);
try { try {
bool first_load = _messages.size == 0; 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); Message[] messages = Repository.get_instance().get_messages(conversation.guid, last_message_id);
// Clear existing set bool fallback_full_reload = first_load || force_full_reload;
uint old_count = _messages.size; if (!first_load && messages.length > 0 && previous_messages.contains(messages[0])) {
_messages.clear(); fallback_full_reload = true;
participants.clear();
// Notify of removal
if (old_count > 0) {
items_changed(0, old_count, 0);
} }
// Process each conversation if (fallback_full_reload) {
uint position = 0; uint old_count = _messages.size;
_messages.clear();
participants.clear();
for (int i = 0; i < messages.length; i++) { if (old_count > 0) {
var message = messages[i]; items_changed(0, old_count, 0);
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;
} }
_messages.add(message); uint position = 0;
position++; for (int i = 0; i < messages.length; i++) {
} var message = messages[i];
participants.add(message.sender);
// Notify of additions if (!first_load && !previous_messages.contains(message)) {
if (position > 0) { message.should_animate = true;
items_changed(0, 0, position); }
_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) { } catch (Error e) {
warning("Failed to load messages: %s", e.message); warning("Failed to load messages: %s", e.message);

View File

@@ -363,49 +363,23 @@ private class TranscriptDrawingArea : Widget
// Check for attachments. For each one, add an image layout bubble // Check for attachments. For each one, add an image layout bubble
foreach (var attachment in message.attachments) { 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; Graphene.Size? image_size = null;
if (attachment.metadata != null) { if (attachment.metadata != null && attachment.metadata.attribution_info != null) {
image_size = Graphene.Size() { image_size = Graphene.Size() {
width = attachment.metadata.attribution_info.width, width = attachment.metadata.attribution_info.width,
height = attachment.metadata.attribution_info.height 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.id = @"image-$(attachment.guid)";
image_layout.attachment_guid = attachment.guid;
if (animate) { if (animate) {
start_animation(image_layout.id); start_animation(image_layout.id);
} }
image_layout.is_downloaded = preview_downloaded; image_layout.is_downloaded = attachment.preview_downloaded;
items.add(image_layout); 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; last_sender = message.sender;

View File

@@ -148,7 +148,7 @@ public class TranscriptView : Adw.Bin
GLib.Idle.add(() => { GLib.Idle.add(() => {
if (needs_reload) { if (needs_reload) {
debug("Reloading messages for attachment download"); debug("Reloading messages for attachment download");
model.load_messages(); model.load_messages(true);
needs_reload = false; needs_reload = false;
} }