diff options
Diffstat (limited to 'main/src/ui')
17 files changed, 390 insertions, 195 deletions
diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index 03128964..cb3d52e4 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -126,6 +126,7 @@ public class Dino.Ui.Application : Adw.Application, Dino.Application { case "join": show_join_muc_dialog(null, jid); break; + case "roster": case "message": Gee.List<Account> accounts = stream_interactor.get_accounts(); Jid parsed_jid = null; @@ -291,6 +292,16 @@ public class Dino.Ui.Application : Adw.Application, Dino.Application { call_state.reject(); }); add_action(deny_call_action); + + SimpleAction save_file_action = new SimpleAction("file_open_save_dialog", VariantType.INT32); + save_file_action.activate.connect((variant) => { + print("action open save dialog\n"); + int file_transfer_id = variant.get_int32(); + FileTransfer? file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_id(file_transfer_id, null); + + window.conversation_view.open_save_file_dialog(file_transfer); + }); + add_action(save_file_action); } private void show_preferences_window() { diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index c93a9e22..afbef637 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -245,11 +245,11 @@ public class ChatInputController : Object { send_text(); } - foreach (File file in files_to_send) { - stream_interactor.get_module(FileManager.IDENTITY).send_file.begin(file, conversation); + if (!files_to_send.is_empty) { + stream_interactor.get_module(FileManager2.IDENTITY).send_files.begin(files_to_send, conversation); + files_to_send = new ArrayList<File>(); + chat_input.clear_files(); } - files_to_send = new ArrayList<File>(); - chat_input.clear_files(); } private void on_text_input_changed() { diff --git a/main/src/ui/conversation_content_view/content_populator.vala b/main/src/ui/conversation_content_view/content_populator.vala index ef859bde..4d1b8c90 100644 --- a/main/src/ui/conversation_content_view/content_populator.vala +++ b/main/src/ui/conversation_content_view/content_populator.vala @@ -70,6 +70,8 @@ public class ContentProvider : ContentItemCollection, Object { return new FileMetaItem(content_item, stream_interactor); } else if (content_item.type_ == CallItem.TYPE) { return new CallMetaItem(content_item, stream_interactor); + } else if (content_item.type_ == FileGroupItem.TYPE) { + return new FileGroupMetaItem(content_item, stream_interactor); } critical("Got unknown content item type %s", content_item.type_); return null; diff --git a/main/src/ui/conversation_content_view/file_default_widget.vala b/main/src/ui/conversation_content_view/file_default_widget.vala index a21e2afb..1ad3d6c2 100644 --- a/main/src/ui/conversation_content_view/file_default_widget.vala +++ b/main/src/ui/conversation_content_view/file_default_widget.vala @@ -51,13 +51,13 @@ public class FileDefaultWidget : Box { mime_label.label = content_type != null ? content_type.get_description() : null; } - public void update_file_info(Xmpp.FileContentType? content_type, FileTransfer.State state, bool direction, int64 size, int64 transferred_bytes) { - this.state = state; + public void update_file_info(FileTransfer file_transfer) { + this.state = file_transfer.state; spinner.stop(); // A hidden spinning spinner still uses CPU. Deactivate asap - content_type_image.icon_name = get_file_icon_name(content_type); - string? mime_description = content_type != null ? content_type.get_description() : null; + content_type_image.icon_name = get_file_icon_name(file_transfer.content_type); + string? mime_description = file_transfer.content_type != null ? file_transfer.content_type.get_description() : null; switch (state) { case FileTransfer.State.COMPLETE: @@ -66,23 +66,31 @@ public class FileDefaultWidget : Box { // Create a menu Menu menu_model = new Menu(); - menu_model.append(_("Open"), "file.open"); - menu_model.append(_("Save as…"), "file.save_as"); + + MenuItem open_file_item = new MenuItem(_("Open"), "app.file_open_externally"); + open_file_item.set_action_and_target_value("app.file_open_externally", new Variant.int32(file_transfer.id)); + + MenuItem save_as_item = new MenuItem(_("Save as…"), "app.file_save_as"); + save_as_item.set_action_and_target_value("app.file_open_save_dialog", new Variant.int32(file_transfer.id)); + + menu_model.append_item(open_file_item); + menu_model.append_item(save_as_item); + Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model); file_menu.popover = popover_menu; popover_menu.closed.connect(on_pointer_left); break; case FileTransfer.State.IN_PROGRESS: - if (direction == FileTransfer.DIRECTION_RECEIVED) { - if (size > 0) { - int64 progress = transferred_bytes * 100 / size; - mime_label.label = _("Downloading %s… (%u%%)").printf(get_size_string(size), progress); + if (file_transfer.direction == FileTransfer.DIRECTION_RECEIVED) { + if (file_transfer.size > 0) { + int64 progress = file_transfer.transferred_bytes * 100 / file_transfer.size; + mime_label.label = _("Downloading %s… (%u%%)").printf(get_size_string(file_transfer.size), progress); } else { - mime_label.label = _("Downloading %s…").printf(get_size_string(size)); + mime_label.label = _("Downloading %s…").printf(get_size_string(file_transfer.size)); } } else { - int64 progress = transferred_bytes * 100 / size; - mime_label.label = _("Uploading %s… (%u%%)").printf(get_size_string(size), progress); + int64 progress = file_transfer.transferred_bytes * 100 / file_transfer.size; + mime_label.label = _("Uploading %s… (%u%%)").printf(get_size_string(file_transfer.size), progress); } spinner.start(); image_stack.set_visible_child_name("spinner"); @@ -96,9 +104,9 @@ public class FileDefaultWidget : Box { break; case FileTransfer.State.NOT_STARTED: if (mime_description != null) { - mime_label.label = _("%s offered: %s").printf(mime_description, get_size_string(size)); - } else if (size != -1) { - mime_label.label = _("File offered: %s").printf(get_size_string(size)); + mime_label.label = _("%s offered: %s").printf(mime_description, get_size_string(file_transfer.size)); + } else if (file_transfer.size != -1) { + mime_label.label = _("File offered: %s").printf(get_size_string(file_transfer.size)); } else { mime_label.label = _("File offered"); } diff --git a/main/src/ui/conversation_content_view/file_group_widget.vala b/main/src/ui/conversation_content_view/file_group_widget.vala new file mode 100644 index 00000000..80c41a7a --- /dev/null +++ b/main/src/ui/conversation_content_view/file_group_widget.vala @@ -0,0 +1,120 @@ +using Gee; +using Gdk; +using Gtk; +using Pango; +using Xmpp; + +using Dino.Entities; + +namespace Dino.Ui { + + public class FileGroupMetaItem : ConversationSummary.ContentMetaItem { + + private StreamInteractor stream_interactor; + private FileGroupItem file_group_item; + + public FileGroupMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { + base(content_item); + this.stream_interactor = stream_interactor; + this.file_group_item = content_item as FileGroupItem; + } + + public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { + var file_transfers = file_group_item.file_group.file_transfers; + + if (file_transfers.size > 1 && all_files_images(file_transfers)) { + return new ImageGroupWidget(file_transfers); + } else { + Box box = new Box(Orientation.VERTICAL, 6); + foreach (FileTransfer file_transfer in file_transfers) { + FileWidget widget = new FileWidget(file_transfer) { halign = Align.START }; + widget.add_css_class("file-image-widget"); + FileWidgetController widget_controller = new FileWidgetController(widget, file_transfer, stream_interactor); + box.append(widget); + } + return box; + } + } + + public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { + Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>(); + return actions; + } + } + + public class ImageGroupWidget : Box { + + private Gee.ArrayList<FileTransfer> file_transfers; + + public ImageGroupWidget(Gee.ArrayList<FileTransfer> file_transfers) { + this.file_transfers = file_transfers; + + if (file_transfers.size == 3) { + Box box = new Box(Orientation.HORIZONTAL, 4) { + overflow = Overflow.HIDDEN, + halign = Align.START + }; + box.add_css_class("image-group"); + + var picture1 = new FileImageWidget.from_file_transfer(file_transfers[0], 200); + box.append(picture1); + + Box box1 = new Box(Orientation.VERTICAL, 4); + + var picture2 = new FileImageWidget.from_file_transfer(file_transfers[1], 100); + box1.append(picture2); + var picture3 = new FileImageWidget.from_file_transfer(file_transfers[2], 100); + box1.append(picture3); + box.append(box1); + + this.append(box); + } else { + Box box = new Box(Orientation.VERTICAL, 4) { + overflow = Overflow.HIDDEN, + halign = Align.START + }; + box.add_css_class("image-group"); + + Box row_upper_row = new Box(Orientation.HORIZONTAL, 4); + + var picture1 = new FileImageWidget.from_file_transfer(file_transfers[0], 150); + row_upper_row.append(picture1); + + var picture2 = new FileImageWidget.from_file_transfer(file_transfers[1], 150); + row_upper_row.append(picture2); + box.append(row_upper_row); + + if (file_transfers.size >= 4) { + + int size = 150; + if (file_transfers.size == 5) size = 99; + + Box row_lower_row = new Box(Orientation.HORIZONTAL, 4); + + var picture3 = new FileImageWidget.from_file_transfer(file_transfers[2], size); + row_lower_row.append(picture3); + + var picture4 = new FileImageWidget.from_file_transfer(file_transfers[3], size); + row_lower_row.append(picture4); + + if (file_transfers.size == 5) { + var picture5 = new FileImageWidget.from_file_transfer(file_transfers[4], size); + row_lower_row.append(picture5); + } + + box.append(row_lower_row); + } + this.append(box); + } + } + } + + public static bool all_files_images(Gee.ArrayList<FileTransfer> file_transfers) { + foreach (FileTransfer file_transfer in file_transfers) { + if (!Dino.Util.is_pixbuf_supported_content_type(file_transfer.content_type)) { + return false; + } + } + return true; + } +}
\ No newline at end of file diff --git a/main/src/ui/conversation_content_view/file_image_widget.vala b/main/src/ui/conversation_content_view/file_image_widget.vala index e936ca25..9f12f892 100644 --- a/main/src/ui/conversation_content_view/file_image_widget.vala +++ b/main/src/ui/conversation_content_view/file_image_widget.vala @@ -22,6 +22,8 @@ public class FileImageWidget : Widget { private Gtk.Box image_overlay_toolbar = new Gtk.Box(Orientation.VERTICAL, 0) { halign=Align.END, valign=Align.START, margin_top=10, margin_start=10, margin_end=10, margin_bottom=10, vexpand=false, visible=false }; private Label file_size_label = new Label(null) { halign=Align.START, valign=Align.END, margin_bottom=4, margin_start=4, visible=false }; + private int size = -1; + private FileTransfer file_transfer; private FileTransmissionProgress transmission_progress = new FileTransmissionProgress() { halign=Align.CENTER, valign=Align.CENTER, visible=false }; @@ -34,22 +36,6 @@ public class FileImageWidget : Widget { public FileImageWidget(int MAX_WIDTH=600, int MAX_HEIGHT=300) { this.halign = Align.START; - this.add_css_class("file-image-widget"); - - // Setup menu button overlay - MenuButton button = new MenuButton(); - button.icon_name = "dino-view-more"; - Menu menu_model = new Menu(); - menu_model.append(_("Open"), "file.open"); - menu_model.append(_("Save as…"), "file.save_as"); - Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model); - button.popover = popover_menu; - image_overlay_toolbar.append(button); - image_overlay_toolbar.add_css_class("card"); - image_overlay_toolbar.add_css_class("toolbar"); - image_overlay_toolbar.add_css_class("compact-card-toolbar"); - image_overlay_toolbar.set_cursor_from_name("default"); - file_size_label.add_css_class("file-details"); overlay.set_child(stack); @@ -65,12 +51,65 @@ public class FileImageWidget : Widget { gesture_click_controller.button = 1; // listen for left clicks gesture_click_controller.released.connect(on_image_clicked); stack.add_controller(gesture_click_controller); + } + + public FileImageWidget.from_file_transfer(FileTransfer file_transfer, int size = -1) { + this(); + + this.file_transfer = file_transfer; + this.size = size; + + this.file_transfer.bind_property("size", file_size_label, "label", BindingFlags.SYNC_CREATE, file_size_label_transform); + this.file_transfer.bind_property("size", transmission_progress, "file-size", BindingFlags.SYNC_CREATE); + this.file_transfer.bind_property("transferred-bytes", transmission_progress, "transferred-size"); + + setup_signals(transmission_progress, file_transfer); + setup_menu_button(); + + file_transfer.notify["state"].connect(refresh_state); + file_transfer.sources_changed.connect(refresh_state); + refresh_state(); + } + + private static void setup_signals(FileTransmissionProgress transmission_progress, FileTransfer file_transfer) { + var file_transfer_id = file_transfer.id; + + transmission_progress.start_download.connect(() => { + Dino.Application.get_default().activate_action("file_start_download", new Variant.int32(file_transfer_id)); + }); + transmission_progress.cancel_transfer.connect(() => { + Dino.Application.get_default().activate_action("file_cancel_transfer", new Variant.int32(file_transfer_id)); + }); + } + + private void setup_menu_button() { + MenuButton button = new MenuButton(); + button.icon_name = "dino-view-more"; + Menu menu_model = new Menu(); + + MenuItem open_file_item = new MenuItem(_("Open"), "app.file_open_externally"); + open_file_item.set_action_and_target_value("app.file_open_externally", new Variant.int32(file_transfer.id)); + + MenuItem save_as_item = new MenuItem(_("Save as…"), "app.file_save_as"); + save_as_item.set_action_and_target_value("app.file_open_save_dialog", new Variant.int32(file_transfer.id)); + + menu_model.append_item(open_file_item); + menu_model.append_item(save_as_item); + + Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model); + button.popover = popover_menu; + image_overlay_toolbar.append(button); + image_overlay_toolbar.add_css_class("card"); + image_overlay_toolbar.add_css_class("toolbar"); + image_overlay_toolbar.add_css_class("compact-card-toolbar"); + image_overlay_toolbar.set_cursor_from_name("default"); EventControllerMotion this_motion_events = new EventControllerMotion(); this.add_controller(this_motion_events); this_motion_events.enter.connect((controller, x, y) => { (controller.widget as FileImageWidget).on_motion_event_enter(); }); + attach_on_motion_event_leave(this_motion_events, button); } @@ -88,18 +127,6 @@ public class FileImageWidget : Widget { file_size_label.visible = file_transfer != null && file_transfer.direction == FileTransfer.DIRECTION_RECEIVED && file_transfer.state == FileTransfer.State.NOT_STARTED && !file_transfer.sfs_sources.is_empty; } - public async void set_file_transfer(FileTransfer file_transfer) { - this.file_transfer = file_transfer; - - this.file_transfer.bind_property("size", file_size_label, "label", BindingFlags.SYNC_CREATE, file_size_label_transform); - this.file_transfer.bind_property("size", transmission_progress, "file-size", BindingFlags.SYNC_CREATE); - this.file_transfer.bind_property("transferred-bytes", transmission_progress, "transferred-size"); - - file_transfer.notify["state"].connect(refresh_state); - file_transfer.sources_changed.connect(refresh_state); - refresh_state(); - } - private static bool file_size_label_transform(Binding binding, Value from_value, ref Value to_value) { to_value = FileDefaultWidget.get_size_string((int64) from_value); return true; @@ -153,7 +180,7 @@ public class FileImageWidget : Widget { } public async void load_from_file(File file, string file_name) throws GLib.Error { - FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=600, max_height=300 }; + FixedRatioPicture image = create_picture(); FileInputStream file_stream = null; try { @@ -201,7 +228,7 @@ public class FileImageWidget : Widget { throw new Error(-1, 0, "Error scaling preview image"); } - FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=600, max_height=300 }; + FixedRatioPicture image = create_picture(); image.paintable = Texture.for_pixbuf(pixbuf); stack.add_child(image); stack.set_visible_child(image); @@ -216,12 +243,12 @@ public class FileImageWidget : Widget { if (n_press == 1) { image_overlay_toolbar.visible = !image_overlay_toolbar.visible; } else if (n_press == 2) { - this.activate_action("file.open", null); + Dino.Application.get_default().activate_action("file_open_externally", new Variant.int32(file_transfer.id)); image_overlay_toolbar.visible = false; } break; default: - this.activate_action("file.open", null); + Dino.Application.get_default().activate_action("file_open_externally", new Variant.int32(file_transfer.id)); image_overlay_toolbar.visible = false; break; } @@ -237,6 +264,20 @@ public class FileImageWidget : Widget { (file_transfer.state == FileTransfer.State.COMPLETE || file_transfer.thumbnails.size > 0); } + private FixedRatioPicture create_picture() { + if (size == -1) { + return new FixedRatioPicture() { + min_width=100, max_width=600, + min_height=100, max_height=300 + }; + } else { + return new FixedRatioPicture() { + height_request = size, width_request = size, + content_fit = ContentFit.COVER, halign = Align.START + }; + } + } + public override void dispose() { if (overlay != null && overlay.parent != null) overlay.unparent(); base.dispose(); diff --git a/main/src/ui/conversation_content_view/file_transmission_progress.vala b/main/src/ui/conversation_content_view/file_transmission_progress.vala index e3f33448..3d893f52 100644 --- a/main/src/ui/conversation_content_view/file_transmission_progress.vala +++ b/main/src/ui/conversation_content_view/file_transmission_progress.vala @@ -9,6 +9,9 @@ namespace Dino.Ui { public class FileTransmissionProgress : Adw.Bin { + public signal void start_download(); + public signal void cancel_transfer(); + public enum State { UNKNOWN_SOURCE, DOWNLOAD_NOT_STARTED, @@ -81,9 +84,7 @@ namespace Dino.Ui { } } -#if GTK_4_14 private static extern Gsk.Path create_progress_arc(Gsk.Path circle, float percentage); -#endif public override void snapshot(Gtk.Snapshot snapshot) { base.snapshot(snapshot); @@ -94,7 +95,6 @@ namespace Dino.Ui { float line_width = (float) LINE_WIDTH; snapshot.translate({get_width() / 2, get_height() / 2}); -#if GTK_4_14 var fg_color = get_color(); get_style_context().lookup_color("accent_color", out fg_color); var builder = new Gsk.PathBuilder(); @@ -104,16 +104,6 @@ namespace Dino.Ui { stroke.set_line_cap(Gsk.LineCap.ROUND); var arc_path = create_progress_arc(circle_path, (float) progress_animation.value); snapshot.append_stroke(arc_path, stroke, fg_color); -#else - var fg_color = get_style_context().get_color(); - get_style_context().lookup_color("accent_color", out fg_color); - var context = snapshot.append_cairo({{-radius, -radius}, {radius*2, radius*2}}); - context.set_line_cap(Cairo.LineCap.ROUND); - context.set_line_width(line_width); - context.set_source_rgba(fg_color.red, fg_color.green, fg_color.blue, fg_color.alpha); - context.arc(0, 0, radius - line_width / 2, -0.5 * Math.PI, (progress_animation.value - 0.25) * 2 * Math.PI); - context.stroke(); -#endif } } @@ -124,11 +114,12 @@ namespace Dino.Ui { break; case DOWNLOAD_NOT_STARTED_FAILED_BEFORE: case DOWNLOAD_NOT_STARTED: + start_download(); this.activate_action("file.download", null); break; case DOWNLOADING: case UPLOADING: - this.activate_action("file.cancel", null); + cancel_transfer(); break; } } diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index 3c21412a..d6d2415a 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -58,18 +58,6 @@ public class FileWidget : SizeRequestBin { private FileDefaultWidgetController default_widget_controller; private Widget? content = null; - public signal void open_file(); - public signal void save_file_as(); - public signal void start_download(); - public signal void cancel_download(); - - class construct { - install_action("file.open", null, (widget, action_name) => { ((FileWidget) widget).open_file(); }); - install_action("file.save_as", null, (widget, action_name) => { ((FileWidget) widget).save_file_as(); }); - install_action("file.download", null, (widget, action_name) => { ((FileWidget) widget).start_download(); }); - install_action("file.cancel", null, (widget, action_name) => { ((FileWidget) widget).cancel_download(); }); - } - construct { margin_top = 4; size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH; @@ -80,11 +68,6 @@ public class FileWidget : SizeRequestBin { this.file_transfer = file_transfer; update_widget.begin(); -// size_allocate.connect((allocation) => { -// if (allocation.height > parent.get_allocated_height()) { -// Idle.add(() => { parent.queue_resize(); return false; }); -// } -// }); file_transfer.bind_property("state", this, "file-transfer-state"); file_transfer.bind_property("content-type", this, "file-transfer-content-type"); @@ -102,8 +85,8 @@ public class FileWidget : SizeRequestBin { FileImageWidget file_image_widget = null; try { - file_image_widget = new FileImageWidget(); - yield file_image_widget.set_file_transfer(file_transfer); + file_image_widget = new FileImageWidget.from_file_transfer(file_transfer); + file_image_widget.add_css_class("file-image-widget"); // If the widget changed in the meanwhile, stop if (content != content_bak) return; @@ -166,50 +149,6 @@ public class FileWidgetController : Object { }); this.file_transfer = file_transfer; this.stream_interactor = stream_interactor; - - widget.open_file.connect(open_file); - widget.save_file_as.connect(save_file); - widget.start_download.connect(start_download); - widget.cancel_download.connect(cancel_download); - } - - private void open_file() { - try{ - AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); - } catch (Error err) { - warning("Failed to open %s - %s", file_transfer.get_file().get_uri(), err.message); - } - } - - private void save_file() { - var save_dialog = new FileChooserNative(_("Save as…"), widget.get_root() as Gtk.Window, FileChooserAction.SAVE, null, null); - save_dialog.set_modal(true); - save_dialog.set_current_name(file_transfer.file_name); - - save_dialog.response.connect(() => { - File? target_file = save_dialog.get_file(); - if (target_file == null) { - warning("No file returned from save dialog."); - return; - } - try { - file_transfer.get_file().copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null); - } catch (Error err) { - warning("Failed copy file %s - %s", file_transfer.get_file().get_uri(), err.message); - } - }); - - save_dialog.show(); - } - - private void start_download() { - if (stream_interactor != null) { - stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer); - } - } - - private void cancel_download() { - file_transfer.cancellable.cancel(); } } @@ -248,16 +187,16 @@ public class FileDefaultWidgetController : Object { private void update_file_info() { state = file_transfer.state; - widget.update_file_info(file_transfer.content_type, file_transfer.state, file_transfer.direction, file_transfer.size, file_transfer.transferred_bytes); + widget.update_file_info(file_transfer); } private void on_clicked() { switch (state) { case FileTransfer.State.COMPLETE: - widget.activate_action("file.open", null); + Dino.Application.get_default().activate_action("file_open_externally", new GLib.Variant.int32(file_transfer.id)); break; case FileTransfer.State.NOT_STARTED: - widget.activate_action("file.download", null); + Dino.Application.get_default().activate_action("file_start_download", new GLib.Variant.int32(file_transfer.id)); break; default: // Clicking doesn't do anything in FAILED and IN_PROGRESS states diff --git a/main/src/ui/conversation_selector/conversation_selector.vala b/main/src/ui/conversation_selector/conversation_selector.vala index c11eea82..25ea1b1e 100644 --- a/main/src/ui/conversation_selector/conversation_selector.vala +++ b/main/src/ui/conversation_selector/conversation_selector.vala @@ -23,7 +23,7 @@ public class ConversationSelector : Widget { list_box.add_css_class("navigation-sidebar"); this.layout_manager = new BinLayout(); - stream_interactor.get_module(ConversationManager.IDENTITY).conversation_activated.connect(add_conversation); + stream_interactor.get_module(ConversationManager.IDENTITY).conversation_activated.connect(on_conversation_activated); stream_interactor.get_module(ConversationManager.IDENTITY).conversation_deactivated.connect(remove_conversation); stream_interactor.get_module(ContentItemStore.IDENTITY).new_item.connect(on_content_item_received); Timeout.add_seconds(60, () => { @@ -34,6 +34,7 @@ public class ConversationSelector : Widget { foreach (Conversation conversation in stream_interactor.get_module(ConversationManager.IDENTITY).get_active_conversations()) { add_conversation(conversation); } + list_box.invalidate_sort(); return this; } @@ -74,6 +75,7 @@ public class ConversationSelector : Widget { public void on_conversation_selected(Conversation conversation) { if (!rows.has_key(conversation)) { add_conversation(conversation); + list_box.invalidate_sort(); } list_box.select_row(rows[conversation]); } @@ -84,6 +86,11 @@ public class ConversationSelector : Widget { } } + private void on_conversation_activated(Conversation conversation) { + add_conversation(conversation); + list_box.invalidate_sort(); + } + private void add_conversation(Conversation conversation) { ConversationSelectorRow row; if (!rows.has_key(conversation)) { @@ -113,7 +120,6 @@ public class ConversationSelector : Widget { }); row.add_controller(drop_motion_controller); } - list_box.invalidate_sort(); } private void select_fallback_conversation(Conversation conversation) { diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index 82c96fdc..95384a5e 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -257,6 +257,25 @@ public class ConversationSelectorRow : ListBoxRow { message_label.label = (file_is_image ? _("Image received") : _("File received") ); } break; + case FileGroupItem.TYPE: + FileGroupItem file_group_item = last_content_item as FileGroupItem; + FileTransfer first_transfer = file_group_item.file_group.file_transfers.get(0); + + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + // TODO properly display nick for oneself + nick_label.label = Util.get_participant_display_name(stream_interactor, conversation, first_transfer.from, true) + ": "; + } else { + nick_label.label = first_transfer.direction == Message.DIRECTION_SENT ? _("Me") + ": " : ""; + } + + bool all_files_images = Ui.all_files_images(file_group_item.file_group.file_transfers); + change_label_attribute(message_label, attr_style_new(Pango.Style.ITALIC)); + if (first_transfer.direction == Message.DIRECTION_SENT) { + message_label.label = (all_files_images ? _("Images sent") : _("Files sent") ); + } else { + message_label.label = (all_files_images ? _("Images received") : _("Files received") ); + } + break; case CallItem.TYPE: CallItem call_item = (CallItem) last_content_item; Call call = call_item.call; diff --git a/main/src/ui/conversation_view.vala b/main/src/ui/conversation_view.vala index 26043666..4bb1f537 100644 --- a/main/src/ui/conversation_view.vala +++ b/main/src/ui/conversation_view.vala @@ -17,6 +17,27 @@ public class ConversationView : Widget { construct { this.layout_manager = new BinLayout(); } + + public void open_save_file_dialog(FileTransfer file_transfer) { + var save_dialog = new FileChooserNative(_("Save as…"), (Gtk.Window) this.get_root(), FileChooserAction.SAVE, null, null); + save_dialog.set_modal(true); + save_dialog.set_current_name(file_transfer.file_name); + + save_dialog.response.connect(() => { + File? target_file = save_dialog.get_file(); + if (target_file == null) { + warning("No file returned from save dialog."); + return; + } + try { + file_transfer.get_file().copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null); + } catch (Error err) { + warning("Failed copy file %s - %s", file_transfer.get_file().get_uri(), err.message); + } + }); + + save_dialog.show(); + } } } diff --git a/main/src/ui/conversation_view_controller.vala b/main/src/ui/conversation_view_controller.vala index 0acaae59..bff8a70b 100644 --- a/main/src/ui/conversation_view_controller.vala +++ b/main/src/ui/conversation_view_controller.vala @@ -228,10 +228,6 @@ public class ConversationViewController : Object { chooser.show(); } - private void send_file(File file) { - stream_interactor.get_module(FileManager.IDENTITY).send_file.begin(file, conversation); - } - private bool forward_key_press_to_chat_input(EventControllerKey key_controller, uint keyval, uint keycode, Gdk.ModifierType state) { if (view.get_root().get_focus() is TextView) { return false; diff --git a/main/src/ui/main_window.vala b/main/src/ui/main_window.vala index 98d68e64..fc18d2bd 100644 --- a/main/src/ui/main_window.vala +++ b/main/src/ui/main_window.vala @@ -11,23 +11,23 @@ public class MainWindow : Gtk.ApplicationWindow { public signal void conversation_selected(Conversation conversation); - [GtkChild] public Stack stack; - [GtkChild] public Adw.NavigationSplitView navigation_split_view; + [GtkChild] public unowned Stack stack; + [GtkChild] public unowned Adw.NavigationSplitView navigation_split_view; - [GtkChild] public SearchEntry conversation_search_entry; - [GtkChild] public MenuButton menu_button; + [GtkChild] public unowned SearchEntry conversation_search_entry; + [GtkChild] public unowned MenuButton menu_button; [GtkChild] private unowned Box conversation_list_toolbar; - [GtkChild] public Adw.HeaderBar conversation_headerbar; - [GtkChild] public Button conversation_title_button; - [GtkChild] public Adw.WindowTitle conversation_window_title; + [GtkChild] public unowned Adw.HeaderBar conversation_headerbar; + [GtkChild] public unowned Button conversation_title_button; + [GtkChild] public unowned Adw.WindowTitle conversation_window_title; - [GtkChild] public ConversationView conversation_view; - [GtkChild] public ConversationSelector conversation_selector; - [GtkChild] public Adw.Flap search_flap; - [GtkChild] private Stack left_stack; - [GtkChild] private Stack right_stack; - [GtkChild] public Adw.Bin search_frame; + [GtkChild] public unowned ConversationView conversation_view; + [GtkChild] public unowned ConversationSelector conversation_selector; + [GtkChild] public unowned Adw.Flap search_flap; + [GtkChild] private unowned Stack left_stack; + [GtkChild] private unowned Stack right_stack; + [GtkChild] public unowned Adw.Bin search_frame; public GlobalSearch global_search; @@ -54,6 +54,9 @@ public class MainWindow : Gtk.ApplicationWindow { this.title = "Dino"; this.add_css_class("dino-main"); + if (Dino.get_version().contains("~git")) { + this.add_css_class("devel"); + } var header_size_group = new SizeGroup(SizeGroupMode.VERTICAL); header_size_group.add_widget(conversation_list_toolbar); diff --git a/main/src/ui/util/data_forms.vala b/main/src/ui/util/data_forms.vala index 7fb9b87d..0b91da15 100644 --- a/main/src/ui/util/data_forms.vala +++ b/main/src/ui/util/data_forms.vala @@ -18,7 +18,7 @@ namespace Dino.Ui.Util { } public static ViewModel.PreferencesRow.Any? get_data_form_field_view_model(DataForms.DataForm.Field field) { - if (field.type_ == null) return null; + if (field.type_ == DataForms.DataForm.Type.UNKNOWN) return null; ViewModel.PreferencesRow.Any? view_model = null; diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index e9035be0..a126a196 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -415,18 +415,7 @@ public bool use_tooltips() { } public static void menu_button_set_icon_with_size(MenuButton menu_button, string icon_name, int pixel_size) { -#if GTK_4_6 && VALA_0_52 menu_button.set_child(new Image.from_icon_name(icon_name) { pixel_size=pixel_size }); -#else - menu_button.set_icon_name(icon_name); - var button = menu_button.get_first_child() as Button; - if (button == null) return; - var box = button.child as Box; - if (box == null) return; - var image = box.get_first_child() as Image; - if (image == null) return; - image.pixel_size = pixel_size; -#endif } } diff --git a/main/src/ui/widgets/avatar_picture.vala b/main/src/ui/widgets/avatar_picture.vala index 18e4d02f..b4636f49 100644 --- a/main/src/ui/widgets/avatar_picture.vala +++ b/main/src/ui/widgets/avatar_picture.vala @@ -5,7 +5,20 @@ using Xmpp; public class Dino.Ui.ViewModel.AvatarPictureTileModel : Object { public string display_text { get; set; } public Gdk.RGBA background_color { get; set; } - public File? image_file { get; set; } + + private File? _image_file; + [CCode (notify = false)] + public File? image_file { + get { + return _image_file; + } + set { + if (_image_file == value) return; + if (_image_file != null && value != null && _image_file.equal(value)) return; + _image_file = value; + notify_property("image-file"); + } + } } public class Dino.Ui.ViewModel.AvatarPictureModel : Object { @@ -23,8 +36,18 @@ public class Dino.Ui.ViewModel.ConversationParticipantAvatarPictureTileModel : A public ConversationParticipantAvatarPictureTileModel(StreamInteractor stream_interactor, Conversation conversation, Jid jid) { this.stream_interactor = stream_interactor; + + set_conversation_participant(conversation, jid); + + stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect(on_roster_updated); + avatar_manager.received_avatar.connect(on_received_avatar); + avatar_manager.fetched_avatar.connect(on_received_avatar); + } + + public void set_conversation_participant(Conversation conversation, Jid jid) { this.conversation = conversation; this.primary_avatar_jid = jid; + this.secondary_avatar_jid = null; this.display_name_jid = jid; string color_id = jid.to_string(); @@ -43,14 +66,11 @@ public class Dino.Ui.ViewModel.ConversationParticipantAvatarPictureTileModel : A } string display = Util.get_participant_display_name(stream_interactor, conversation, display_name_jid); display_text = display.get_char(0).toupper().to_string(); - stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect(on_roster_updated); float[] rgbf = color_id != null ? Xep.ConsistentColor.string_to_rgbf(color_id) : new float[] {0.5f, 0.5f, 0.5f}; background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}; update_image_file(); - avatar_manager.received_avatar.connect(on_received_avatar); - avatar_manager.fetched_avatar.connect(on_received_avatar); } private void update_image_file() { @@ -140,55 +160,76 @@ public class Dino.Ui.ViewModel.CompatAvatarPictureModel : AvatarPictureModel { private void on_room_updated(Account account, Jid room) { if (conversation != null && account.equals(conversation.account) && conversation.counterpart.equals_bare(room)) { - reset(); - set_conversation(conversation); + update_all(); } } private void on_received_avatar(Jid jid, Account account) { + // Only update group chat avatar when the respective jid avatar is actually used in it + if (conversation == null || conversation.type_ == Conversation.Type.GROUPCHAT && !jid.is_bare()) return; on_room_updated(account, jid); } - public void reset() { - (tiles as GLib.ListStore).remove_all(); - } - public CompatAvatarPictureModel set_conversation(Conversation conversation) { if (stream_interactor == null) { critical("set_conversation() used on CompatAvatarPictureModel without stream_interactor"); return this; } this.conversation = conversation; + update_all(); + return this; + } + + private void update_all() { + uint idx = 0; if (conversation.type_ == Conversation.Type.GROUPCHAT) { if (avatar_manager.has_avatar(conversation.account, conversation.counterpart)) { - add_internal("#", conversation.counterpart.to_string(), avatar_manager.get_avatar_file(conversation.account, conversation.counterpart)); + set_internal(idx++, "#", conversation.counterpart.to_string(), avatar_manager.get_avatar_file(conversation.account, conversation.counterpart)); } else { Gee.List<Jid>? occupants = muc_manager.get_other_offline_members(conversation.counterpart, conversation.account); if (occupants != null && !occupants.is_empty && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { int count = occupants.size > 4 ? 3 : occupants.size; for (int i = 0; i < count; i++) { - add_participant(conversation, occupants[i]); + set_participant(idx++, conversation, occupants[i]); } if (occupants.size > 4) { - add_internal("+"); + set_internal(idx++, "+"); } } else { - add_internal("#", conversation.counterpart.to_string()); + set_internal(idx++, "#", conversation.counterpart.to_string()); } } } else { - add_participant(conversation, conversation.counterpart); + set_participant(idx++, conversation, conversation.counterpart); + } + + GLib.ListStore store = tiles as GLib.ListStore; + if (store.get_n_items() >= idx) { + store.splice(idx, store.get_n_items() - idx, {}); } - return this; } public CompatAvatarPictureModel add_participant(Conversation conversation, Jid jid) { + set_participant(tiles.get_n_items(), conversation, jid); + return this; + } + + public AvatarPictureTileModel set_participant(uint idx, Conversation conversation, Jid jid) { if (stream_interactor == null) { - critical("add_participant() used on CompatAvatarPictureModel without stream_interactor"); - return this; + error("set_participant() used on CompatAvatarPictureModel without stream_interactor"); } - (tiles as GLib.ListStore).append(new ConversationParticipantAvatarPictureTileModel(stream_interactor, conversation, jid)); - return this; + GLib.ListStore store = tiles as GLib.ListStore; + while (idx >= store.get_n_items()) { + store.append(new ViewModel.AvatarPictureTileModel()); + } + var model = store.get_item(idx) as ConversationParticipantAvatarPictureTileModel; + if (model == null) { + model = new ConversationParticipantAvatarPictureTileModel(stream_interactor, conversation, jid); + store.splice(idx, 1, {model}); + } else { + model.set_conversation_participant(conversation, jid); + } + return model; } public CompatAvatarPictureModel add(string display, string? color_id = null, File? image_file = null) { @@ -197,14 +238,28 @@ public class Dino.Ui.ViewModel.CompatAvatarPictureModel : AvatarPictureModel { } private AvatarPictureTileModel add_internal(string display, string? color_id = null, File? image_file = null) { + return set_internal(tiles.get_n_items(), display, color_id, image_file); + } + + private AvatarPictureTileModel set_internal(uint idx, string display, string? color_id = null, File? image_file = null) { GLib.ListStore store = tiles as GLib.ListStore; float[] rgbf = color_id != null ? Xep.ConsistentColor.string_to_rgbf(color_id) : new float[] {0.5f, 0.5f, 0.5f}; - var model = new ViewModel.AvatarPictureTileModel() { - display_text = display.get_char(0).toupper().to_string(), - background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}, - image_file = image_file - }; - store.append(model); + while (idx >= store.get_n_items()) { + store.append(new ViewModel.AvatarPictureTileModel()); + } + var model = store.get_item(idx) as ViewModel.AvatarPictureTileModel; + if (model == null) { + model = new ViewModel.AvatarPictureTileModel() { + display_text = display.get_char(0).toupper().to_string(), + background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}, + image_file = image_file + }; + store.splice(idx, 1, {model}); + } else { + model.display_text = display.get_char(0).toupper().to_string(); + model.background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}; + model.image_file = image_file; + } return model; } } @@ -476,11 +531,7 @@ public class Dino.Ui.AvatarPicture : Gtk.Widget { label.insert_after(this, null); label.attributes = new Pango.AttrList(); label.attributes.insert(Pango.attr_foreground_new(uint16.MAX, uint16.MAX, uint16.MAX)); -#if GTK_4_8 && (VALA_0_56_GREATER_5 || VALA_0_58) picture.content_fit = Gtk.ContentFit.COVER; -#elif GTK_4_8 - picture.@set("content-fit", 2); -#endif picture.insert_after(this, label); this.notify["model"].connect(on_model_changed); check_widget_leak(this); diff --git a/main/src/ui/widgets/fixed_ratio_picture.vala b/main/src/ui/widgets/fixed_ratio_picture.vala index 6b04ea61..1898efa7 100644 --- a/main/src/ui/widgets/fixed_ratio_picture.vala +++ b/main/src/ui/widgets/fixed_ratio_picture.vala @@ -8,9 +8,7 @@ class Dino.Ui.FixedRatioPicture : Gtk.Widget { public int max_height { get; set; default = int.MAX; } public File file { get { return inner.file; } set { inner.file = value; } } public Gdk.Paintable paintable { get { return inner.paintable; } set { inner.paintable = value; } } -#if GTK_4_8 && (VALA_0_56_GREATER_5 || VALA_0_58) public Gtk.ContentFit content_fit { get { return inner.content_fit; } set { inner.content_fit = value; } } -#endif private Gtk.Picture inner = new Gtk.Picture(); construct { |
