From ec5c0d9fc1db577e4799365be9744781b71dee11 Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Mon, 27 Jan 2025 15:29:13 +0200 Subject: [PATCH 1/4] fix(AttachmentsPage): handle application/octet-stream (#1312) --- src/Dialogs/Composer/AttachmentsPage.vala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Dialogs/Composer/AttachmentsPage.vala b/src/Dialogs/Composer/AttachmentsPage.vala index 42f92a0fd..589bc171d 100644 --- a/src/Dialogs/Composer/AttachmentsPage.vala +++ b/src/Dialogs/Composer/AttachmentsPage.vala @@ -356,6 +356,10 @@ public class Tuba.AttachmentsPage : ComposerPage { && accounts.active.instance_info.configuration.media_attachments != null && accounts.active.instance_info.configuration.media_attachments.supported_mime_types != null && accounts.active.instance_info.configuration.media_attachments.supported_mime_types.size > 0 + && !( + accounts.active.instance_info.configuration.media_attachments.supported_mime_types.size == 1 + && accounts.active.instance_info.configuration.media_attachments.supported_mime_types[0] == "application/octet-stream" + ) ) { supported_mimes = accounts.active.instance_info.configuration.media_attachments.supported_mime_types; } From 62b8dc0fe7276d95842e33bce9d607512149d69d Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Fri, 31 Jan 2025 12:54:28 +0200 Subject: [PATCH 2/4] feat(Status): move thread lines to snapshotting (#1315) --- data/style.css | 14 ------- data/ui/widgets/status.ui | 25 ------------ src/Widgets/Status.vala | 81 ++++++++++++++++++++++++++++----------- 3 files changed, 59 insertions(+), 61 deletions(-) diff --git a/data/style.css b/data/style.css index 5246e941b..a0480a020 100644 --- a/data/style.css +++ b/data/style.css @@ -61,11 +61,6 @@ headerbar.flat.no-title .title { margin: 0px; } -.ttl-thread-line { - background: var(--window-fg-color); - opacity: .1; -} - .ttl-code { padding: 12px; background: rgba(150, 150, 150, .1); @@ -352,15 +347,6 @@ headerbar.flat.no-title .title { font-weight: initial; } -.ttl-thread-line.top { - margin-top: -18px; - margin-bottom: -3px; -} - -.ttl-thread-line.bottom { - margin-bottom: -20px; -} - .attachment-overlay-icon { border-radius: 100%; min-height: 64px; diff --git a/data/ui/widgets/status.ui b/data/ui/widgets/status.ui index 21ca5b28e..60bd41c02 100644 --- a/data/ui/widgets/status.ui +++ b/data/ui/widgets/status.ui @@ -36,18 +36,6 @@ 1 - - - 0 - 4 - center - 4 - - - 3 @@ -63,19 +51,6 @@ - - - 0 - 4 - 1 - center - 4 - - - diff --git a/src/Widgets/Status.vala b/src/Widgets/Status.vala index 3cc3abad0..cf70663e5 100644 --- a/src/Widgets/Status.vala +++ b/src/Widgets/Status.vala @@ -104,8 +104,6 @@ [GtkChild] protected unowned Gtk.Image header_icon; [GtkChild] protected unowned Widgets.RichLabel header_label; [GtkChild] protected unowned Gtk.Button header_button; - [GtkChild] public unowned Gtk.Image thread_line_top; - [GtkChild] public unowned Gtk.Image thread_line_bottom; [GtkChild] public unowned Widgets.Avatar avatar; [GtkChild] public unowned Gtk.Overlay avatar_overlay; @@ -1288,27 +1286,66 @@ } } + const float THREAD_WIDTH = 4f; + + public override void snapshot (Gtk.Snapshot snapshot) { + if (!expanded && enable_thread_lines && status.formal.tuba_thread_role != NONE && filter_stack.visible_child_name == "status") { + float y; + float height; + + // Get the avatar's position + Graphene.Point avatar_point; + avatar.compute_point ( + this, + Graphene.Point () { x = 0.0f, y=0.0f }, + out avatar_point + ); + + // NOTE: we need the thread line to be > status height as + // it looks better if it reaches the status' bounds + // so the sizes below are either always bigger or + // start at a negative point + switch (status.formal.tuba_thread_role) { + // Thread starter line needs to start from the + // center of the avatar and end at the end of + // the status + case START: + y = avatar_point.y + avatar.get_height () / 2f; + height = (float) this.get_height (); + break; + // Thread in-between line needs to start from the + // top and end at the end of the status + case MIDDLE: + y = -4f; + height = this.get_height () * 1.2f; + break; + // Thread end line needs to start from the + // status top and end at the center of the + // avatar + case END: + y = -4f; + height = (avatar_point.y + avatar.get_height () / 2f) + 4f; + break; + default: + assert_not_reached (); + } + + var line_rect = Graphene.Rect () { + // we need the center of the avatar for the x point minus half the thread line width + origin = Graphene.Point () { x = avatar_point.x + avatar.get_width () / 2f - THREAD_WIDTH / 2f, y = y }, + size = Graphene.Size () { width = THREAD_WIDTH, height = height } + }; + + snapshot.push_opacity (0.1); + snapshot.append_color (this.get_color (), line_rect); + snapshot.pop (); + } + + base.snapshot (snapshot); + } + // Threads public void install_thread_line () { - if (expanded || !enable_thread_lines) return; - - switch (status.formal.tuba_thread_role) { - case NONE: - thread_line_top.visible = false; - thread_line_bottom.visible = false; - break; - case START: - thread_line_top.visible = false; - thread_line_bottom.visible = true; - break; - case MIDDLE: - thread_line_top.visible = true; - thread_line_bottom.visible = true; - break; - case END: - thread_line_top.visible = true; - thread_line_bottom.visible = false; - break; - } + this.queue_draw (); } } From feab6220228f499f55cf082b4bab88c72a572fa1 Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Fri, 31 Jan 2025 13:46:45 +0200 Subject: [PATCH 3/4] fix(ci): flatpak (#1316) --- .github/workflows/build.yml | 4 ++-- build-aux/dev.geopjr.Tuba.Devel.json | 33 +++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f828ebc3f..f672f44ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest needs: [ lint ] container: - image: bilelmoussaoui/flatpak-github-actions:gnome-43 + image: bilelmoussaoui/flatpak-github-actions:gnome-47 options: --privileged strategy: matrix: @@ -39,7 +39,7 @@ jobs: uses: docker/setup-qemu-action@v2 with: platforms: arm64 - - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + - uses: flathub-infra/flatpak-github-actions/flatpak-builder@4388a4c5fc8bab58e1dfb7fc63267dca0f7b4976 with: bundle: "dev.geopjr.Tuba.Devel.flatpak" run-tests: true diff --git a/build-aux/dev.geopjr.Tuba.Devel.json b/build-aux/dev.geopjr.Tuba.Devel.json index 26ff18b70..fff35a3a9 100644 --- a/build-aux/dev.geopjr.Tuba.Devel.json +++ b/build-aux/dev.geopjr.Tuba.Devel.json @@ -3,6 +3,7 @@ "runtime": "org.gnome.Platform", "runtime-version": "47", "sdk": "org.gnome.Sdk", + "sdk-extensions": [ "org.freedesktop.Sdk.Extension.llvm19" ], "command": "dev.geopjr.Tuba", "finish-args": [ "--device=dri", @@ -26,12 +27,23 @@ ], "modules": [ { - "name" : "libspelling", - "buildsystem" : "meson", - "config-opts" : [ + "name": "libspelling", + "buildsystem": "meson", + "config-opts": [ "-Ddocs=false" ], - "sources" : [ + "build-options": { + "arch": { + "aarch64": { + "append-path": "/usr/lib/sdk/llvm19/bin", + "prepend-ld-library-path": "/usr/lib/sdk/llvm19/lib", + "env": { + "CC": "clang" + } + } + } + }, + "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/GNOME/libspelling.git", @@ -43,9 +55,20 @@ "name": "tuba", "builddir": true, "buildsystem": "meson", - "config-opts" : [ + "config-opts": [ "-Ddevel=true" ], + "build-options": { + "arch": { + "aarch64": { + "append-path": "/usr/lib/sdk/llvm19/bin", + "prepend-ld-library-path": "/usr/lib/sdk/llvm19/lib", + "env": { + "CC": "clang" + } + } + } + }, "sources": [ { "type": "dir", From 66580942bd23b12e4a528364949990886e5e7a23 Mon Sep 17 00:00:00 2001 From: Marius Melzer Date: Sat, 25 Jan 2025 17:35:35 +0100 Subject: [PATCH 4/4] Refactor ListView (WIP) Currently not again in a working state. Two major changes: 1) Use ListView only for Status containers (Timelines and Threads). Reason for this is mostly to not need to adapt all the other Widgets). 2) In order to use the performance benefit of ListView: differente for Status between the setup of the widget and the (re)initialisation of the widget with content. Point 2 could performancewise be (probably significantly) improved by moving more of the initialisation that's not content specific out of init() and into the empty Status constructor. --- src/API/Conversation.vala | 2 +- src/API/Status.vala | 10 +- src/Dialogs/Report.vala | 6 +- src/Views/Base.vala | 14 +-- src/Views/ContentBase.vala | 101 +++++------------ src/Views/ContentBaseListView.vala | 136 +++++++++++++++++++++++ src/Views/EditHistory.vala | 12 +- src/Views/Lists.vala | 14 --- src/Views/MutesBlocks.vala | 4 +- src/Views/NotificationRequests.vala | 12 +- src/Views/Profile.vala | 68 +++++------- src/Views/StatusStats.vala | 6 +- src/Views/TabbedBase.vala | 14 +-- src/Views/Thread.vala | 33 +++--- src/Views/Timeline.vala | 70 +----------- src/Views/meson.build | 1 + src/Widgets/ScheduledStatus.vala | 5 +- src/Widgets/Status.vala | 17 ++- src/Widgets/WidgetizableForListView.vala | 17 +++ src/Widgets/meson.build | 1 + 20 files changed, 270 insertions(+), 273 deletions(-) create mode 100644 src/Views/ContentBaseListView.vala create mode 100644 src/Widgets/WidgetizableForListView.vala diff --git a/src/API/Conversation.vala b/src/API/Conversation.vala index d4825e8cc..f6fca9c0c 100644 --- a/src/API/Conversation.vala +++ b/src/API/Conversation.vala @@ -1,4 +1,4 @@ -public class Tuba.API.Conversation : Entity, Widgetizable { +public class Tuba.API.Conversation : Entity, WidgetizableForListView { public string id { get; set; } public Gee.ArrayList? accounts { get; set; } diff --git a/src/API/Status.vala b/src/API/Status.vala index e4ff96117..65d999fdb 100644 --- a/src/API/Status.vala +++ b/src/API/Status.vala @@ -1,4 +1,4 @@ -public class Tuba.API.Status : Entity, Widgetizable, SearchResult { +public class Tuba.API.Status : Entity, WidgetizableForListView, SearchResult { ~Status () { debug (@"[OBJ] Destroyed $(uri ?? "")"); @@ -219,7 +219,13 @@ public class Tuba.API.Status : Entity, Widgetizable, SearchResult { } public override Gtk.Widget to_widget () { - return new Widgets.Status (this); + return new Widgets.Status (); + } + + public override void fill_widget_with_content (Gtk.Widget widget) throws Oopsie { + ((Widgets.Status) widget).kind_instigator = this.account; + ((Widgets.Status) widget).status = this; + ((Widgets.Status) widget).init (this); } public override void open () { diff --git a/src/Dialogs/Report.vala b/src/Dialogs/Report.vala index 8cb9e09fe..ee70a4177 100644 --- a/src/Dialogs/Report.vala +++ b/src/Dialogs/Report.vala @@ -475,11 +475,7 @@ public class Tuba.Dialogs.Report : Adw.Dialog { widget_status.can_target = false; widget_status.focusable = false; widget_status.actions.visible = false; - #if USE_LISTVIEW - widget_status.can_be_opened = false; - #else - widget_status.activatable = false; - #endif + widget_status.can_be_opened = false; listbox.append (new StatusRow (checkbutton, widget_status)); }); diff --git a/src/Views/Base.vala b/src/Views/Base.vala index c25c09c75..64d4aa66b 100644 --- a/src/Views/Base.vala +++ b/src/Views/Base.vala @@ -68,11 +68,7 @@ public class Tuba.Views.Base : Adw.BreakpointBin { // [GtkChild] protected unowned Adw.Clamp clamp; // [GtkChild] protected unowned Gtk.Box column_view; [GtkChild] protected unowned Gtk.Stack states; - #if USE_LISTVIEW - [GtkChild] protected unowned Adw.ClampScrollable content_box; - #else - [GtkChild] protected unowned Adw.Clamp content_box; - #endif + [GtkChild] protected unowned Adw.ClampScrollable content_box; [GtkChild] protected unowned Gtk.Button status_button; [GtkChild] unowned Gtk.Image status_image; [GtkChild] unowned Gtk.Stack status_stack; @@ -186,11 +182,9 @@ public class Tuba.Views.Base : Adw.BreakpointBin { base.dispose (); } - #if !USE_LISTVIEW - public virtual void unbind_listboxes () { - this.last_widget = null; - } - #endif + public virtual void unbind_listboxes () { + this.last_widget = null; + } protected virtual void build_actions () {} diff --git a/src/Views/ContentBase.vala b/src/Views/ContentBase.vala index 533156c18..ac6ec93d2 100644 --- a/src/Views/ContentBase.vala +++ b/src/Views/ContentBase.vala @@ -1,10 +1,5 @@ public class Tuba.Views.ContentBase : Views.Base { - - #if USE_LISTVIEW - protected Gtk.ListView content; - #else - protected Gtk.ListBox content; - #endif + protected Gtk.ListBox content; protected signal void reached_close_to_top (); public GLib.ListStore model; private bool bottom_reached_locked = false; @@ -16,32 +11,21 @@ public class Tuba.Views.ContentBase : Views.Base { construct { model = new GLib.ListStore (typeof (Widgetizable)); - #if USE_LISTVIEW - Gtk.SignalListItemFactory signallistitemfactory = new Gtk.SignalListItemFactory (); - signallistitemfactory.bind.connect (bind_listitem_cb); - - content = new Gtk.ListView (new Gtk.NoSelection (model), signallistitemfactory) { - css_classes = { "contentbase", "content", "background" }, - single_click_activate = true - }; - - content.activate.connect (on_content_item_activated); - model.items_changed.connect (on_content_changed); - #else - model.items_changed.connect (on_content_changed); - content = new Gtk.ListBox () { - selection_mode = Gtk.SelectionMode.NONE, - css_classes = { "content", "background" } - }; - - content.row_activated.connect (on_content_item_activated); - content.bind_model (model, on_create_model_widget); - #endif + model.items_changed.connect (on_content_changed); + + content = new Gtk.ListBox () { + selection_mode = Gtk.SelectionMode.NONE, + css_classes = { "fake-content", "background" } + }; + + content.row_activated.connect (on_content_item_activated); + content.bind_model (model, on_create_model_widget); content_box.child = content; scrolled.vadjustment.value_changed.connect (on_scrolled_vadjustment_value_change); scroll_to_top_rev.bind_property ("child-revealed", scroll_to_top_rev, "visible", GLib.BindingFlags.SYNC_CREATE); } + ~ContentBase () { debug ("Destroying ContentBase"); } @@ -71,26 +55,9 @@ public class Tuba.Views.ContentBase : Views.Base { scroll_to_top_rev.reveal_child = reveal; } - #if USE_LISTVIEW - protected virtual void bind_listitem_cb (GLib.Object item) { - ((Gtk.ListItem) item).child = on_create_model_widget (((Gtk.ListItem) item).item); - - var gtklistitemwidget = ((Gtk.ListItem) item).child.get_parent (); - if (gtklistitemwidget != null) { - gtklistitemwidget.add_css_class ("card"); - gtklistitemwidget.add_css_class ("card-spacing"); - gtklistitemwidget.focusable = true; - - // Thread lines overflow slightly - gtklistitemwidget.overflow = Gtk.Overflow.HIDDEN; - } - } - #endif public override void dispose () { - #if !USE_LISTVIEW - unbind_listboxes (); - #endif + unbind_listboxes (); base.dispose (); } @@ -114,31 +81,25 @@ public class Tuba.Views.ContentBase : Views.Base { } } - #if !USE_LISTVIEW - public override void unbind_listboxes () { - if (content != null) - content.bind_model (null, null); - base.unbind_listboxes (); - } - #endif + public override void unbind_listboxes () { + if (content != null) + content.bind_model (null, null); + base.unbind_listboxes (); + } public virtual Gtk.Widget on_create_model_widget (Object obj) { var obj_widgetable = obj as Widgetizable; if (obj_widgetable == null) Process.exit (0); try { - #if !USE_LISTVIEW - Gtk.Widget widget = obj_widgetable.to_widget (); - widget.add_css_class ("card"); - widget.add_css_class ("card-spacing"); - widget.focusable = true; - - // Thread lines overflow slightly - widget.overflow = Gtk.Overflow.HIDDEN; - return widget; - #else - return obj_widgetable.to_widget (); - #endif + Gtk.Widget widget = obj_widgetable.to_widget (); + widget.add_css_class ("card"); + widget.add_css_class ("card-spacing"); + widget.focusable = true; + + // Thread lines overflow slightly + widget.overflow = Gtk.Overflow.HIDDEN; + return widget; } catch (Oopsie e) { warning (@"Error on_create_model_widget: $(e.message)"); Process.exit (0); @@ -155,13 +116,7 @@ public class Tuba.Views.ContentBase : Views.Base { }, Priority.LOW); } - #if USE_LISTVIEW - public virtual void on_content_item_activated (uint pos) { - ((Widgetizable) ((ListModel) content.model).get_item (pos)).open (); - } - #else - public virtual void on_content_item_activated (Gtk.ListBoxRow row) { - Signal.emit_by_name (row, "open"); - } - #endif + public virtual void on_content_item_activated (Gtk.ListBoxRow row) { + Signal.emit_by_name (row, "open"); + } } diff --git a/src/Views/ContentBaseListView.vala b/src/Views/ContentBaseListView.vala new file mode 100644 index 000000000..3f92b5da3 --- /dev/null +++ b/src/Views/ContentBaseListView.vala @@ -0,0 +1,136 @@ +public class Tuba.Views.ContentBaseListView : Views.Base { + + protected Gtk.ListView content; + protected signal void reached_close_to_top (); + public GLib.ListStore model; + private bool bottom_reached_locked = false; + + public bool empty { + get { return model.get_n_items () <= 0; } + } + + construct { + model = new GLib.ListStore (typeof (WidgetizableForListView)); + + Gtk.SignalListItemFactory signallistitemfactory = new Gtk.SignalListItemFactory (); + signallistitemfactory.setup.connect (setup_listitem_cb); + signallistitemfactory.bind.connect (bind_listitem_cb); + + content = new Gtk.ListView (new Gtk.NoSelection (model), signallistitemfactory) { + css_classes = { "content", "background" }, + single_click_activate = true + }; + + content.activate.connect (on_content_item_activated); + content_box.child = content; + + scrolled.vadjustment.value_changed.connect (on_scrolled_vadjustment_value_change); + scroll_to_top_rev.bind_property ("child-revealed", scroll_to_top_rev, "visible", GLib.BindingFlags.SYNC_CREATE); + } + ~ContentBaseListView () { + debug ("Destroying ContentBaseListView"); + } + + protected virtual void on_scrolled_vadjustment_value_change () { + if ( + !bottom_reached_locked + && scrolled.vadjustment.value > scrolled.vadjustment.upper - scrolled.vadjustment.page_size * 2 + ) { + bottom_reached_locked = true; + on_bottom_reached (); + } + + var is_close_to_top = scrolled.vadjustment.value <= 100; + set_scroll_to_top_reveal_child ( + !is_close_to_top + && scrolled.vadjustment.value + scrolled.vadjustment.page_size + 100 < scrolled.vadjustment.upper + ); + } + + protected void set_scroll_to_top_reveal_child (bool reveal) { + if (reveal == scroll_to_top_rev.reveal_child) return; + if (reveal) scroll_to_top_rev.visible = true; + + scroll_to_top_rev.reveal_child = reveal; + } + + protected void setup_listitem_cb (GLib.Object item) { + Gtk.ListItem i = (Gtk.ListItem) item; + i.child = on_create_model_widget (i.item); + + var gtklistitemwidget = i.child.get_parent (); + if (gtklistitemwidget != null) { + gtklistitemwidget.add_css_class ("card"); + gtklistitemwidget.add_css_class ("card-spacing"); + gtklistitemwidget.focusable = true; + + // Thread lines overflow slightly + gtklistitemwidget.overflow = Gtk.Overflow.HIDDEN; + } + } + + protected virtual void bind_listitem_cb (GLib.Object item) { + var obj_widgetable = ((Gtk.ListItem) item).item as WidgetizableForListView; + if (obj_widgetable == null) + Process.exit (0); + + try { + obj_widgetable.fill_widget_with_content (((Gtk.ListItem) item).child); + } catch (Oopsie e) { + warning (@"Error bind_listitem_cb: $(e.message)"); + Process.exit (0); + } + } + + public override void dispose () { + base.dispose (); + } + + public override void clear () { + base.clear (); + this.model.remove_all (); + } + + protected virtual void clear_all_but_first (int i = 1) { + base.clear (); + + print ("before splice!\n"); + if (model.n_items > i) + model.splice (i, model.n_items - i, {}); + } + + public override void on_content_changed () { + if (empty) { + base_status = new StatusMessage (); + } else { + base_status = null; + } + } + + public override void unbind_listboxes () {} + public virtual Gtk.Widget on_create_model_widget (Object obj) { + var obj_widgetable = obj as Widgetizable; + if (obj_widgetable == null) + Process.exit (0); + try { + return obj_widgetable.to_widget (); + } catch (Oopsie e) { + warning (@"Error on_create_model_widget: $(e.message)"); + Process.exit (0); + } + } + + public virtual void on_bottom_reached () { + uint timeout = 0; + timeout = Timeout.add (1000, () => { + bottom_reached_locked = false; + GLib.Source.remove (timeout); + + return true; + }, Priority.LOW); + } + + public virtual void on_content_item_activated (uint pos) { + ((WidgetizableForListView) ((ListModel) content.model).get_item (pos)).open (); + } +} diff --git a/src/Views/EditHistory.vala b/src/Views/EditHistory.vala index a7db20ae7..f05114640 100644 --- a/src/Views/EditHistory.vala +++ b/src/Views/EditHistory.vala @@ -14,17 +14,11 @@ public class Tuba.Views.EditHistory : Views.Timeline { widget_status.actions.visible = false; widget_status.menu_button.visible = false; - #if USE_LISTVIEW - widget_status.can_be_opened = false; - widget_status.content.selectable = true; - #else - widget_status.activatable = false; - #endif + widget_status.can_be_opened = false; + widget_status.content.selectable = true; return widget_status; } - #if USE_LISTVIEW - public override void on_content_item_activated (uint pos) {} - #endif + public override void on_content_item_activated (uint pos) {} } diff --git a/src/Views/Lists.vala b/src/Views/Lists.vala index d9d974750..b0efd02b2 100644 --- a/src/Views/Lists.vala +++ b/src/Views/Lists.vala @@ -30,10 +30,6 @@ public class Tuba.Views.Lists : Views.Timeline { action_box.append (edit_button); action_box.append (delete_button); - #if !USE_LISTVIEW - this.activated.connect (() => open ()); - #endif - this.activatable = true; this.add_suffix (action_box); @@ -142,16 +138,6 @@ public class Tuba.Views.Lists : Views.Timeline { public Adw.PreferencesDialog create_edit_preferences_dialog (API.List t_list) { return new Dialogs.ListEdit (t_list); } - - #if !USE_LISTVIEW - public virtual signal void open () { - if (this.list == null) - return; - - var view = new Views.List (list); - app.main_window.open_view (view); - } - #endif } public new bool empty { diff --git a/src/Views/MutesBlocks.vala b/src/Views/MutesBlocks.vala index 62fa48ac9..d069cc318 100644 --- a/src/Views/MutesBlocks.vala +++ b/src/Views/MutesBlocks.vala @@ -1,6 +1,6 @@ public class Tuba.Views.MutesBlocks : Views.TabbedBase { - Views.ContentBase mutes; - Views.ContentBase blocks; + Views.ContentBaseListView mutes; + Views.ContentBaseListView blocks; construct { label = _("Mutes & Blocks"); diff --git a/src/Views/NotificationRequests.vala b/src/Views/NotificationRequests.vala index d8b39c88c..2f4361f81 100644 --- a/src/Views/NotificationRequests.vala +++ b/src/Views/NotificationRequests.vala @@ -44,13 +44,7 @@ public class Tuba.Views.NotificationRequests : Views.Timeline { .exec (); } - #if USE_LISTVIEW - public override void on_content_item_activated (uint pos) { - ((Widgetizable) ((ListModel) content.model).get_item (pos)).open (); - } - #else - public override void on_content_item_activated (Gtk.ListBoxRow row) { - ((Widgets.NotificationRequest) row).open (); - } - #endif + public override void on_content_item_activated (uint pos) { + ((Widgetizable) ((ListModel) content.model).get_item (pos)).open (); + } } diff --git a/src/Views/Profile.vala b/src/Views/Profile.vala index 24aea0870..ac28c31d3 100644 --- a/src/Views/Profile.vala +++ b/src/Views/Profile.vala @@ -52,11 +52,15 @@ public class Tuba.Views.Profile : Views.Accounts { public class FilterGroup : Widgetizable, GLib.Object { public bool visible { get; set; default=true; } - public override Gtk.Widget to_widget () { + private Gtk.Widget create_widget () { var widget = new Widgets.ProfileFilterGroup (); this.bind_property ("visible", widget, "visible", GLib.BindingFlags.SYNC_CREATE); return widget; } + + public override Gtk.Widget to_widget () { + return create_widget (); + } } public ProfileAccount profile { get; construct set; } @@ -134,27 +138,10 @@ public class Tuba.Views.Profile : Views.Accounts { widget_cover.rs_invalidated.connect (on_rs_updated); widget_cover.timeline_change.connect (change_timeline_source); widget_cover.aria_updated.connect (on_cover_aria_update); - #if !USE_LISTVIEW - widget_cover.remove_css_class ("card"); - widget_cover.remove_css_class ("card-spacing"); - #endif this.cover_profile_update.connect (widget_cover.update_cover_from_profile); - #if USE_LISTVIEW - widget_cover.update_aria (); - return widget_cover; - #else - var row = new Gtk.ListBoxRow () { - focusable = true, - activatable = false, - child = widget_cover, - css_classes = { "card-spacing", "card" }, - overflow = Gtk.Overflow.HIDDEN - }; - widget_cover.update_aria (); - - return row; - #endif + widget_cover.update_aria (); + return widget_cover; } var widget_status = widget as Widgets.Status; @@ -165,40 +152,35 @@ public class Tuba.Views.Profile : Views.Accounts { var widget_filter_group = widget as Widgets.ProfileFilterGroup; if (widget_filter_group != null) { - #if !USE_LISTVIEW - widget_filter_group.remove_css_class ("card"); - #endif widget_filter_group.filter_change.connect (change_filter); } return widget; } - #if USE_LISTVIEW - protected override void bind_listitem_cb (GLib.Object item) { - ((Gtk.ListItem) item).child = on_create_model_widget (((Gtk.ListItem) item).item); - - var gtklistitemwidget = ((Gtk.ListItem) item).child.get_parent (); - if (gtklistitemwidget != null) { - gtklistitemwidget.add_css_class ("card-spacing"); - - if ((((Gtk.ListItem) item).child as Widgets.ProfileFilterGroup) == null) gtklistitemwidget.add_css_class ("keep-margin"); - if ((((Gtk.ListItem) item).child as Widgets.ProfileFilterGroup) == null && (((Gtk.ListItem) item).child as Widgets.Cover) == null) { - gtklistitemwidget.add_css_class ("card"); - } else { - ((Gtk.ListItem) item).activatable = false; - } + protected override void bind_listitem_cb (GLib.Object item) { + ((Gtk.ListItem) item).child = on_create_model_widget (((Gtk.ListItem) item).item); - gtklistitemwidget.focusable = true; + var gtklistitemwidget = ((Gtk.ListItem) item).child.get_parent (); + if (gtklistitemwidget != null) { + gtklistitemwidget.add_css_class ("card-spacing"); - // Thread lines overflow slightly - gtklistitemwidget.overflow = Gtk.Overflow.HIDDEN; + if ((((Gtk.ListItem) item).child as Widgets.ProfileFilterGroup) == null) gtklistitemwidget.add_css_class ("keep-margin"); + if ((((Gtk.ListItem) item).child as Widgets.ProfileFilterGroup) == null && (((Gtk.ListItem) item).child as Widgets.Cover) == null) { + gtklistitemwidget.add_css_class ("card"); + } else { + ((Gtk.ListItem) item).activatable = false; } - if (((((Gtk.ListItem) item).item) as ProfileAccount) != null) - ((Gtk.ListItem) item).activatable = false; + gtklistitemwidget.focusable = true; + + // Thread lines overflow slightly + gtklistitemwidget.overflow = Gtk.Overflow.HIDDEN; } - #endif + + if (((((Gtk.ListItem) item).item) as ProfileAccount) != null) + ((Gtk.ListItem) item).activatable = false; + } public override void on_refresh () { base.on_refresh (); diff --git a/src/Views/StatusStats.vala b/src/Views/StatusStats.vala index 13f93f7f3..44a67861c 100644 --- a/src/Views/StatusStats.vala +++ b/src/Views/StatusStats.vala @@ -1,7 +1,7 @@ public class Tuba.Views.StatusStats : Views.TabbedBase { - Views.ContentBase favorited; - Views.ContentBase boosted; - Views.ContentBase reacted; + Views.ContentBaseListView favorited; + Views.ContentBaseListView boosted; + Views.ContentBaseListView reacted; construct { label = _("Post Stats"); diff --git a/src/Views/TabbedBase.vala b/src/Views/TabbedBase.vala index 93ad097dc..49eac94a6 100644 --- a/src/Views/TabbedBase.vala +++ b/src/Views/TabbedBase.vala @@ -47,14 +47,12 @@ public class Tuba.Views.TabbedBase : Views.Base { views = {}; } - #if !USE_LISTVIEW - public override void unbind_listboxes () { - foreach (var tab in views) { - tab.unbind_listboxes (); - } - base.unbind_listboxes (); + public override void unbind_listboxes () { + foreach (var tab in views) { + tab.unbind_listboxes (); } - #endif + base.unbind_listboxes (); + } protected virtual bool title_stack_page_visible { get { @@ -118,7 +116,7 @@ public class Tuba.Views.TabbedBase : Views.Base { return tab; } - public Views.ContentBase add_timeline_tab (string label, string icon, string url, Type accepts, string? empty_state_title = null, string? empty_state_icon = null) { + public Views.ContentBaseListView add_timeline_tab (string label, string icon, string url, Type accepts, string? empty_state_title = null, string? empty_state_icon = null) { var tab = new Views.Accounts () { url = url, label = label, diff --git a/src/Views/Thread.vala b/src/Views/Thread.vala index 17c218426..99d007b4e 100644 --- a/src/Views/Thread.vala +++ b/src/Views/Thread.vala @@ -1,4 +1,4 @@ -public class Tuba.Views.Thread : Views.ContentBase, AccountHolder { +public class Tuba.Views.Thread : Views.ContentBaseListView, AccountHolder { public enum ThreadRole { NONE, START, @@ -184,17 +184,15 @@ public class Tuba.Views.Thread : Views.ContentBase, AccountHolder { connect_threads (); on_content_changed (); - #if USE_LISTVIEW - if (to_add_ancestors.length > 0) { - uint timeout = 0; - timeout = Timeout.add (1000, () => { - content.scroll_to (to_add_ancestors.length, Gtk.ListScrollFlags.FOCUS, null); + if (to_add_ancestors.length > 0) { + uint timeout = 0; + timeout = Timeout.add (1000, () => { + content.scroll_to (to_add_ancestors.length, Gtk.ListScrollFlags.FOCUS, null); - GLib.Source.remove (timeout); - return true; - }, Priority.LOW); - } - #endif + GLib.Source.remove (timeout); + return true; + }, Priority.LOW); + } }) .exec (); @@ -231,9 +229,6 @@ public class Tuba.Views.Thread : Views.ContentBase, AccountHolder { widget_status.kind = null; if (((API.Status) obj).id == root_status.id) { - #if !USE_LISTVIEW - widget_status.activatable = false; - #endif widget_status.expand_root (); root_status_widget = widget_status; } @@ -241,12 +236,10 @@ public class Tuba.Views.Thread : Views.ContentBase, AccountHolder { return widget_status; } - #if USE_LISTVIEW protected override void bind_listitem_cb (GLib.Object item) { - base.bind_listitem_cb (item); + base.bind_listitem_cb (item); - if (((API.Status) ((Gtk.ListItem) item).item).id == root_status.id) - ((Gtk.ListItem) item).activatable = false; - } - #endif + if (((API.Status) ((Gtk.ListItem) item).item).id == root_status.id) + ((Gtk.ListItem) item).activatable = false; + } } diff --git a/src/Views/Timeline.vala b/src/Views/Timeline.vala index 9645931f7..f960f9e3b 100644 --- a/src/Views/Timeline.vala +++ b/src/Views/Timeline.vala @@ -1,11 +1,8 @@ -public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase { +public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBaseListView { public string url { get; construct set; } public bool is_public { get; construct set; default = false; } public Type accepts { get; set; default = typeof (API.Status); } - #if !USE_LISTVIEW - public bool use_queue { get; set; default = true; } - #endif protected InstanceAccount? account { get; set; default = null; } @@ -14,9 +11,6 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase public string? page_prev { get; set; } protected int entity_queue_size { get; set; default=0; } - #if !USE_LISTVIEW - Entity[] entity_queue = {}; - #endif private Adw.Spinner pull_to_refresh_spinner; private bool _is_pulling = false; @@ -102,10 +96,6 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase settings.notify["show-preview-cards"].connect (on_refresh); settings.notify["enlarge-custom-emojis"].connect (on_refresh); - #if !USE_LISTVIEW - content.bind_model (model, on_create_model_widget); - #endif - var drag = new Gtk.GestureDrag (); drag.drag_update.connect (on_drag_update); drag.drag_end.connect (on_drag_end); @@ -117,21 +107,9 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase destruct_account_holder (); destruct_streamable (); - #if !USE_LISTVIEW - content.bind_model (null, null); - entity_queue = {}; - #endif entity_queue_size = 0; } - #if !USE_LISTVIEW - public override void unbind_listboxes () { - destruct_account_holder (); - destruct_streamable (); - base.unbind_listboxes (); - } - #endif - public override void dispose () { destruct_streamable (); base.dispose (); @@ -236,11 +214,7 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase } public virtual void on_refresh () { - #if !USE_LISTVIEW - entity_queue = {}; - #endif entity_queue_size = 0; - scrolled.vadjustment.value = 0; status_button.sensitive = false; clear (); @@ -299,49 +273,17 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase if (!has_finished_request) return; try { - #if USE_LISTVIEW - model.insert (0, Entity.from_json (accepts, ev.get_node ())); - if (scrolled.vadjustment.value > 100) { - entity_queue_size += 1; - return; - } - #else - var entity = Entity.from_json (accepts, ev.get_node ()); - if (should_hide (entity)) return; - - if (use_queue && scrolled.vadjustment.value > 100) { - entity_queue += entity; - entity_queue_size += 1; - return; - } - - // This can occur on race conditions or multiple calls. - // The post might already be in the timeline due to a refresh etc. - // So just if the id exists already in the first page and remove it. - if (accepts == typeof (API.Status)) { - string e_id = ((API.Status) entity).id; - for (uint i = 0; i < uint.min (model.n_items, settings.timeline_page_size); i++) { - var status_obj = model.get_item (i) as API.Status; - if (status_obj != null && status_obj.id == e_id) { - model.remove (i); - } - } - } - - model.insert (0, entity); - #endif + model.insert (0, Entity.from_json (accepts, ev.get_node ())); + if (scrolled.vadjustment.value > 100) { + entity_queue_size += 1; + return; + } } catch (Error e) { warning (@"Error getting Entity from json: $(e.message)"); } } private void finish_queue () { - #if !USE_LISTVIEW - if (entity_queue.length == 0) return; - model.splice (0, 0, (Object[])entity_queue); - - entity_queue = {}; - #endif entity_queue_size = 0; } diff --git a/src/Views/meson.build b/src/Views/meson.build index 4872191f5..ce1dd8998 100644 --- a/src/Views/meson.build +++ b/src/Views/meson.build @@ -5,6 +5,7 @@ sources += files( 'Bookmarks.vala', 'Bubble.vala', 'ContentBase.vala', + 'ContentBaseListView.vala', 'Conversations.vala', 'DraftStatuses.vala', 'Explore.vala', diff --git a/src/Widgets/ScheduledStatus.vala b/src/Widgets/ScheduledStatus.vala index e213a8c5d..c3b2721c0 100644 --- a/src/Widgets/ScheduledStatus.vala +++ b/src/Widgets/ScheduledStatus.vala @@ -116,11 +116,8 @@ public class Tuba.Widgets.ScheduledStatus : Gtk.ListBoxRow { if (scheduled_status.props.language != null) status.language = scheduled_status.props.language; - var widg = new Widgets.Status (status); + var widg = new Widgets.Status.from_status (status); widg.can_be_opened = false; - #if !USE_LISTVIEW - widg.activatable = false; - #endif widg.actions.visible = false; widg.menu_button.visible = false; widg.date_label.visible = false; diff --git a/src/Widgets/Status.vala b/src/Widgets/Status.vala index 68ff863cb..ae65b32bf 100644 --- a/src/Widgets/Status.vala +++ b/src/Widgets/Status.vala @@ -1,9 +1,5 @@ [GtkTemplate (ui = "/dev/geopjr/Tuba/ui/widgets/status.ui")] -#if USE_LISTVIEW - public class Tuba.Widgets.Status : Adw.Bin { -#else - public class Tuba.Widgets.Status : Gtk.ListBoxRow { -#endif +public class Tuba.Widgets.Status : Adw.Bin { API.Status? _bound_status = null; public API.Status? status { @@ -218,12 +214,20 @@ return res; } - public Status (API.Status status) { + public Status () { + Object (); + } + + public Status.from_status (API.Status status) { Object ( kind_instigator: status.account, status: status ); + init (status); + } + + public void init (API.Status status) { if (kind == null) { if (status.reblog != null) { kind = InstanceAccount.KIND_REMOTE_REBLOG; @@ -243,6 +247,7 @@ } } } + ~Status () { debug ("Destroying Status widget"); if (context_menu != null) { diff --git a/src/Widgets/WidgetizableForListView.vala b/src/Widgets/WidgetizableForListView.vala new file mode 100644 index 000000000..60891495f --- /dev/null +++ b/src/Widgets/WidgetizableForListView.vala @@ -0,0 +1,17 @@ +public interface Tuba.WidgetizableForListView : GLib.Object { + public virtual Gtk.Widget to_widget () throws Oopsie { + throw new Tuba.Oopsie.INTERNAL ("Widgetizable didn't provide a Widget!"); + } + + public virtual void open () { + warning ("Widgetizable didn't provide a way to open it!"); + } + + public virtual void resolve_open (InstanceAccount account) { + this.open (); + } + + public virtual void fill_widget_with_content (Gtk.Widget widget) throws Oopsie { + throw new Tuba.Oopsie.INTERNAL ("Widgetizable didn't fill widget with content!"); + } +} diff --git a/src/Widgets/meson.build b/src/Widgets/meson.build index 05d28e5ac..038f55ab5 100644 --- a/src/Widgets/meson.build +++ b/src/Widgets/meson.build @@ -33,6 +33,7 @@ sources += files( 'VoteBox.vala', 'VoteCheckButton.vala', 'Widgetizable.vala', + 'WidgetizableForListView.vala', ) subdir('Admin')