/* This file is part of GNUnet. Copyright (C) 2021--2022 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 . SPDX-License-Identifier: AGPL3.0-or-later */ /* * @author Tobias Frisch * @file ui/message.c */ #include "message.h" #include #include "../application.h" #include "../file.h" #include "../ui.h" static void handle_downloading_file(void *cls, const struct GNUNET_CHAT_File *file, uint64_t completed, uint64_t size) { MESSENGER_Application *app = (MESSENGER_Application*) cls; if (!app) return; file_update_download_info(file, app, completed, size); } static void handle_file_button_click(GtkButton *button, gpointer user_data) { MESSENGER_Application *app = (MESSENGER_Application*) user_data; UI_MESSAGE_Handle* handle = (UI_MESSAGE_Handle*) ( g_object_get_qdata(G_OBJECT(button), app->quarks.ui) ); if (!handle) return; struct GNUNET_CHAT_File *file = (struct GNUNET_CHAT_File*) ( g_object_get_qdata(G_OBJECT(handle->file_progress_bar), app->quarks.data) ); if (!file) return; uint64_t size = GNUNET_CHAT_file_get_size(file); if (size <= 0) return; uint64_t local_size = GNUNET_CHAT_file_get_local_size(file); if (GNUNET_YES == GNUNET_CHAT_file_is_downloading(file)) { GNUNET_CHAT_file_stop_download(file); gtk_image_set_from_icon_name( handle->file_status_image, "folder-download-symbolic", GTK_ICON_SIZE_BUTTON ); } else if (local_size < size) { GNUNET_CHAT_file_start_download(file, handle_downloading_file, app); gtk_image_set_from_icon_name( handle->file_status_image, "process-stop-symbolic", GTK_ICON_SIZE_BUTTON ); } else if (size > 0) { const gchar *preview = GNUNET_CHAT_file_open_preview(file); if (!preview) return; GString* uri = g_string_new("file://"); g_string_append(uri, preview); if (!g_app_info_launch_default_for_uri(uri->str, NULL, NULL)) GNUNET_CHAT_file_close_preview(file); g_string_free(uri, TRUE); } } static int handle_message_redraw_animation(gpointer user_data) { UI_MESSAGE_Handle *handle = (UI_MESSAGE_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) { UI_MESSAGE_Handle *handle = (UI_MESSAGE_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_message_redraw_animation, handle ); render_image: if (!image) return FALSE; int dwidth = gdk_pixbuf_get_width(image); int dheight = gdk_pixbuf_get_height(image); gint optimal_height = width * dheight / dwidth; gtk_widget_set_size_request( GTK_WIDGET(drawing_area), width, optimal_height ); double ratio_width = 1.0 * width / dwidth; double ratio_height = 1.0 * height / dheight; const double ratio = ratio_width < ratio_height? ratio_width : ratio_height; dwidth = (int) (dwidth * ratio); dheight = (int) (dheight * ratio); double dx = (width - dwidth) * 0.5; double dy = (height - dheight) * 0.5; const int interp_type = (ratio >= 1.0? GDK_INTERP_NEAREST : GDK_INTERP_BILINEAR ); GdkPixbuf* scaled = gdk_pixbuf_scale_simple( image, dwidth, dheight, interp_type ); gtk_render_icon(context, cairo, scaled, dx, dy); cairo_fill(cairo); g_object_unref(scaled); return FALSE; } static void _clear_message_preview_data(UI_MESSAGE_Handle *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; } if (handle->preview_drawing_area) gtk_widget_set_size_request( GTK_WIDGET(handle->preview_drawing_area), -1, -1 ); } UI_MESSAGE_Handle* ui_message_new(MESSENGER_Application *app, UI_MESSAGE_Type type) { UI_MESSAGE_Handle* handle = g_malloc(sizeof(UI_MESSAGE_Handle)); handle->type = type; handle->timestamp = GNUNET_TIME_absolute_get_zero_(); handle->msg = NULL; const char *ui_builder_file; switch (handle->type) { case UI_MESSAGE_SENT: ui_builder_file = "ui/message-sent.ui"; break; case UI_MESSAGE_STATUS: ui_builder_file = "ui/message-status.ui"; break; default: ui_builder_file = "ui/message.ui"; break; } handle->builder[0] = gtk_builder_new_from_resource( application_get_resource_path(app, ui_builder_file) ); handle->message_box = GTK_WIDGET( gtk_builder_get_object(handle->builder[0], "message_box") ); handle->sender_avatar = HDY_AVATAR( gtk_builder_get_object(handle->builder[0], "sender_avatar") ); handle->sender_label = GTK_LABEL( gtk_builder_get_object(handle->builder[0], "sender_label") ); if (UI_MESSAGE_STATUS == handle->type) { handle->deny_revealer = GTK_REVEALER( gtk_builder_get_object(handle->builder[0], "deny_revealer") ); handle->accept_revealer = GTK_REVEALER( gtk_builder_get_object(handle->builder[0], "accept_revealer") ); handle->deny_button = GTK_BUTTON( gtk_builder_get_object(handle->builder[0], "deny_button") ); handle->accept_button = GTK_BUTTON( gtk_builder_get_object(handle->builder[0], "accept_button") ); } else { handle->deny_revealer = NULL; handle->accept_revealer = NULL; handle->deny_button = NULL; handle->accept_button = NULL; } GtkContainer *content_box = GTK_CONTAINER( gtk_builder_get_object(handle->builder[0], "content_box") ); handle->builder[1] = gtk_builder_new_from_resource( application_get_resource_path(app, "ui/message_content.ui") ); handle->timestamp_label = GTK_LABEL( gtk_builder_get_object(handle->builder[1], "timestamp_label") ); handle->read_receipt_image = GTK_IMAGE( gtk_builder_get_object(handle->builder[1], "read_receipt_image") ); handle->content_stack = GTK_STACK( gtk_builder_get_object(handle->builder[1], "content_stack") ); handle->text_label = GTK_LABEL( gtk_builder_get_object(handle->builder[1], "text_label") ); handle->file_revealer = GTK_REVEALER( gtk_builder_get_object(handle->builder[1], "file_revealer") ); handle->filename_label = GTK_LABEL( gtk_builder_get_object(handle->builder[1], "filename_label") ); handle->file_progress_bar = GTK_PROGRESS_BAR( gtk_builder_get_object(handle->builder[1], "file_progress_bar") ); handle->file_button = GTK_BUTTON( gtk_builder_get_object(handle->builder[1], "file_button") ); g_signal_connect( handle->file_button, "clicked", G_CALLBACK(handle_file_button_click), app ); handle->file_status_image = GTK_IMAGE( gtk_builder_get_object(handle->builder[1], "file_status_image") ); g_object_set_qdata(G_OBJECT(handle->file_button), app->quarks.ui, handle); handle->preview_drawing_area = GTK_DRAWING_AREA( gtk_builder_get_object(handle->builder[1], "preview_drawing_area") ); g_signal_connect( handle->preview_drawing_area, "draw", G_CALLBACK(handle_preview_drawing_area_draw), handle ); handle->whisper_box = GTK_WIDGET( gtk_builder_get_object(handle->builder[1], "whisper_box") ); switch (handle->type) { case UI_MESSAGE_STATUS: gtk_widget_set_visible(GTK_WIDGET(handle->timestamp_label), FALSE); break; default: break; } gtk_container_add(content_box, GTK_WIDGET( gtk_builder_get_object(handle->builder[1], "message_content_box") )); handle->preview_image = NULL; handle->preview_animation = NULL; handle->preview_animation_iter = NULL; handle->redraw_animation = 0; return handle; } static int _iterate_read_receipts(void *cls, UNUSED const struct GNUNET_CHAT_Message *message, const struct GNUNET_CHAT_Contact *contact, int read_receipt) { int *count_read_receipts = (int*) cls; if ((GNUNET_YES == read_receipt) && (GNUNET_NO == GNUNET_CHAT_contact_is_owned(contact))) (*count_read_receipts)++; return GNUNET_YES; } void ui_message_refresh(UI_MESSAGE_Handle *handle) { if ((!(handle->msg)) || (GNUNET_YES != GNUNET_CHAT_message_is_sent(handle->msg))) return; int count = 0; if ((0 < GNUNET_CHAT_message_get_read_receipt(handle->msg, _iterate_read_receipts, &count)) && (0 < count)) gtk_widget_show(GTK_WIDGET(handle->read_receipt_image)); else gtk_widget_hide(GTK_WIDGET(handle->read_receipt_image)); } static void _update_file_message(UI_MESSAGE_Handle *handle, MESSENGER_Application *app, struct GNUNET_CHAT_File *file) { const char *filename = GNUNET_CHAT_file_get_name(file); uint64_t size = GNUNET_CHAT_file_get_size(file); uint64_t local_size = GNUNET_CHAT_file_get_local_size(file); gboolean autostart_download = FALSE; if ((size <= 0) || (size > local_size)) { gtk_image_set_from_icon_name( handle->file_status_image, "folder-download-symbolic", GTK_ICON_SIZE_BUTTON ); if ((app->settings.accept_all_files) && (!GNUNET_CHAT_file_is_downloading(file))) autostart_download = TRUE; goto file_content; } if (!(handle->preview_drawing_area)) goto file_progress; const char *preview = GNUNET_CHAT_file_open_preview(file); if (!preview) goto file_progress; 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); if ((handle->preview_animation) || (handle->preview_animation)) { gtk_widget_set_size_request( GTK_WIDGET(handle->preview_drawing_area), 250, -1 ); gtk_stack_set_visible_child( handle->content_stack, GTK_WIDGET(handle->preview_drawing_area) ); gtk_widget_queue_draw(GTK_WIDGET(handle->preview_drawing_area)); return; } GNUNET_CHAT_file_close_preview(file); file_progress: gtk_progress_bar_set_fraction(handle->file_progress_bar, 1.0); gtk_image_set_from_icon_name( handle->file_status_image, "document-open-symbolic", GTK_ICON_SIZE_BUTTON ); file_content: ui_label_set_text(handle->filename_label, filename); gtk_stack_set_visible_child( handle->content_stack, GTK_WIDGET(handle->file_revealer) ); gtk_revealer_set_reveal_child(handle->file_revealer, TRUE); g_object_set_qdata( G_OBJECT(handle->file_progress_bar), app->quarks.data, file ); if (autostart_download) gtk_button_clicked(handle->file_button); } void ui_message_update(UI_MESSAGE_Handle *handle, MESSENGER_Application *app, const struct GNUNET_CHAT_Message *msg) { struct GNUNET_CHAT_File *file = NULL; handle->msg = msg; ui_message_refresh(handle); if (msg) { if (GNUNET_CHAT_KIND_WHISPER == GNUNET_CHAT_message_get_kind(msg)) gtk_stack_set_visible_child( handle->content_stack, GTK_WIDGET(handle->whisper_box) ); file = GNUNET_CHAT_message_get_file(msg); handle->timestamp = GNUNET_CHAT_message_get_timestamp(msg); g_object_set_qdata(G_OBJECT(handle->message_box), app->quarks.data, file); } else file = (struct GNUNET_CHAT_File*) ( g_object_get_qdata(G_OBJECT(handle->message_box), app->quarks.data) ); if (!file) return; _update_file_message(handle, app, file); } void ui_message_delete(UI_MESSAGE_Handle *handle) { _clear_message_preview_data(handle); g_object_unref(handle->builder[1]); g_object_unref(handle->builder[0]); g_free(handle); }