From 762884325dcd1c7c5df3ba778efe8e68a47196b1 Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Thu, 12 Sep 2024 19:05:20 +0300 Subject: [PATCH] feat(PreviewCard): verified authors (#1112) --- data/style.css | 13 +++ data/ui/widgets/preview_card.ui | 161 ++++++++++++++-------------- src/API/Status/PreviewCard.vala | 9 ++ src/Widgets/PreviewCard.vala | 182 ++++++++++++++++++++++++++++++-- src/Widgets/RichLabel.vala | 7 +- src/Widgets/Status.vala | 6 +- 6 files changed, 290 insertions(+), 88 deletions(-) diff --git a/data/style.css b/data/style.css index 554bdb18c..093ca62b9 100644 --- a/data/style.css +++ b/data/style.css @@ -552,6 +552,14 @@ video > overlay > revealer > controls, .audio-controls { border-radius: 0px; } +.ttl-view .small .preview_card.explore { + border-radius: 0px; +} + +.ttl-view .preview_card.explore { + border-radius: 12px; +} + /* .ttl-view .small.content .card.card-spacing { */ .ttl-view .small.fake-content .card.card-spacing { margin: 0px; @@ -734,3 +742,8 @@ popover.mini-profile > contents { border-radius: 9999px; padding: 4px; } + +.verified-author { + padding: 0 5px; + font-size: small; +} diff --git a/data/ui/widgets/preview_card.ui b/data/ui/widgets/preview_card.ui index a8843755f..901649fbd 100644 --- a/data/ui/widgets/preview_card.ui +++ b/data/ui/widgets/preview_card.ui @@ -1,80 +1,87 @@ - - + + diff --git a/src/API/Status/PreviewCard.vala b/src/API/Status/PreviewCard.vala index 047cc4488..f1ecabba9 100644 --- a/src/API/Status/PreviewCard.vala +++ b/src/API/Status/PreviewCard.vala @@ -95,12 +95,19 @@ public class Tuba.API.PreviewCard : Entity, Widgetizable { } } + public class AuthorEntity : Entity { + public string? name { get; set; } + public string? url { get; set; } + public API.Account? account { get; set; } + } + public string url { get; set; } public string title { get; set; default=""; } public string description { get; set; default=""; } public string kind { get; set; default="link"; } public string author_name { get; set; default=""; } public string author_url { get; set; default=""; } + public Gee.ArrayList? authors { get; set; default=null; } public string provider_name { get; set; default=""; } public string provider_url { get; set; default=""; } public string? image { get; set; default=null; } @@ -124,6 +131,8 @@ public class Tuba.API.PreviewCard : Entity, Widgetizable { switch (prop) { case "history": return typeof (API.TagHistory); + case "authors": + return typeof (AuthorEntity); } return base.deserialize_array_type (prop); diff --git a/src/Widgets/PreviewCard.vala b/src/Widgets/PreviewCard.vala index 96280bd29..393e2384a 100644 --- a/src/Widgets/PreviewCard.vala +++ b/src/Widgets/PreviewCard.vala @@ -1,15 +1,20 @@ [GtkTemplate (ui = "/dev/geopjr/Tuba/ui/widgets/preview_card.ui")] -public class Tuba.Widgets.PreviewCard : Gtk.Button { - construct { - this.css_classes = {"preview_card", "flat"}; +public class Tuba.Widgets.PreviewCard : Gtk.Box { + ~PreviewCard () { + debug ("Destroying PreviewCard"); } + [GtkChild] public unowned Gtk.Button button; [GtkChild] unowned Gtk.Box box; [GtkChild] unowned Gtk.Label author_label; [GtkChild] unowned Gtk.Label title_label; [GtkChild] unowned Gtk.Label description_label; [GtkChild] unowned Gtk.Label used_times_label; + private Gee.ArrayList? verified_authors = null; + private string? author_url = null; + private API.Account? author_account = null; + public PreviewCard (API.PreviewCard card_obj) { var is_video = card_obj.kind == "video"; @@ -71,7 +76,7 @@ public class Tuba.Widgets.PreviewCard : Gtk.Button { author_label.label = author; if (card_obj.title != "") { - title_label.label = title_label.tooltip_text = card_obj.title.strip (); + title_label.label = title_label.tooltip_text = card_obj.title.replace ("\n", " ").strip (); title_label.visible = true; } @@ -87,9 +92,9 @@ public class Tuba.Widgets.PreviewCard : Gtk.Button { image_widget.add_css_class ("preview_card_h"); image_widget.remove_css_class ("preview_card_v"); - this.add_css_class ("explore"); - - this.clicked.connect (() => Host.open_url (card_obj.url)); + button.add_css_class ("explore"); + button.remove_css_class ("frame"); + button.clicked.connect (() => Host.open_url (card_obj.url)); if (description_label.visible) { if (description_label.label.length > 109) @@ -118,6 +123,169 @@ public class Tuba.Widgets.PreviewCard : Gtk.Button { used_times_label.label = subtitle; used_times_label.visible = true; + } else if (card_obj.authors != null && card_obj.authors.size > 0) { + bool should_add = true; + + Gtk.Widget more_from_button = new Gtk.Button () { + halign = Gtk.Align.START, + css_classes = { "flat", "verified-author" } + }; + + if (card_obj.authors.size == 1) { + var verified_author = card_obj.authors.get (0); + if (verified_author.account != null) { + // translators: the variable is a user name. This is shown on + // preview cards of articles from 'verified' fedi authors. + // By + // (As in, 'written by ') + ((Gtk.Button) more_from_button).child = new Widgets.RichLabel.with_emojis (_("By %s").printf (verified_author.account.display_name), verified_author.account.emojis_map) { + use_markup = false, + xalign = 0.0f + }; + author_account = verified_author.account; + + ((Gtk.Button) more_from_button).clicked.connect (open_author); + } else if (verified_author.name != null && verified_author.name != "") { + var verified_author_label = new Gtk.Label (_("By %s").printf (verified_author.name)) { + xalign = 0.0f, + wrap = true, + wrap_mode = Pango.WrapMode.WORD_CHAR + }; + + if (verified_author.url == null || verified_author.url == "") { + more_from_button = verified_author_label; + more_from_button.add_css_class ("font-bold"); + more_from_button.add_css_class ("verified-author"); + } else { + author_url = verified_author.url; + ((Gtk.Button) more_from_button).child = verified_author_label; + ((Gtk.Button) more_from_button).clicked.connect (open_author); + } + } else { + should_add = false; + } + } else { + ((Gtk.Button) more_from_button).child = new Gtk.Label ( + // translators: the variable is a number. This is shown on + // preview cards of articles from 'verified' fedi authors, + // when there's more than 1. + // Sell all authors + GLib.ngettext ("See %d author", "See all %d authors", (ulong) card_obj.authors.size).printf (card_obj.authors.size) + ) { + xalign = 0.0f, + wrap = true, + wrap_mode = Pango.WrapMode.WORD_CHAR, + css_classes = { "font-bold" } + }; + verified_authors = card_obj.authors; + + ((Gtk.Button) more_from_button).clicked.connect (open_authors); + } + + if (should_add) this.append (more_from_button); + } + } + + private void open_author () { + if (author_account != null) { + author_account.open (); + } else if (author_url != null && author_url != "") { + Host.open_url (author_url); + } + } + + private class AuthorRow : Gtk.ListBoxRow { + string? callback_url = null; + API.Account? callback_account = null; + + public AuthorRow (API.PreviewCard.AuthorEntity author_entity) { + if (author_entity.account != null) { + var widget = new Widgets.EmojiLabel () { + use_markup = false, + margin_top = 8, + margin_start = 8, + margin_end = 8, + margin_bottom = 8 + }; + widget.instance_emojis = author_entity.account.emojis_map; + widget.content = author_entity.account.display_name; + this.child = widget; + + callback_account = author_entity.account; + this.activatable = callback_account != null; + } else { + var widget = new Gtk.Label (author_entity.name) { + xalign = 0.0f, + wrap = true, + wrap_mode = Pango.WrapMode.WORD_CHAR, + margin_top = 8, + margin_start = 8, + margin_end = 8, + margin_bottom = 8 + }; + this.child = widget; + + callback_url = author_entity.url; + this.activatable = callback_url != null && callback_url != ""; + } } + + public void open () { + if (callback_account != null) { + callback_account.open (); + } else if (callback_url != null && callback_url != "") { + Host.open_url (callback_url); + } + } + } + + Gtk.Popover? authors_popover = null; + private void open_authors (Gtk.Button btn) { + if (authors_popover != null) return; + + var listbox = new Gtk.ListBox () { + selection_mode = Gtk.SelectionMode.NONE, + css_classes = { "background-none" } + }; + listbox.row_activated.connect (on_author_row_activated); + + foreach (var author in verified_authors) { + if ( + author.account != null + || ( + author.name != null + && author.name != "" + ) + ) listbox.append (new AuthorRow (author)); + } + + authors_popover = new Gtk.Popover () { + child = new Gtk.ScrolledWindow () { + child = listbox, + hexpand = true, + vexpand = true, + hscrollbar_policy = Gtk.PolicyType.NEVER, + max_content_height = 500, + width_request = 360, + propagate_natural_height = true + } + }; + + authors_popover.closed.connect (clear_authors_popover); + authors_popover.set_parent (btn); + authors_popover.popup (); + } + + private void clear_authors_popover () { + if (authors_popover == null) return; + + authors_popover.unparent (); + authors_popover.dispose (); + authors_popover = null; + } + + private void on_author_row_activated (Gtk.ListBoxRow row) { + clear_authors_popover (); + ((AuthorRow) row).open (); } } diff --git a/src/Widgets/RichLabel.vala b/src/Widgets/RichLabel.vala index c0f230fee..5252c8f00 100644 --- a/src/Widgets/RichLabel.vala +++ b/src/Widgets/RichLabel.vala @@ -81,7 +81,12 @@ public class Tuba.Widgets.RichLabel : Adw.Bin { public RichLabel (string? text = null) { if (text != null) - label = text; + this.label = text; + } + + public RichLabel.with_emojis (string? text = null, Gee.HashMap? instance_emojis = null) { + if (instance_emojis != null) this.instance_emojis = instance_emojis; + if (text != null) this.label = text; } construct { diff --git a/src/Widgets/Status.vala b/src/Widgets/Status.vala index ba1ab84cf..0cfa5ac83 100644 --- a/src/Widgets/Status.vala +++ b/src/Widgets/Status.vala @@ -840,7 +840,7 @@ } } - protected Gtk.Button prev_card; + protected Widgets.PreviewCard prev_card; private Widgets.Attachment.Box attachments; private Gtk.Label translation_label; private Widgets.VoteBox poll; @@ -1008,8 +1008,8 @@ if (prev_card != null) content_box.remove (prev_card); if (settings.show_preview_cards && !status.formal.has_media && status.formal.card != null && status.formal.card.kind in ALLOWED_CARD_TYPES) { try { - prev_card = (Gtk.Button) status.formal.card.to_widget (); - prev_card.clicked.connect (open_card_url); + prev_card = (Widgets.PreviewCard) status.formal.card.to_widget (); + prev_card.button.clicked.connect (open_card_url); content_box.append (prev_card); } catch {} }