gtk: implement get_attachment_fd and texture/attachment cache, viewport loading
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,27 +70,26 @@ 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
|
|
||||||
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 };
|
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() {
|
private void load_image_if_needed() {
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>();
|
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;
|
||||||
|
if (!first_load && messages.length > 0 && previous_messages.contains(messages[0])) {
|
||||||
|
fallback_full_reload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallback_full_reload) {
|
||||||
uint old_count = _messages.size;
|
uint old_count = _messages.size;
|
||||||
_messages.clear();
|
_messages.clear();
|
||||||
participants.clear();
|
participants.clear();
|
||||||
|
|
||||||
// Notify of removal
|
|
||||||
if (old_count > 0) {
|
if (old_count > 0) {
|
||||||
items_changed(0, old_count, 0);
|
items_changed(0, old_count, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each conversation
|
|
||||||
uint position = 0;
|
uint position = 0;
|
||||||
|
|
||||||
for (int i = 0; i < messages.length; i++) {
|
for (int i = 0; i < messages.length; i++) {
|
||||||
var message = messages[i];
|
var message = messages[i];
|
||||||
participants.add(message.sender);
|
participants.add(message.sender);
|
||||||
|
|
||||||
if (!first_load && !previous_messages.contains(message)) {
|
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;
|
message.should_animate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +99,29 @@ public class MessageListModel : Object, ListModel
|
|||||||
position++;
|
position++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify of additions
|
|
||||||
if (position > 0) {
|
if (position > 0) {
|
||||||
items_changed(0, 0, position);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user