commit a001a54216f37fd789420ae64605f57e26635c83
parent e0b8f28cf38b9a73fa151f59479fe6bf98cb6f65
Author: Jacki <jacki@thejackimonster.de>
Date: Sat, 23 Mar 2024 20:51:22 +0100
Add media previews to chat details
Signed-off-by: Jacki <jacki@thejackimonster.de>
Diffstat:
8 files changed, 457 insertions(+), 9 deletions(-)
diff --git a/resources/ui.gresource.xml b/resources/ui.gresource.xml
@@ -12,6 +12,7 @@
<file compressed="true">ui/file_entry.ui</file>
<file compressed="true">ui/file_load_entry.ui</file>
<file compressed="true">ui/invite_contact.ui</file>
+ <file compressed="true">ui/media_preview.ui</file>
<file compressed="true">ui/message_content.ui</file>
<file compressed="true">ui/message.ui</file>
<file compressed="true">ui/message-sent.ui</file>
diff --git a/resources/ui/media_preview.ui b/resources/ui/media_preview.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.40.0
+
+Copyright (C) 2024 GNUnet e.V.
+
+GNUnet is free software: you can redistribute it and/or modify it
+under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, either version 3 of the License,
+or (at your option) any later version.
+
+GNUnet is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+SPDX-License-Identifier: AGPL3.0-or-later
+Author: Tobias Frisch
+
+-->
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <object class="GtkBox" id="media_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkDrawingArea" id="preview_drawing_area">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+</interface>
diff --git a/src/ui/chat.c b/src/ui/chat.c
@@ -33,6 +33,7 @@
#include "chat_entry.h"
#include "file_entry.h"
#include "file_load_entry.h"
+#include "media_preview.h"
#include "message.h"
#include "messenger.h"
#include "picker.h"
@@ -1857,7 +1858,7 @@ ui_chat_new(MESSENGER_Application *app)
struct IterateChatClosure {
MESSENGER_Application *app;
- GtkListBox *listbox;
+ GtkContainer *container;
};
static enum GNUNET_GenericReturnValue
@@ -1869,7 +1870,7 @@ iterate_ui_chat_update_group_contacts(void *cls,
(struct IterateChatClosure*) cls
);
- GtkListBox *listbox = closure->listbox;
+ GtkListBox *listbox = GTK_LIST_BOX(closure->container);
UI_ACCOUNT_ENTRY_Handle* entry = ui_account_entry_new(closure->app);
ui_account_entry_set_contact(entry, contact);
@@ -1920,7 +1921,7 @@ _chat_update_contacts(UI_CHAT_Handle *handle,
{
struct IterateChatClosure closure;
closure.app = app;
- closure.listbox = handle->chat_contacts_listbox;
+ closure.container = GTK_CONTAINER(handle->chat_contacts_listbox);
GNUNET_CHAT_group_iterate_contacts(
group,
@@ -1944,7 +1945,7 @@ iterate_ui_chat_update_context_files(void *cls,
(struct IterateChatClosure*) cls
);
- GtkListBox *listbox = closure->listbox;
+ GtkListBox *listbox = GTK_LIST_BOX(closure->container);
UI_FILE_ENTRY_Handle* entry = ui_file_entry_new(closure->app);
ui_file_entry_update(entry, file);
@@ -1992,7 +1993,7 @@ _chat_update_files(UI_CHAT_Handle *handle,
struct IterateChatClosure closure;
closure.app = app;
- closure.listbox = handle->chat_files_listbox;
+ closure.container = GTK_CONTAINER(handle->chat_files_listbox);
const int count = GNUNET_CHAT_context_iterate_files(
context,
@@ -2006,6 +2007,86 @@ _chat_update_files(UI_CHAT_Handle *handle,
);
}
+static enum GNUNET_GenericReturnValue
+iterate_ui_chat_update_context_media(void *cls,
+ struct GNUNET_CHAT_Context *context,
+ struct GNUNET_CHAT_File *file)
+{
+ struct IterateChatClosure *closure = (
+ (struct IterateChatClosure*) cls
+ );
+
+ GtkFlowBox *flowbox = GTK_FLOW_BOX(closure->container);
+ UI_MEDIA_PREVIEW_Handle* handle = ui_media_preview_new(closure->app);
+ ui_media_preview_update(handle, file);
+
+ if ((! handle->preview_animation) && (! handle->preview_image))
+ {
+ ui_media_preview_delete(handle);
+ return GNUNET_YES;
+ }
+
+ gtk_flow_box_insert(flowbox, handle->media_box, -1);
+
+ GtkFlowBoxChild *child = GTK_FLOW_BOX_CHILD(
+ gtk_widget_get_parent(handle->media_box)
+ );
+
+ g_object_set_qdata(G_OBJECT(child), closure->app->quarks.data, file);
+ g_object_set_qdata_full(
+ G_OBJECT(child),
+ closure->app->quarks.ui,
+ handle,
+ (GDestroyNotify) ui_media_preview_delete
+ );
+
+ gtk_widget_set_size_request(GTK_WIDGET(child), 80, 80);
+
+ gtk_widget_show_all(GTK_WIDGET(child));
+ return GNUNET_YES;
+}
+
+static void
+_chat_update_media(UI_CHAT_Handle *handle,
+ MESSENGER_Application *app,
+ struct GNUNET_CHAT_Context *context)
+{
+ g_assert((handle) && (app) && (context));
+
+ GList* children = gtk_container_get_children(
+ GTK_CONTAINER(handle->chat_media_flowbox)
+ );
+
+ GList *item = children;
+ while (item) {
+ GtkWidget *widget = GTK_WIDGET(item->data);
+ item = item->next;
+
+ gtk_container_remove(
+ GTK_CONTAINER(handle->chat_media_flowbox),
+ widget
+ );
+ }
+
+ if (children)
+ g_list_free(children);
+
+ struct IterateChatClosure closure;
+ closure.app = app;
+ closure.container = GTK_CONTAINER(handle->chat_media_flowbox);
+
+ const int count = GNUNET_CHAT_context_iterate_files(
+ context,
+ iterate_ui_chat_update_context_media,
+ &closure
+ );
+
+ gtk_widget_set_visible(
+ GTK_WIDGET(handle->chat_details_media_box),
+ count? TRUE : FALSE
+ );
+}
+
void
ui_chat_update(UI_CHAT_Handle *handle,
MESSENGER_Application *app,
@@ -2061,6 +2142,7 @@ ui_chat_update(UI_CHAT_Handle *handle,
_chat_update_contacts(handle, app, group);
_chat_update_files(handle, app, context);
+ _chat_update_media(handle, app, context);
g_object_set_qdata(
G_OBJECT(handle->reveal_identity_button),
diff --git a/src/ui/file_entry.h b/src/ui/file_entry.h
@@ -29,8 +29,6 @@
#include "messenger.h"
-typedef struct UI_CHAT_Handle UI_CHAT_Handle;
-
typedef struct UI_FILE_ENTRY_Handle
{
GtkBuilder *builder;
diff --git a/src/ui/media_preview.c b/src/ui/media_preview.c
@@ -0,0 +1,247 @@
+/*
+ This file is part of GNUnet.
+ Copyright (C) 2024 GNUnet e.V.
+
+ GNUnet is free software: you can redistribute it and/or modify it
+ under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License,
+ or (at your option) any later version.
+
+ GNUnet is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+ */
+/*
+ * @author Tobias Frisch
+ * @file ui/media_preview.c
+ */
+
+#include "media_preview.h"
+
+#include "../application.h"
+
+static int
+handle_media_preview_redraw_animation(gpointer user_data)
+{
+ g_assert(user_data);
+
+ UI_MEDIA_PREVIEW_Handle *handle = (UI_MEDIA_PREVIEW_Handle*) user_data;
+
+ handle->redraw_animation = 0;
+
+ if ((handle->preview_drawing_area) &&
+ ((handle->preview_image) ||
+ (handle->preview_animation) ||
+ (handle->preview_animation_iter)))
+ gtk_widget_queue_draw(GTK_WIDGET(handle->preview_drawing_area));
+
+ return FALSE;
+}
+
+static gboolean
+handle_preview_drawing_area_draw(GtkWidget* drawing_area,
+ cairo_t* cairo,
+ gpointer user_data)
+{
+ g_assert((drawing_area) && (cairo) && (user_data));
+
+ UI_MEDIA_PREVIEW_Handle *handle = (UI_MEDIA_PREVIEW_Handle*) user_data;
+
+ GtkStyleContext* context = gtk_widget_get_style_context(drawing_area);
+
+ const guint width = gtk_widget_get_allocated_width(drawing_area);
+ const guint height = gtk_widget_get_allocated_height(drawing_area);
+
+ gtk_render_background(context, cairo, 0, 0, width, height);
+
+ GdkPixbuf *image = handle->preview_image;
+
+ if (!(handle->preview_animation))
+ goto render_image;
+
+ if (handle->preview_animation_iter)
+ gdk_pixbuf_animation_iter_advance(handle->preview_animation_iter, NULL);
+ else
+ handle->preview_animation_iter = gdk_pixbuf_animation_get_iter(
+ handle->preview_animation, NULL
+ );
+
+ image = gdk_pixbuf_animation_iter_get_pixbuf(handle->preview_animation_iter);
+
+ const int delay = gdk_pixbuf_animation_iter_get_delay_time(
+ handle->preview_animation_iter
+ );
+
+ handle->redraw_animation = g_timeout_add(
+ delay, handle_media_preview_redraw_animation, handle
+ );
+
+render_image:
+ if (!image)
+ return FALSE;
+
+ int swidth = gdk_pixbuf_get_width(image);
+ int sheight = gdk_pixbuf_get_height(image);
+
+ int sx = 0;
+ int sy = 0;
+
+ if (swidth > sheight)
+ {
+ sx = swidth - sheight;
+ swidth -= sx;
+ sx /= 2;
+ }
+ else
+ {
+ sy = sheight - swidth;
+ sheight -= sy;
+ sy /= 2;
+ }
+
+ double ratio_width = 1.0 * width / swidth;
+ double ratio_height = 1.0 * height / sheight;
+
+ const double ratio = ratio_width < ratio_height? ratio_width : ratio_height;
+
+ const int interp_type = (ratio >= 1.0?
+ GDK_INTERP_NEAREST :
+ GDK_INTERP_BILINEAR
+ );
+
+ GdkPixbuf* subimage = gdk_pixbuf_new_subpixbuf(
+ image,
+ sx,
+ sy,
+ swidth,
+ sheight
+ );
+
+ GdkPixbuf* scaled = gdk_pixbuf_scale_simple(
+ subimage,
+ width,
+ height,
+ interp_type
+ );
+
+ g_object_unref(subimage);
+ gtk_render_icon(context, cairo, scaled, 0, 0);
+
+ cairo_fill(cairo);
+ g_object_unref(scaled);
+
+ if (handle->preview_image)
+ {
+ g_object_unref(handle->preview_image);
+ handle->preview_image = NULL;
+ }
+
+ return FALSE;
+}
+
+static void
+_clear_message_preview_data(UI_MEDIA_PREVIEW_Handle *handle)
+{
+ g_assert(handle);
+
+ if (handle->preview_image)
+ {
+ g_object_unref(handle->preview_image);
+ handle->preview_image = NULL;
+ }
+
+ if (handle->redraw_animation)
+ {
+ g_source_remove(handle->redraw_animation);
+ handle->redraw_animation = 0;
+ }
+
+ if (handle->preview_animation_iter)
+ {
+ g_object_unref(handle->preview_animation_iter);
+ handle->preview_animation_iter = NULL;
+ }
+
+ if (handle->preview_animation)
+ {
+ g_object_unref(handle->preview_animation);
+ handle->preview_animation = NULL;
+ }
+}
+
+UI_MEDIA_PREVIEW_Handle*
+ui_media_preview_new(MESSENGER_Application *app)
+{
+ g_assert(app);
+
+ UI_MEDIA_PREVIEW_Handle* handle = g_malloc(sizeof(UI_MEDIA_PREVIEW_Handle));
+
+ handle->builder = gtk_builder_new_from_resource(
+ application_get_resource_path(app, "ui/media_preview.ui")
+ );
+
+ handle->media_box = GTK_WIDGET(
+ gtk_builder_get_object(handle->builder, "media_box")
+ );
+
+ handle->preview_drawing_area = GTK_DRAWING_AREA(
+ gtk_builder_get_object(handle->builder, "preview_drawing_area")
+ );
+
+ g_signal_connect(
+ handle->preview_drawing_area,
+ "draw",
+ G_CALLBACK(handle_preview_drawing_area_draw),
+ handle
+ );
+
+ handle->preview_image = NULL;
+ handle->preview_animation = NULL;
+ handle->preview_animation_iter = NULL;
+
+ handle->redraw_animation = 0;
+
+ return handle;
+}
+
+void
+ui_media_preview_update(UI_MEDIA_PREVIEW_Handle *handle,
+ struct GNUNET_CHAT_File *file)
+{
+ g_assert((handle) && (file));
+
+ const char *preview = GNUNET_CHAT_file_open_preview(file);
+
+ if (!preview)
+ return;
+
+ handle->preview_animation = gdk_pixbuf_animation_new_from_file(
+ preview, NULL
+ );
+
+ if (!(handle->preview_animation))
+ handle->preview_image = gdk_pixbuf_new_from_file(preview, NULL);
+
+ GNUNET_CHAT_file_close_preview(file);
+
+ if ((handle->preview_animation) || (handle->preview_image))
+ gtk_widget_queue_draw(GTK_WIDGET(handle->preview_drawing_area));
+}
+
+void
+ui_media_preview_delete(UI_MEDIA_PREVIEW_Handle *handle)
+{
+ g_assert(handle);
+
+ _clear_message_preview_data(handle);
+
+ g_object_unref(handle->builder);
+
+ g_free(handle);
+}
diff --git a/src/ui/media_preview.h b/src/ui/media_preview.h
@@ -0,0 +1,78 @@
+/*
+ This file is part of GNUnet.
+ Copyright (C) 2024 GNUnet e.V.
+
+ GNUnet is free software: you can redistribute it and/or modify it
+ under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License,
+ or (at your option) any later version.
+
+ GNUnet is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+ */
+/*
+ * @author Tobias Frisch
+ * @file ui/media_preview.h
+ */
+
+#ifndef UI_MEDIA_PREVIEW_H_
+#define UI_MEDIA_PREVIEW_H_
+
+#include <gnunet/gnunet_chat_lib.h>
+
+#include "messenger.h"
+
+typedef struct UI_MEDIA_PREVIEW_Handle
+{
+ GtkBuilder *builder;
+
+ GtkWidget *media_box;
+
+ GtkDrawingArea *preview_drawing_area;
+
+ GdkPixbuf *preview_image;
+ GdkPixbufAnimation *preview_animation;
+ GdkPixbufAnimationIter *preview_animation_iter;
+
+ guint redraw_animation;
+} UI_MEDIA_PREVIEW_Handle;
+
+/**
+ * Allocates and creates a new media preview handle
+ * to manage loading files for a given messenger
+ * application.
+ *
+ * @param app Messenger application
+ * @return New media preview handle
+ */
+UI_MEDIA_PREVIEW_Handle*
+ui_media_preview_new(MESSENGER_Application *app);
+
+/**
+ * Updates a media preview handle with a selected
+ * file to represent it visually.
+ *
+ * @param handle Media preview handle
+ * @param file Chat file
+ */
+void
+ui_media_preview_update(UI_MEDIA_PREVIEW_Handle *handle,
+ struct GNUNET_CHAT_File *file);
+
+/**
+ * Frees its resources and destroys a given media
+ * preview handle.
+ *
+ * @param handle Media preview handle
+ */
+void
+ui_media_preview_delete(UI_MEDIA_PREVIEW_Handle *handle);
+
+#endif /* UI_MEDIA_PREVIEW_H_ */
diff --git a/src/ui/meson.build b/src/ui/meson.build
@@ -31,6 +31,7 @@ messenger_gtk_ui_sources = files([
'file_entry.c', 'file_entry.h',
'file_load_entry.c', 'file_load_entry.h',
'invite_contact.c', 'invite_contact.h',
+ 'media_preview.c', 'media_preview.h',
'message.c', 'message.h',
'messenger.c', 'messenger.h',
'new_account.c', 'new_account.h',
diff --git a/src/ui/message.c b/src/ui/message.c
@@ -634,6 +634,8 @@ _update_file_message(UI_MESSAGE_Handle *handle,
if (!(handle->preview_animation))
handle->preview_image = gdk_pixbuf_new_from_file(preview, NULL);
+ GNUNET_CHAT_file_close_preview(file);
+
if ((handle->preview_animation) || (handle->preview_image))
{
gtk_widget_set_size_request(
@@ -651,8 +653,6 @@ _update_file_message(UI_MESSAGE_Handle *handle,
return;
}
- GNUNET_CHAT_file_close_preview(file);
-
if (_message_media_supports_file_extension(filename))
{
gtk_image_set_from_icon_name(