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);
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;
}
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<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
{
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,27 +70,26 @@ 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
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 };
}
private void queue_preview_download_if_needed() {
if (is_downloaded || preview_download_queued || attachment_guid == "") {
return;
}
try {
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);
}
}
private void load_image_if_needed() {
@@ -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();

View File

@@ -62,34 +62,36 @@ public class MessageListModel : Object, ListModel
}
}
public void load_messages() {
public void load_messages(bool force_full_reload = false) {
var previous_messages = new HashSet<Message>();
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);
Message[] messages = Repository.get_instance().get_messages(conversation.guid, last_message_id);
// Clear existing set
bool fallback_full_reload = first_load || force_full_reload;
if (!first_load && messages.length > 0 && previous_messages.contains(messages[0])) {
fallback_full_reload = true;
}
if (fallback_full_reload) {
uint old_count = _messages.size;
_messages.clear();
participants.clear();
// Notify of removal
if (old_count > 0) {
items_changed(0, old_count, 0);
}
// 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;
}
@@ -97,10 +99,29 @@ public class MessageListModel : Object, ListModel
position++;
}
// Notify of additions
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);
}

View File

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

View File

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