From 669325f8772f50fdf5ec9343a999154b1584efb1 Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Wed, 6 Nov 2024 17:27:59 +0200 Subject: [PATCH] feat(notifications): groups --- src/API/GroupedNotificationsResults.vala | 56 +++++++++++++++++ src/API/Instance.vala | 1 - src/API/Notification.vala | 1 + src/API/meson.build | 1 + src/Dialogs/Preferences.vala | 4 +- src/Services/Accounts/AccountStore.vala | 8 +++ src/Services/Accounts/InstanceAccount.vala | 11 ++-- src/Services/Accounts/SecretAccountStore.vala | 13 ++++ src/Services/Network/Request.vala | 10 +-- src/Views/Notifications.vala | 61 +++++++++++++++++-- src/Widgets/PreviewCardExplore.vala | 2 +- 11 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 src/API/GroupedNotificationsResults.vala diff --git a/src/API/GroupedNotificationsResults.vala b/src/API/GroupedNotificationsResults.vala new file mode 100644 index 000000000..0acb6ad03 --- /dev/null +++ b/src/API/GroupedNotificationsResults.vala @@ -0,0 +1,56 @@ +public class Tuba.API.GroupedNotificationsResults : Entity { + public class NotificationGroup : API.Notification, Widgetizable { + public string most_recent_notification_id { get; set; } + public Gee.ArrayList sample_account_ids { get; set; } + public Gee.ArrayList accounts { get; set; } + public string? status_id { get; set; default = null; } + + public override Type deserialize_array_type (string prop) { + switch (prop) { + case "sample-account-ids": + return Type.STRING; + } + + return base.deserialize_array_type (prop); + } + + public override Gtk.Widget to_widget () { + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + var avi_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + box.append (avi_box); + box.append (base.to_widget ()); + + foreach (var account in accounts) { + avi_box.append (new Widgets.Avatar () { + account = account, + size = 30, + overflow = Gtk.Overflow.HIDDEN, + allow_mini_profile = true + }); + } + + return box; + } + } + + public Gee.ArrayList accounts { get; set; } + public Gee.ArrayList statuses { get; set; } + public Gee.ArrayList notification_groups { get; set; } + + public override Type deserialize_array_type (string prop) { + switch (prop) { + case "accounts": + return typeof (API.Account); + case "statuses": + return typeof (API.Status); + case "notification-groups": + return typeof (NotificationGroup); + } + + return base.deserialize_array_type (prop); + } + + public static GroupedNotificationsResults from (Json.Node node) throws Error { + return Entity.from_json (typeof (API.GroupedNotificationsResults), node) as API.GroupedNotificationsResults; + } +} diff --git a/src/API/Instance.vala b/src/API/Instance.vala index 05ccd02b2..094b29d22 100644 --- a/src/API/Instance.vala +++ b/src/API/Instance.vala @@ -17,7 +17,6 @@ public class Tuba.API.Instance : Entity { public Gee.ArrayList? rules { get; set; } public bool tuba_can_translate { get; set; default=false; } - public int8 tuba_mastodon_version { get; set; default=0; } public override Type deserialize_array_type (string prop) { switch (prop) { diff --git a/src/API/Notification.vala b/src/API/Notification.vala index 2d5b2fd79..22f2effe0 100644 --- a/src/API/Notification.vala +++ b/src/API/Notification.vala @@ -65,6 +65,7 @@ public class Tuba.API.Notification : Entity, Widgetizable { public string? emoji { get; set; default = null; } public string? emoji_url { get; set; default = null; } public API.Admin.Report? report { get; set; default = null; } + public string? group_key { get; set; default = null; } // the docs claim that 'relationship_severance_event' // is the one used but that is not true diff --git a/src/API/meson.build b/src/API/meson.build index 5de4908dc..a93f797a6 100644 --- a/src/API/meson.build +++ b/src/API/meson.build @@ -10,6 +10,7 @@ sources += files( 'EmojiReaction.vala', 'Entity.vala', 'Funkwhale.vala', + 'GroupedNotificationsResults.vala', 'Instance.vala', 'InstanceV2.vala', 'List.vala', diff --git a/src/Dialogs/Preferences.vala b/src/Dialogs/Preferences.vala index 8ad9f0ed9..bb3d30633 100644 --- a/src/Dialogs/Preferences.vala +++ b/src/Dialogs/Preferences.vala @@ -213,7 +213,7 @@ public class Tuba.Dialogs.Preferences : Adw.PreferencesDialog { private Gee.HashMap? notification_filter_policy_status = null; void setup_notification_filters () { - if (!accounts.active.probably_has_notification_filters) return; + if (!accounts.active.tuba_probably_has_notification_filters) return; new Request.GET ("/api/v1/notifications/policy") .with_account (accounts.active) @@ -239,7 +239,7 @@ public class Tuba.Dialogs.Preferences : Adw.PreferencesDialog { }) .on_error ((code, message) => { if (code == 404) { - accounts.active.probably_has_notification_filters = false; + accounts.active.tuba_probably_has_notification_filters = false; } else { warning (@"Error while trying to get notification policy: $code $message"); } diff --git a/src/Services/Accounts/AccountStore.vala b/src/Services/Accounts/AccountStore.vala index 2d600cee7..7c1992b24 100644 --- a/src/Services/Accounts/AccountStore.vala +++ b/src/Services/Accounts/AccountStore.vala @@ -34,6 +34,7 @@ public abstract class Tuba.AccountStore : GLib.Object { public abstract void load () throws GLib.Error; public abstract void save () throws GLib.Error; + public abstract void update_account (InstanceAccount account) throws GLib.Error; public void safe_save () { try { save (); @@ -122,6 +123,13 @@ public abstract class Tuba.AccountStore : GLib.Object { if (account == null) throw new Oopsie.INTERNAL (@"Account $handle has unknown backend: $backend"); + if (obj.has_member ("api-versions")) { + var api_versions = obj.get_object_member ("api-versions"); + if (api_versions != null) { + account.tuba_mastodon_version = (int8) api_versions.get_int_member ("mastodon"); + } + } + if (account.uuid == null || !GLib.Uuid.string_is_valid (account.uuid)) account.uuid = GLib.Uuid.string_random (); return account; } diff --git a/src/Services/Accounts/InstanceAccount.vala b/src/Services/Accounts/InstanceAccount.vala index 483c8f9ca..f7becf533 100644 --- a/src/Services/Accounts/InstanceAccount.vala +++ b/src/Services/Accounts/InstanceAccount.vala @@ -34,7 +34,8 @@ public class Tuba.InstanceAccount : API.Account, Streamable { public string? access_token { get; set; } public bool needs_update { get; set; default=false; } public Error? error { get; set; } //TODO: use this field when server invalidates the auth token - public bool probably_has_notification_filters { get; set; default=false; } + public bool tuba_probably_has_notification_filters { get; set; default=false; } + public int8 tuba_mastodon_version { get; set; default=0; } public GLib.ListStore known_places = new GLib.ListStore (typeof (Place)); @@ -488,15 +489,17 @@ public class Tuba.InstanceAccount : API.Account, Streamable { var node = network.parse_node (parser); if (node == null) return; - this.probably_has_notification_filters = true; + this.tuba_probably_has_notification_filters = true; var instance_v2 = API.InstanceV2.from (node); if (instance_v2 != null) { if (instance_v2.configuration != null && instance_v2.configuration.translation != null) this.instance_info.tuba_can_translate = instance_v2.configuration.translation.enabled; - if (instance_v2.api_versions != null && instance_v2.api_versions.mastodon > 0) - this.instance_info.tuba_mastodon_version = instance_v2.api_versions.mastodon; + if (instance_v2.api_versions != null && instance_v2.api_versions.mastodon > 0 && this.tuba_mastodon_version != instance_v2.api_versions.mastodon) { + this.tuba_mastodon_version = instance_v2.api_versions.mastodon; + accounts.update_account (this); + } } }) .exec (); diff --git a/src/Services/Accounts/SecretAccountStore.vala b/src/Services/Accounts/SecretAccountStore.vala index 6221244b7..6bfedec2e 100644 --- a/src/Services/Accounts/SecretAccountStore.vala +++ b/src/Services/Accounts/SecretAccountStore.vala @@ -101,6 +101,11 @@ public class Tuba.SecretAccountStore : AccountStore { debug (@"Saved $(saved.size) accounts"); } + public override void update_account (InstanceAccount account) throws GLib.Error { + account_to_secret (account); + debug (@"Updated $(account.full_handle)"); + } + public override void remove (InstanceAccount account) throws GLib.Error { base.remove (account); @@ -181,6 +186,14 @@ public class Tuba.SecretAccountStore : AccountStore { builder.set_member_name ("admin-mode"); builder.add_boolean_value (account.admin_mode); + builder.set_member_name ("api-versions"); + builder.begin_object (); + + builder.set_member_name ("mastodon"); + builder.add_int_value (account.tuba_mastodon_version); + + builder.end_object (); + // If display name has emojis it's // better to save and load them // so users don't see their shortcode diff --git a/src/Services/Network/Request.vala b/src/Services/Network/Request.vala index f36ccbbd1..90cdce405 100644 --- a/src/Services/Network/Request.vala +++ b/src/Services/Network/Request.vala @@ -102,13 +102,15 @@ public class Tuba.Request : GLib.Object { public Request with_ctx (Gtk.Widget ctx) { this.has_ctx = true; this.ctx = ctx; - this.ctx.destroy.connect (() => { - this.cancellable.cancel (); - this.ctx = null; - }); + this.ctx.destroy.connect (on_ctx_destroy); return this; } + private void on_ctx_destroy () { + this.cancellable.cancel (); + this.ctx = null; + } + public Request on_error (owned Network.ErrorCallback cb) { this.error_cb = (owned) cb; return this; diff --git a/src/Views/Notifications.vala b/src/Views/Notifications.vala index 0829f6cea..1df182afb 100644 --- a/src/Views/Notifications.vala +++ b/src/Views/Notifications.vala @@ -3,6 +3,7 @@ public class Tuba.Views.Notifications : Views.Timeline, AccountHolder, Streamabl private Binding badge_number_binding; private Binding filtered_notifications_count_binding; private Adw.Banner notifications_filter_banner; + private bool enabled_group_notifications = false; public int32 filtered_notifications { set { @@ -32,7 +33,8 @@ public class Tuba.Views.Notifications : Views.Timeline, AccountHolder, Streamabl } construct { - url = "/api/v1/notifications"; + enabled_group_notifications = accounts.active.tuba_mastodon_version >= 2; + url = @"/api/v$(enabled_group_notifications ? 2 : 1)/notifications"; label = _("Notifications"); icon = "tuba-bell-outline-symbolic"; accepts = typeof (API.Notification); @@ -96,6 +98,7 @@ public class Tuba.Views.Notifications : Views.Timeline, AccountHolder, Streamabl } public override void on_account_changed (InstanceAccount? acc) { + enabled_group_notifications = acc.tuba_mastodon_version >= 2; filters_changed (false); base.on_account_changed (acc); @@ -143,7 +146,7 @@ public class Tuba.Views.Notifications : Views.Timeline, AccountHolder, Streamabl : account.last_read_id ); - if (account.probably_has_notification_filters) + if (account.tuba_probably_has_notification_filters) update_filtered_notifications (); } } @@ -155,6 +158,56 @@ public class Tuba.Views.Notifications : Views.Timeline, AccountHolder, Streamabl } } + public override bool request () { + if (!enabled_group_notifications) return base.request (); + + append_params (new Request.GET (get_req_url ())) + .with_account (account) + .with_ctx (this) + .with_extra_data (Tuba.Network.ExtraData.RESPONSE_HEADERS) + .then ((in_stream, headers) => { + var parser = Network.get_parser_from_inputstream (in_stream); + var node = network.parse_node (parser); + if (node == null) return; + + Object[] to_add = {}; + var group_notifications = API.GroupedNotificationsResults.from (node); + foreach (var group in group_notifications.notification_groups) { + group.kind = null; + + Gee.ArrayList group_accounts = new Gee.ArrayList (); + foreach (var account in group_notifications.accounts) { + if (account.id in group.sample_account_ids) + group_accounts.add (account); + } + group.accounts = group_accounts; + + if (group.status_id != null) { + foreach (var status in group_notifications.statuses) { + if (status.id == group.status_id) { + group.status = status; + break; + } + } + } + + if (!(should_hide (group))) to_add += group; + } + model.splice (model.get_n_items (), 0, to_add); + + if (headers != null) + get_pages (headers.get_one ("Link")); + + if (to_add.length == 0) + on_content_changed (); + on_request_finish (); + }) + .on_error (on_error) + .exec (); + + return GLib.Source.REMOVE; + } + public void update_filtered_notifications () { new Request.GET ("/api/v1/notifications/policy") .with_account (accounts.active) @@ -174,7 +227,7 @@ public class Tuba.Views.Notifications : Views.Timeline, AccountHolder, Streamabl .on_error ((code, message) => { accounts.active.filtered_notifications_count = 0; if (code == 404) { - accounts.active.probably_has_notification_filters = false; + accounts.active.tuba_probably_has_notification_filters = false; } else { warning (@"Error while trying to get notification policy: $code $message"); } @@ -195,7 +248,7 @@ public class Tuba.Views.Notifications : Views.Timeline, AccountHolder, Streamabl } public void filters_changed (bool refresh = true) { - string new_url = "/api/v1/notifications"; + string new_url = @"/api/v$(enabled_group_notifications ? 2 : 1)/notifications"; if (settings.notification_filters.length == 0) { this.is_all = true; diff --git a/src/Widgets/PreviewCardExplore.vala b/src/Widgets/PreviewCardExplore.vala index a3aced07d..37c40beed 100644 --- a/src/Widgets/PreviewCardExplore.vala +++ b/src/Widgets/PreviewCardExplore.vala @@ -91,7 +91,7 @@ public class Tuba.Widgets.PreviewCardExplore : Gtk.ListBoxRow { css_classes = { "caption" } }; - if (accounts.active.instance_info.tuba_mastodon_version >= 1) { + if (accounts.active.tuba_mastodon_version >= 1) { Gtk.Button discussions_button = new Gtk.Button () { child = used_times_label, // translators: tooltip text on 'explore' tab button to