From dc9981f16a127a0c81decc086a3ef0ef2e21f2bc Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Mon, 25 Nov 2024 01:38:30 +0200 Subject: [PATCH] feat(profile): refresh user when appropriate (#1174) --- src/Views/Profile.vala | 41 +++- src/Views/Timeline.vala | 9 +- src/Widgets/Account.vala | 2 +- src/Widgets/ProfileCover.vala | 339 ++++++++++++++++------------- src/Widgets/Status/ActionsRow.vala | 2 +- 5 files changed, 231 insertions(+), 162 deletions(-) diff --git a/src/Views/Profile.vala b/src/Views/Profile.vala index dcd978d18..d8b510c06 100644 --- a/src/Views/Profile.vala +++ b/src/Views/Profile.vala @@ -7,6 +7,25 @@ public class Tuba.Views.Profile : Views.Accounts { Object (account: t_acc, rs: new API.Relationship.for_account (t_acc)); } + public async bool update_profile () { + Request req = new Request.GET (@"/api/v1/accounts/$(account.id)").with_account (accounts.active); + + try { + yield req.await (); + var parser = Network.get_parser_from_inputstream (req.response_body); + var node = network.parse_node (parser); + var updated = API.Account.from (node); + account.patch (updated); + + return true; + } catch (Error e) { + warning (@"Couldn't update account $(account.id): $(e.message)"); + app.toast (e.message); + } + + return false; + } + public override Gtk.Widget to_widget () { return new Widgets.Cover (this); } @@ -29,6 +48,7 @@ public class Tuba.Views.Profile : Views.Accounts { public ProfileAccount profile { get; construct set; } public Widgets.ProfileFilterGroup.Filter filter { get; set; default = Widgets.ProfileFilterGroup.Filter.POSTS; } public string source { get; set; default = "statuses"; } + private signal void cover_profile_update (API.Account acc); protected Gtk.MenuButton menu_button; protected SimpleAction muting_action; @@ -52,6 +72,8 @@ public class Tuba.Views.Profile : Views.Accounts { model.insert (0, profile); model.insert (1, filter_group); profile.rs.invalidated.connect (on_rs_updated); + + if (acc.is_self ()) update_profile_cover (); } ~Profile () { debug ("Destroying Profile view"); @@ -100,6 +122,7 @@ public class Tuba.Views.Profile : Views.Accounts { widget_cover.aria_updated.connect (on_cover_aria_update); widget_cover.remove_css_class ("card"); widget_cover.remove_css_class ("card-spacing"); + this.cover_profile_update.connect (widget_cover.update_cover_from_profile); var row = new Gtk.ListBoxRow () { focusable = true, @@ -142,6 +165,11 @@ public class Tuba.Views.Profile : Views.Accounts { GLib.Idle.add (append_pinned); } + public override void on_manual_refresh () { + update_profile_cover (); + base.on_manual_refresh (); + } + protected void change_timeline_source (string t_source) { source = t_source; @@ -204,8 +232,6 @@ public class Tuba.Views.Profile : Views.Accounts { private void on_edit_save () { if (profile.account.is_self ()) { - model.remove (0); - // for (uint i = 0; i < model.get_n_items (); i++) { // var status_obj = (API.Status)model.get_item (i); // if (status_obj.formal.account.id == profile.account.id) { @@ -213,11 +239,18 @@ public class Tuba.Views.Profile : Views.Accounts { // } // } - model.insert (0, new ProfileAccount (accounts.active)); - on_refresh (); + this.cover_profile_update (accounts.active); } } + private void update_profile_cover () { + profile.update_profile.begin ((obj, res) => { + if (profile.update_profile.end (res)) { + this.cover_profile_update (profile.account); + } + }); + } + protected override void clear () { base.clear_all_but_first (2); } diff --git a/src/Views/Timeline.vala b/src/Views/Timeline.vala index 12cec9066..6260a67f8 100644 --- a/src/Views/Timeline.vala +++ b/src/Views/Timeline.vala @@ -63,7 +63,7 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase private void on_drag_end (double x, double y) { if (scrolled.vadjustment.value == 0.0 && pull_to_refresh_spinner.margin_top >= 125) { - on_refresh (); + on_manual_refresh (); } is_pulling = false; @@ -86,8 +86,8 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase reached_close_to_top.connect (finish_queue); #endif - app.refresh.connect (on_refresh); - status_button.clicked.connect (on_refresh); + app.refresh.connect (on_manual_refresh); + status_button.clicked.connect (on_manual_refresh); construct_account_holder (); @@ -249,6 +249,9 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase GLib.Idle.add (request); } + public virtual void on_manual_refresh () { + on_refresh (); + } protected virtual void on_account_changed (InstanceAccount? acc) { account = acc; diff --git a/src/Widgets/Account.vala b/src/Widgets/Account.vala index 026e1f143..dbcbed227 100644 --- a/src/Widgets/Account.vala +++ b/src/Widgets/Account.vala @@ -234,7 +234,7 @@ public class Tuba.Widgets.Account : Gtk.ListBoxRow { } private void on_tuba_rs () { - if (api_account != null) + if (api_account != null && api_account.tuba_rs != null) rs = api_account.tuba_rs; } diff --git a/src/Widgets/ProfileCover.vala b/src/Widgets/ProfileCover.vala index 860fbc6d3..27c7ec7eb 100644 --- a/src/Widgets/ProfileCover.vala +++ b/src/Widgets/ProfileCover.vala @@ -13,7 +13,7 @@ protected class Tuba.Widgets.Cover : Gtk.Box { [GtkChild] unowned Gtk.Label cover_badge; [GtkChild] unowned Gtk.Image cover_bot_badge; [GtkChild] unowned Gtk.Box cover_badge_box; - [GtkChild] public unowned Gtk.ListBox info; + [GtkChild] unowned Gtk.ListBox info; [GtkChild] unowned Widgets.EmojiLabel display_name; [GtkChild] unowned Gtk.Label handle; [GtkChild] unowned Widgets.Avatar avatar; @@ -129,13 +129,11 @@ protected class Tuba.Widgets.Cover : Gtk.Box { (ulong) total_fields ).printf (total_fields); - if (fields_box_row != null) { - this.fields_box_row.update_property ( - Gtk.AccessibleProperty.LABEL, - aria_fileds, - -1 - ); - } + this.fields_box_row.update_property ( + Gtk.AccessibleProperty.LABEL, + aria_fileds, + -1 + ); } string final_aria = "%s %s".printf ( @@ -187,19 +185,7 @@ protected class Tuba.Widgets.Cover : Gtk.Box { _mini = mini; if (mini) { note_row.sensitive = false; - } else if (profile.account.moved != null) { - moved_btn.visible = true; - moved_btn.child = new Gtk.Label ( - // translators: Button label shown when a user has moved to another instance. - // The first variable is this account's handle while the second - // is the moved-to account's handle - _("%s has moved to %s").printf (@"$(profile.account.full_handle)", @"$(profile.account.moved.full_handle)") - ) { - use_markup = true, - wrap = true, - wrap_mode = Pango.WrapMode.WORD_CHAR - }; - moved_to_account = profile.account.moved; + } else { moved_btn.clicked.connect (on_moved_btn_clicked); } @@ -208,19 +194,113 @@ protected class Tuba.Widgets.Cover : Gtk.Box { this.add_css_class ("thanks"); } - display_name.instance_emojis = profile.account.emojis_map; - display_name.content = profile.account.display_name; - handle.label = profile.account.handle; - avatar.account = profile.account; - note.instance_emojis = profile.account.emojis_map; - note.content = profile.account.note; - cover_bot_badge.visible = profile.account.bot; + if (profile.account.id != accounts.active.id) { + note_entry_row.notify["text"].connect (on_note_changed); + profile.rs.invalidated.connect (on_rs_invalidation); + rsbtn.handle = profile.account.handle; + rsbtn.rs = profile.rs; + rsbtn.visible = true; + } + + if (mini) { + avatar.clicked.connect (on_avatar_clicked); + } else { + avatar.clicked.connect (open_pfp_in_media_viewer); + } + + fields_box = new Gtk.FlowBox () { + max_children_per_line = app.is_mobile ? 1 : 2, + min_children_per_line = 1, + selection_mode = Gtk.SelectionMode.NONE, + css_classes = {"ttl-profile-fields-box"} + }; + fields_box_row = new Gtk.ListBoxRow () { + child = fields_box, + activatable = false, + css_classes = {"ttl-profile-fields-box-container"} + }; + info.append (fields_box_row); + + if (!mini) { + build_profile_stats (profile.account); + } else { + background.height_request = 64; + + // translators: Used in profile stats. + // The variable is a shortened number of the amount of posts a user has made. + string posts_str = GLib.ngettext ( + "%s Post", + "%s Posts", + (ulong) profile.account.statuses_count + ).printf (@"$(Tuba.Units.shorten (profile.account.statuses_count))"); + + // translators: Used in profile stats. + // The variable is a shortened number of the amount of followers a user has. + string followers_str = GLib.ngettext ( + "%s Follower", + "%s Followers", + (ulong) profile.account.statuses_count + ).printf (@"$(Tuba.Units.shorten (profile.account.followers_count))"); + + stats_string = "%s %s %s".printf ( + posts_str, + // translators: Used in profile stats. + // The variable is a shortened number of the amount of people a user follows. + _("%s Following").printf (@"$(Tuba.Units.shorten (profile.account.following_count))"), + followers_str + ); + + info.append ( + new Gtk.ListBoxRow () { + activatable = false, + child = new Gtk.Label (stats_string) { + wrap = true, + wrap_mode = Pango.WrapMode.WORD_CHAR, + hexpand = true, + xalign = 0.0f, + use_markup = true, + css_classes = {"account-stats"}, + valign = Gtk.Align.CENTER, + margin_start = 12, + margin_end = 12, + margin_top = 6, + margin_bottom = 6, + } + } + ); + } + update_cover_from_profile (profile.account); + + if (header_url != "" && !mini) + background.clicked.connect (open_header_in_media_viewer); + + app.notify["is-mobile"].connect (update_fields_max_columns); + } + + public void update_cover_from_profile (API.Account profile) { + handle.label = profile.handle; + + if (display_name.content != profile.display_name) { + display_name.instance_emojis = profile.emojis_map; + display_name.content = profile.display_name; + } + + avi_url = profile.avatar ?? ""; + avatar.account = profile; + + if (note.content != profile.note) { + note.instance_emojis = profile.emojis_map; + note.content = profile.note; + } + + cover_bot_badge.visible = profile.bot; update_cover_badge (); - if (profile.account.roles != null && profile.account.roles.size > 0) { + roles.remove_all (); + if (profile.roles != null && profile.roles.size > 0) { roles.visible = true; - foreach (API.AccountRole role in profile.account.roles) { + foreach (API.AccountRole role in profile.roles) { roles.append ( new Gtk.FlowBoxChild () { child = role.to_widget (), @@ -230,43 +310,38 @@ protected class Tuba.Widgets.Cover : Gtk.Box { } } - if (profile.account.id != accounts.active.id) { - note_entry_row.notify["text"].connect (on_note_changed); - profile.rs.invalidated.connect (on_rs_invalidation); - rsbtn.handle = profile.account.handle; - rsbtn.rs = profile.rs; - rsbtn.visible = true; - } - - if (profile.account.header.contains ("/headers/original/missing.png")) { + if (profile.header.contains ("/headers/original/missing.png")) { header_url = ""; background.paintable = avatar.custom_image; } else { - header_url = profile.account.header ?? ""; - Tuba.Helper.Image.request_paintable (profile.account.header, null, false, on_cache_response); - - if (!mini) - background.clicked.connect (open_header_in_media_viewer); + header_url = profile.header ?? ""; + Tuba.Helper.Image.request_paintable (profile.header, null, false, on_cache_response); } - avi_url = profile.account.avatar ?? ""; - if (mini) { - avatar.clicked.connect (on_avatar_clicked); + if (!_mini && profile.moved != null) { + moved_btn.visible = true; + moved_btn.child = new Gtk.Label ( + // translators: Button label shown when a user has moved to another instance. + // The first variable is this account's handle while the second + // is the moved-to account's handle + _("%s has moved to %s").printf (@"$(profile.full_handle)", @"$(profile.moved.full_handle)") + ) { + use_markup = true, + wrap = true, + wrap_mode = Pango.WrapMode.WORD_CHAR + }; + moved_to_account = profile.moved; } else { - avatar.clicked.connect (open_pfp_in_media_viewer); + moved_btn.visible = false; } - if (profile.account.fields != null || profile.account.created_at != null) { - fields_box = new Gtk.FlowBox () { - max_children_per_line = app.is_mobile ? 1 : 2, - min_children_per_line = 1, - selection_mode = Gtk.SelectionMode.NONE, - css_classes = {"ttl-profile-fields-box"} - }; + fields_box.remove_all (); + total_fields = 0; + if (profile.fields != null || profile.created_at != null) { var sizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL); - total_fields = profile.account.fields.size; + total_fields = profile.fields.size; - foreach (API.AccountField f in profile.account.fields) { + foreach (API.AccountField f in profile.fields) { var row = new Gtk.Box (Gtk.Orientation.VERTICAL, 6) { css_classes = {"ttl-profile-field"} }; @@ -280,7 +355,7 @@ protected class Tuba.Widgets.Cover : Gtk.Box { use_markup = false, css_classes = {"dim-label"} }; - title_label.instance_emojis = profile.account.emojis_map; + title_label.instance_emojis = profile.emojis_map; title_label.content = f.name; fields_box.append (row); @@ -304,12 +379,12 @@ protected class Tuba.Widgets.Cover : Gtk.Box { row.append (val); } - if (profile.account.created_at != null) { + if (profile.created_at != null) { total_fields += 1; var row = new Gtk.Box (Gtk.Orientation.VERTICAL, 6) { css_classes = {"ttl-profile-field"} }; - var parsed_date = new GLib.DateTime.from_iso8601 (profile.account.created_at, null); + var parsed_date = new GLib.DateTime.from_iso8601 (profile.created_at, null); parsed_date = parsed_date.to_timezone (new TimeZone.local ()); var date_local = _("%B %e, %Y"); @@ -320,7 +395,7 @@ protected class Tuba.Widgets.Cover : Gtk.Box { tooltip_text = parsed_date.format ("%F") }; - var creation_date_time = new GLib.DateTime.from_iso8601 (profile.account.created_at, null); + var creation_date_time = new GLib.DateTime.from_iso8601 (profile.created_at, null); var today_date_time = new GLib.DateTime.now_local (); bool is_birthday = creation_date_time.get_month () == today_date_time.get_month () && creation_date_time.get_day_of_month () == today_date_time.get_day_of_month (); @@ -351,11 +426,8 @@ protected class Tuba.Widgets.Cover : Gtk.Box { sizegroup.add_widget (row); } - fields_box_row = new Gtk.ListBoxRow () { - child = fields_box, - activatable = false, - css_classes = {"ttl-profile-fields-box-container"} - }; + fields_box_row.remove_css_class ("odd"); + fields_box_row.remove_css_class ("signle"); if (total_fields % 2 != 0) { fields_box_row.add_css_class ("odd"); @@ -364,58 +436,31 @@ protected class Tuba.Widgets.Cover : Gtk.Box { if (total_fields == 1) { fields_box_row.add_css_class ("single"); } - - info.append (fields_box_row); - app.notify["is-mobile"].connect (update_fields_max_columns); } - if (!mini) { - build_profile_stats (profile.account); - } else { - background.height_request = 64; - + if (posts_btn != null && following_btn != null && followers_btn != null) { // translators: Used in profile stats. // The variable is a shortened number of the amount of posts a user has made. - string posts_str = GLib.ngettext ( + posts_btn.label_template = GLib.ngettext ( "%s Post", "%s Posts", - (ulong) profile.account.statuses_count - ).printf (@"$(Tuba.Units.shorten (profile.account.statuses_count))"); + (ulong) profile.statuses_count + ); + posts_btn.amount = profile.statuses_count; + + // translators: Used in profile stats. + // The variable is a shortened number of the amount of people a user follows. + following_btn.label_template = _("%s Following"); + following_btn.amount = profile.following_count; // translators: Used in profile stats. // The variable is a shortened number of the amount of followers a user has. - string followers_str = GLib.ngettext ( + followers_btn.label_template = GLib.ngettext ( "%s Follower", "%s Followers", - (ulong) profile.account.statuses_count - ).printf (@"$(Tuba.Units.shorten (profile.account.followers_count))"); - - stats_string = "%s %s %s".printf ( - posts_str, - // translators: Used in profile stats. - // The variable is a shortened number of the amount of people a user follows. - _("%s Following").printf (@"$(Tuba.Units.shorten (profile.account.following_count))"), - followers_str - ); - - info.append ( - new Gtk.ListBoxRow () { - activatable = false, - child = new Gtk.Label (stats_string) { - wrap = true, - wrap_mode = Pango.WrapMode.WORD_CHAR, - hexpand = true, - xalign = 0.0f, - use_markup = true, - css_classes = {"account-stats"}, - valign = Gtk.Align.CENTER, - margin_start = 12, - margin_end = 12, - margin_top = 6, - margin_bottom = 6, - } - } + (ulong) profile.followers_count ); + followers_btn.amount = profile.followers_count; } update_aria (); @@ -440,53 +485,54 @@ protected class Tuba.Widgets.Cover : Gtk.Box { if (moved_to_account != null) moved_to_account.open (); } + protected class ProfileStatsButton : Gtk.Button { + public string label_template { get; set; default = "%s"; } + public int64 amount { + set { + this.label = label_template.printf (Tuba.Units.shorten (value)); + this.tooltip_text = label_template.printf (value.to_string ()); + } + } + + construct { + this.css_classes = { "flat", "ttl-profile-stat-button" }; + this.hexpand = true; + this.label = ""; + + var child_label = this.child as Gtk.Label; + child_label.wrap = true; + child_label.justify = Gtk.Justification.CENTER; + } + } + + ProfileStatsButton? posts_btn; + ProfileStatsButton? followers_btn; + ProfileStatsButton? following_btn; protected void build_profile_stats (API.Account account) { var row = new Gtk.ListBoxRow (); var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); var sizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL); - // translators: Used in profile stats. - // The variable is a shortened number of the amount of posts a user has made. - string posts_str = GLib.ngettext ( - "%s Post", - "%s Posts", - (ulong) account.statuses_count - ); - - // translators: Used in profile stats. - // The variable is a shortened number of the amount of followers a user has. - string followers_str = GLib.ngettext ( - "%s Follower", - "%s Followers", - (ulong) account.statuses_count - ); - - var btn = build_profile_stats_button (posts_str.printf (Tuba.Units.shorten (account.statuses_count))); - btn.tooltip_text = posts_str.printf (account.statuses_count.to_string ()); - btn.clicked.connect (() => timeline_change ("statuses")); - sizegroup.add_widget (btn); - box.append (btn); + posts_btn = new ProfileStatsButton (); + posts_btn.clicked.connect (() => timeline_change ("statuses")); + sizegroup.add_widget (posts_btn); + box.append (posts_btn); var separator = new Gtk.Separator (Gtk.Orientation.VERTICAL); box.append (separator); - // translators: Used in profile stats. - // The variable is a shortened number of the amount of people a user follows. - btn = build_profile_stats_button (_("%s Following").printf (Tuba.Units.shorten (account.following_count))); - btn.tooltip_text = _("%s Following").printf (account.following_count.to_string ()); - btn.clicked.connect (() => timeline_change ("following")); - sizegroup.add_widget (btn); - box.append (btn); + following_btn = new ProfileStatsButton (); + following_btn.clicked.connect (() => timeline_change ("following")); + sizegroup.add_widget (following_btn); + box.append (following_btn); separator = new Gtk.Separator (Gtk.Orientation.VERTICAL); box.append (separator); - // translators: the variable is the amount of followers a user has - btn = build_profile_stats_button (followers_str.printf (Tuba.Units.shorten (account.followers_count))); - btn.tooltip_text = followers_str.printf (account.followers_count.to_string ()); - btn.clicked.connect (() => timeline_change ("followers")); - sizegroup.add_widget (btn); - box.append (btn); + followers_btn = new ProfileStatsButton (); + followers_btn.clicked.connect (() => timeline_change ("followers")); + sizegroup.add_widget (followers_btn); + box.append (followers_btn); row.activatable = false; row.focusable = false; @@ -494,19 +540,6 @@ protected class Tuba.Widgets.Cover : Gtk.Box { info.append (row); } - protected Gtk.Button build_profile_stats_button (string btn_label) { - var btn = new Gtk.Button.with_label (btn_label) { - css_classes = { "flat", "ttl-profile-stat-button" }, - hexpand = true - }; - - var child_label = btn.child as Gtk.Label; - child_label.wrap = true; - child_label.justify = Gtk.Justification.CENTER; - - return btn; - } - void on_cache_response (Gdk.Paintable? data) { background.paintable = data; } diff --git a/src/Widgets/Status/ActionsRow.vala b/src/Widgets/Status/ActionsRow.vala index 79ce35a36..f34c1fba9 100644 --- a/src/Widgets/Status/ActionsRow.vala +++ b/src/Widgets/Status/ActionsRow.vala @@ -117,7 +117,7 @@ public class Tuba.Widgets.ActionsRow : Gtk.Box { reblog_button.clicked.connect (on_boost_button_clicked); this.append (reblog_button); - if (accounts.active.instance_info.supports_quote_posting) { + if (accounts.active.instance_info != null && accounts.active.instance_info.supports_quote_posting) { quote_button = new StatusActionButton.with_icon_name ("tuba-quotation-symbolic") { css_classes = { "ttl-status-action-quote", "flat", "circular" }, halign = Gtk.Align.START,