Skip to content

feat(notifications): groups #1193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion data/ui/widgets/status.ui
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
<property name="visible">0</property>
<property name="halign">end</property>
<!-- <property name="margin_bottom">8</property> -->
<property name="icon_name">oops</property>
<property name="icon_size">1</property>
</object>
</child>
Expand Down
79 changes: 79 additions & 0 deletions src/API/GroupedNotificationsResults.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
public class Tuba.API.GroupedNotificationsResults : Entity {
public class NotificationGroup : API.Notification, Widgetizable {
// public int64 most_recent_notification_id { get; set; }
public Gee.ArrayList<string> sample_account_ids { get; set; }
public string? status_id { get; set; default = null; }
public Gee.ArrayList<API.Account> tuba_accounts { get; set; }

public override Type deserialize_array_type (string prop) {
switch (prop) {
case "sample-account-ids":
return Type.STRING;
}

return base.deserialize_array_type (prop);
}

public NotificationGroup.from_notification (API.Notification notification) {
this.patch (notification);
// this.most_recent_notification_id = notification.id;
this.sample_account_ids = new Gee.ArrayList<string>.wrap ({notification.account.id});
if (notification.status != null) this.status_id = notification.status.id;
this.tuba_accounts = new Gee.ArrayList<API.Account>.wrap ({notification.account});
}

public override Gtk.Widget to_widget () {
if (tuba_accounts.size == 1 || group_key.has_prefix ("ungrouped-")) return base.to_widget ();
switch (this.kind) {
case InstanceAccount.KIND_FOLLOW:
case InstanceAccount.KIND_ADMIN_SIGNUP:
return create_basic_card ();
default:
return new Widgets.GroupedNotification (this);
}
}

private Gtk.Widget create_basic_card () {
var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 16) {
margin_top = 8,
margin_bottom = 8,
margin_start = 16,
margin_end = 16
};
Tuba.InstanceAccount.Kind res_kind;
var sub_box = Widgets.GroupedNotification.group_box (this, this.kind, out res_kind);

box.append (new Gtk.Image.from_icon_name (res_kind.icon) {
icon_size = Gtk.IconSize.LARGE
});
box.append (sub_box);

var row = new Widgets.ListBoxRowWrapper () {
child = box,
activatable = false
};
return row;
}
}

