diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f828ebc3..f672f44e 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 26ff18b7..fff35a3a 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", diff --git a/data/style.css b/data/style.css index 5fd22944..56bde378 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); @@ -351,15 +346,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 a32ea475..e672b6bd 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/API/Conversation.vala b/src/API/Conversation.vala index d4825e8c..f6fca9c0 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 e4ff9611..65d999fd 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/Composer/AttachmentsPage.vala b/src/Dialogs/Composer/AttachmentsPage.vala index 42f92a0f..589bc171 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; } diff --git a/src/Dialogs/Report.vala b/src/Dialogs/Report.vala index 8cb9e09f..ee70a417 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 c25c09c7..64d4aa66 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 533156c1..ac6ec93d 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 00000000..3f92b5da --- /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 a7db20ae..f0511464 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 d9d97475..b0efd02b 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 62fa48ac..d069cc31 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 d8b39c88..2f4361f8 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 24aea087..ac28c31d 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 13f93f7f..44a67861 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 93ad097d..49eac94a 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 17c21842..99d007b4 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 9645931f..f960f9e3 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 4872191f..ce1dd899 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 e213a8c5..c3b2721c 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 68ff863c..9ec70871 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 { @@ -106,8 +102,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; @@ -218,12 +212,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 +245,7 @@ } } } + ~Status () { debug ("Destroying Status widget"); if (context_menu != null) { @@ -1286,27 +1289,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 (); } } diff --git a/src/Widgets/WidgetizableForListView.vala b/src/Widgets/WidgetizableForListView.vala new file mode 100644 index 00000000..60891495 --- /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 05d28e5a..038f55ab 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')