public Gee.ArrayList<API.Account> accounts { get; set; }
public Gee.ArrayList<API.Status> statuses { get; set; }
public Gee.ArrayList<NotificationGroup> 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;
}
}
3 changes: 1 addition & 2 deletions src/API/Instance.vala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public class Tuba.API.Instance : Entity {
public Gee.ArrayList<Rule>? rules { get; set; }

public bool tuba_can_translate { get; set; default=false; }
public API.InstanceV2.APIVersions tuba_api_versions { get; set; default= new API.InstanceV2.APIVersions (); }

public override Type deserialize_array_type (string prop) {
switch (prop) {
Expand All @@ -44,7 +43,7 @@ public class Tuba.API.Instance : Entity {
get {
if (pleroma != null && pleroma.metadata != null && pleroma.metadata.features != null) {
return "bubble_timeline" in pleroma.metadata.features;
} else if (tuba_api_versions.chuckya > 0) {
} else if (accounts.active.tuba_api_versions.chuckya > 0) {
return true;
}

Expand Down
5 changes: 5 additions & 0 deletions src/API/InstanceV2.vala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public class Tuba.API.InstanceV2 : Entity {
public class APIVersions : Entity {
public int8 mastodon { get; set; default = 0; }
public int8 chuckya { get; set; default = 0; }

public bool tuba_same (APIVersions new_val) {
return new_val.mastodon == this.mastodon
&& new_val.chuckya == this.chuckya;
}
}

public Configuration configuration { get; set; default = null; }
Expand Down
1 change: 1 addition & 0 deletions src/API/Notification.vala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class Tuba.API.Notification : Entity, Widgetizable {
public string? emoji_url { get; set; default = null; }
public API.Admin.Report? report { get; set; default = null; }
public ModerationWarning? moderation_warning { 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
Expand Down
2 changes: 1 addition & 1 deletion src/API/Status/PreviewCard.vala
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ public class Tuba.API.PreviewCard : Entity, Widgetizable {
API.BookWyrm bookwyrm_obj = API.BookWyrm.from (node);

if (bookwyrm_obj.title != null && bookwyrm_obj.title != "") {
app.main_window.show_book (bookwyrm_obj, this.url);
app.main_window.show_book (bookwyrm_obj);
}
break;
default:
Expand Down
12 changes: 1 addition & 11 deletions src/API/Suggestion.vala
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,6 @@ public class Tuba.API.Suggestion : Entity, Widgetizable {
}

public override Gtk.Widget to_widget () {
try {
return account.to_widget ();
} catch {
return new Gtk.Label (_("Account not found")) {
margin_top = 16,
margin_bottom = 16,
margin_start = 16,
margin_end = 16,
wrap = true
};
}
return account.to_widget ();
}
}
1 change: 1 addition & 0 deletions src/API/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sources += files(
'Entity.vala',
'FamiliarFollowers.vala',
'Funkwhale.vala',
'GroupedNotificationsResults.vala',
'Instance.vala',
'InstanceV2.vala',
'List.vala',
Expand Down
56 changes: 26 additions & 30 deletions src/Dialogs/Admin/Report.vala
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,11 @@ public class Tuba.Dialogs.Admin.Report : Dialogs.Admin.Base {
headerbar.pack_end (take_action_button);
headerbar.pack_start (resolve_button);

try {
Widgets.Account profile = (Widgets.Account) report.target_account.account.to_widget ();
profile.overflow = Gtk.Overflow.HIDDEN;
profile.disable_profile_open = true;
profile.add_css_class ("card");
profile_group.add (profile);
} catch {}
Widgets.Account profile = (Widgets.Account) report.target_account.account.to_widget ();
profile.overflow = Gtk.Overflow.HIDDEN;
profile.disable_profile_open = true;
profile.add_css_class ("card");
profile_group.add (profile);

var info_group = new Adw.PreferencesGroup ();
if (report.target_account.account.created_at != null) {
Expand Down Expand Up @@ -269,29 +267,27 @@ public class Tuba.Dialogs.Admin.Report : Dialogs.Admin.Base {
};

report.statuses.foreach (status => {
try {
status.formal.filtered = null;
status.formal.tuba_spoiler_revealed = true;
if (status.formal.has_media) {
status.formal.media_attachments.foreach (e => {
e.tuba_is_report = true;

return true;
});
}
Widgets.Status widget = (Widgets.Status) status.to_widget ();
widget.add_css_class ("report-status");
widget.add_css_class ("card");
widget.add_css_class ("card-spacing");
widget.actions.visible = false;
widget.menu_button.visible = false;
widget.activatable = false;
widget.filter_stack.can_focus = false;
widget.filter_stack.can_target = false;
widget.filter_stack.focusable = false;

status_group.add (widget);
} catch {}
status.formal.filtered = null;
status.formal.tuba_spoiler_revealed = true;
if (status.formal.has_media) {
status.formal.media_attachments.foreach (e => {
e.tuba_is_report = true;

return true;
});
}
Widgets.Status widget = (Widgets.Status) status.to_widget ();
widget.add_css_class ("report-status");
widget.add_css_class ("card");
widget.add_css_class ("card-spacing");
widget.actions.visible = false;
widget.menu_button.visible = false;
widget.activatable = false;
widget.filter_stack.can_focus = false;
widget.filter_stack.can_target = false;
widget.filter_stack.focusable = false;

status_group.add (widget);

return true;
});
Expand Down
17 changes: 8 additions & 9 deletions src/Dialogs/AnnualReport.vala
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ public class Tuba.Dialogs.AnnualReport : Adw.Dialog {

status.formal.filtered = null;
status.formal.tuba_spoiler_revealed = true;
try {
var widg = status.to_widget () as Widgets.Status;
widg.actions.visible = false;
widg.menu_button.visible = false;
widg.activatable = false;
widg.filter_stack.can_focus = false;
widg.filter_stack.can_target = false;
widg.filter_stack.focusable = false;

var widg = status.to_widget () as Widgets.Status;
widg.actions.visible = false;
widg.menu_button.visible = false;
widg.activatable = false;
widg.filter_stack.can_focus = false;
widg.filter_stack.can_target = false;
widg.filter_stack.focusable = false;

this.child = widg;
} catch {}

this.clicked.connect (on_clicked);
}
Expand Down
59 changes: 27 additions & 32 deletions src/Dialogs/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -131,38 +131,33 @@ public class Tuba.Dialogs.MainWindow: Adw.ApplicationWindow, Saveable {
}

public void show_book (API.BookWyrm book, string? fallback = null) {
try {
var book_widget = book.to_widget ();
var clamp = new Adw.Clamp () {
child = book_widget,
tightening_threshold = 100,
valign = Gtk.Align.START
};
var scroller = new Gtk.ScrolledWindow () {
hexpand = true,
vexpand = true
};
scroller.child = clamp;

var toolbar_view = new Adw.ToolbarView ();
var headerbar = new Adw.HeaderBar ();

toolbar_view.add_top_bar (headerbar);
toolbar_view.set_content (scroller);

var book_dialog = new Adw.Dialog () {
title = book.title,
child = toolbar_view,
content_width = 460,
content_height = 640
};

book_dialog.present (this);

((Widgets.BookWyrmPage) book_widget).selectable = true;
} catch {
if (fallback != null) Utils.Host.open_url.begin (fallback);
}
var book_widget = book.to_widget ();
var clamp = new Adw.Clamp () {
child = book_widget,
tightening_threshold = 100,
valign = Gtk.Align.START
};
var scroller = new Gtk.ScrolledWindow () {
hexpand = true,
vexpand = true
};
scroller.child = clamp;

var toolbar_view = new Adw.ToolbarView ();
var headerbar = new Adw.HeaderBar ();

toolbar_view.add_top_bar (headerbar);
toolbar_view.set_content (scroller);

var book_dialog = new Adw.Dialog () {
title = book.title,
child = toolbar_view,
content_width = 460,
content_height = 640
};

book_dialog.present (this);
((Widgets.BookWyrmPage) book_widget).selectable = true;
}

public Views.Base open_view (Views.Base view) {
Expand Down
4 changes: 2 additions & 2 deletions src/Dialogs/NotificationSettings.vala
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public class Tuba.Dialogs.NotificationSettings : Adw.Dialog {

private Gee.HashMap<Adw.SwitchRow, bool>? 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)
Expand All @@ -186,7 +186,7 @@ public class Tuba.Dialogs.NotificationSettings : Adw.Dialog {
})
.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");
}
Expand Down
9 changes: 9 additions & 0 deletions src/Services/Accounts/AccountStore.vala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public abstract class Tuba.AccountStore : GLib.Object {
ensure_active_account ();
}

public abstract void update_account (InstanceAccount account) throws GLib.Error;
public abstract void load () throws GLib.Error;
public abstract void save () throws GLib.Error;
// public void safe_save () {
Expand Down Expand Up @@ -122,6 +123,14 @@ 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) {
if (api_versions.has_member ("mastodon")) account.tuba_api_versions.mastodon = (int8) api_versions.get_int_member ("mastodon");
if (api_versions.has_member ("chuckya")) account.tuba_api_versions.chuckya = (int8) api_versions.get_int_member ("chuckya");
}
}

if (account.uuid == null || !GLib.Uuid.string_is_valid (account.uuid)) account.uuid = GLib.Uuid.string_random ();
return account;
}
Expand Down
12 changes: 8 additions & 4 deletions src/Services/Accounts/InstanceAccount.vala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,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 API.InstanceV2.APIVersions tuba_api_versions { get; set; default= new API.InstanceV2.APIVersions (); }

public GLib.ListStore known_places = new GLib.ListStore (typeof (Place));
public GLib.ListStore list_places = new GLib.ListStore (typeof (Place));
Expand Down Expand Up @@ -532,10 +533,13 @@ public class Tuba.InstanceAccount : API.Account, Streamable {
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_api_versions = instance_v2.api_versions;
this.probably_has_notification_filters = true;
if (!this.tuba_api_versions.tuba_same (instance_v2.api_versions)) {
this.tuba_api_versions = instance_v2.api_versions;
accounts.update_account (this);
}
this.tuba_probably_has_notification_filters = true;

if (this.instance_info.tuba_api_versions.mastodon > 1) gather_annual_report ();
if (this.tuba_api_versions.mastodon > 1) gather_annual_report ();
}
}

Expand Down
Loading