feat: partial emoji support

This commit is contained in:
Evangelos Paterakis 2022-11-17 20:32:26 +02:00
parent 5f3dca2cef
commit 46f591c232
No known key found for this signature in database
GPG key ID: FE5185F095BFC8C9
16 changed files with 230 additions and 31 deletions

View file

@ -75,8 +75,14 @@
margin-bottom: 32px;
}
.ttl-label-emoji-no-click:hover {
background: transparent;
}
.ttl-label-emoji-no-click {
background: transparent;
padding: 0;
}
/*
Profile

View file

@ -56,8 +56,8 @@
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="ToothWidgetsRichLabel" id="display_name">
<property name="label">Unknown</property>
<object class="ToothWidgetsRichLabelContainer" id="display_name">
<!-- <property name="label">Unknown</property> -->
<style>
<class name="title-3"/>
</style>

View file

@ -27,9 +27,9 @@
</object>
</child>
<child>
<object class="ToothWidgetsRichLabel" id="header_label">
<object class="ToothWidgetsRichLabelContainer" id="header_label">
<property name="visible">0</property>
<property name="ellipsize">end</property>
<!-- <property name="ellipsize">end</property> -->
<property name="margin-bottom">8</property>
<style>
<class name="font-bold"/>
@ -46,13 +46,13 @@
<property name="vexpand">1</property>
<property name="row_homogeneous">1</property>
<child>
<object class="ToothWidgetsRichLabel" id="name_label">
<object class="ToothWidgetsRichLabelContainer" id="name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="no">Name</property>
<property name="ellipsize">end</property>
<property name="single_line_mode">True</property>
<!-- <property name="label" translatable="no">Name</property> -->
<!-- <property name="ellipsize">end</property> -->
<!-- <property name="single_line_mode">True</property> -->
<style>
<class name="font-bold"/>
</style>

View file

@ -48,6 +48,7 @@ sources = files(
'src/API/AccountField.vala',
'src/API/Attachment.vala',
'src/API/Conversation.vala',
'src/API/Emoji.vala',
'src/API/Entity.vala',
'src/API/List.vala',
'src/API/Mention.vala',
@ -109,11 +110,13 @@ sources = files(
'src/Widgets/Avatar.vala',
'src/Widgets/Background.vala',
'src/Widgets/Conversation.vala',
'src/Widgets/Emoji.vala',
'src/Widgets/LockableToggleButton.vala',
'src/Widgets/MarkupView.vala',
'src/Widgets/Notification.vala',
'src/Widgets/RelationshipButton.vala',
'src/Widgets/RichLabel.vala',
'src/Widgets/RichLabelContainer.vala',
'src/Widgets/Status.vala',
'src/Widgets/StatusActionButton.vala',
'src/Widgets/Widgetizable.vala',

View file

@ -22,6 +22,7 @@ public class Tooth.API.Account : Entity, Widgetizable {
public string avatar { get; set; }
public string url { get; set; }
public string created_at { get; set; }
public Gee.ArrayList<API.Emoji>? emojis { get; set; }
public int64 followers_count { get; set; }
public int64 following_count { get; set; }
public int64 statuses_count { get; set; }
@ -38,6 +39,23 @@ public class Tooth.API.Account : Entity, Widgetizable {
return uri.get_host ();
}
}
public Gee.HashMap<string, string>? emojis_map {
owned get {
return gen_emojis_map();
}
}
private Gee.HashMap<string, string>? gen_emojis_map () {
var res = new Gee.HashMap<string, string>();
if (emojis != null && emojis.size > 0) {
emojis.@foreach (e => {
res.set(e.shortcode, e.url);
return true;
});
}
return res;
}
public static Account from (Json.Node node) throws Error {
return Entity.from_json (typeof (API.Account), node) as API.Account;

4
src/API/Emoji.vala Normal file
View file

@ -0,0 +1,4 @@
public class Tooth.API.Emoji : Entity {
public string shortcode { get; set; }
public string url { get; set; }
}

View file

@ -78,6 +78,9 @@ public class Tooth.Entity : GLib.Object, Widgetizable, Json.Serializable {
case "mentions":
contains = typeof (API.Mention);
break;
case "emojis":
contains = typeof (API.Emoji);
break;
case "fields":
contains = typeof (API.AccountField);
break;

View file

@ -12,7 +12,8 @@ public class Tooth.API.Notification : Entity, Widgetizable {
// TODO: notification actions
public virtual GLib.Notification to_toast (InstanceAccount issuer) {
string descr;
issuer.describe_kind (kind, null, out descr, account);
string descr_url;
issuer.describe_kind (kind, null, out descr, account, out descr_url);
var toast = new GLib.Notification ( HtmlUtils.remove_tags (descr) );
if (status != null) {

View file

@ -20,7 +20,7 @@ public class Tooth.Dialogs.NewAccount: Adw.Window {
[GtkChild] unowned Entry code_entry;
[GtkChild] unowned Label code_entry_error;
[GtkChild] unowned Label code_label;
// [GtkChild] unowned Label code_label;
[GtkChild] unowned Adw.StatusPage done_page;
public NewAccount () {

View file

@ -117,9 +117,10 @@ public class Tooth.InstanceAccount : API.Account, Streamable {
return entity;
}
public virtual void describe_kind (string kind, out string? icon, out string? descr, API.Account account) {
public virtual void describe_kind (string kind, out string? icon, out string? descr, API.Account account, out string? descr_url) {
icon = null;
descr = null;
descr_url = null;
}
public virtual void register_known_places (GLib.ListStore places) {}

View file

@ -140,39 +140,47 @@ public class Tooth.Mastodon.Account : InstanceAccount {
});
}
public override void describe_kind (string kind, out string? icon, out string? descr, API.Account account) {
public override void describe_kind (string kind, out string? icon, out string? descr, API.Account account, out string? descr_url) {
switch (kind) {
case KIND_MENTION:
icon = "user-available-symbolic";
descr = _("<span underline=\"none\"><a href=\"%s\">%s</a> mentioned you</span>").printf (account.url, account.display_name);
descr = _("%s mentioned you").printf (account.display_name);
descr_url = account.url;
break;
case KIND_REBLOG:
icon = "media-playlist-repeat-symbolic";
descr = _("<span underline=\"none\"><a href=\"%s\">%s</a> boosted your status</span>").printf (account.url, account.display_name);
descr = _("%s boosted your status").printf (account.display_name);
descr_url = account.url;
break;
case KIND_REMOTE_REBLOG:
icon = "media-playlist-repeat-symbolic";
descr = _("<span underline=\"none\"><a href=\"%s\">%s</a> boosted</span>").printf (account.url, account.display_name);
descr = _("%s boosted").printf (account.display_name);
descr_url = account.url;
break;
case KIND_FAVOURITE:
icon = "starred-symbolic";
descr = _("<span underline=\"none\"><a href=\"%s\">%s</a> favorited your status</span>").printf (account.url, account.display_name);
descr = _("%s favorited your status").printf (account.display_name);
descr_url = account.url;
break;
case KIND_FOLLOW:
icon = "contact-new-symbolic";
descr = _("<span underline=\"none\"><a href=\"%s\">%s</a> now follows you</span>").printf (account.url, account.display_name);
descr = _("%s now follows you").printf (account.display_name);
descr_url = account.url;
break;
case KIND_FOLLOW_REQUEST:
icon = "contact-new-symbolic";
descr = _("<span underline=\"none\"><a href=\"%s\">%s</a> wants to follow you</span>").printf (account.url, account.display_name);
descr = _("%s wants to follow you").printf (account.display_name);
descr_url = account.url;
break;
case KIND_POLL:
icon = "emblem-default-symbolic";
descr = _("Poll results");
descr_url = null;
break;
default:
icon = null;
descr = null;
descr_url = null;
break;
}
}

View file

@ -62,7 +62,7 @@ public class Tooth.Views.Base : Box {
protected virtual void build_actions () {}
protected virtual void build_header () {
var title = new Adw.WindowTitle (null, null);
var title = new Adw.WindowTitle (label, "");
bind_property ("label", title, "title", BindingFlags.SYNC_CREATE);
header.title_widget = title;
}

View file

@ -40,13 +40,14 @@ public class Tooth.Views.Profile : Views.Timeline {
[GtkChild] unowned Widgets.Background background;
[GtkChild] unowned ListBox info;
[GtkChild] unowned Widgets.RichLabel display_name;
[GtkChild] unowned Widgets.RichLabelContainer display_name;
[GtkChild] unowned Label handle;
[GtkChild] unowned Widgets.Avatar avatar;
[GtkChild] unowned Widgets.MarkupView note;
public void bind (API.Account account) {
display_name.label = account.display_name;
// display_name.label = account.display_name;
display_name.set_label(account.display_name, null, account.emojis_map);
handle.label = account.handle;
avatar.account = account;
note.content = account.note;
@ -236,7 +237,7 @@ public class Tooth.Views.Profile : Views.Timeline {
}
// TODO: RS badges
void on_rs_updated () {
// void on_rs_updated () {
// var label = "";
// if (rs_button.sensitive = rs != null) {
// if (rs.requested)
@ -258,8 +259,8 @@ public class Tooth.Views.Profile : Views.Timeline {
// relationship.label = label;
// relationship.visible = label != "";
invalidate_actions (false);
}
// invalidate_actions (false);
// }
public override Request append_params (Request req) {
if (page_next == null && source == "statuses") {

21
src/Widgets/Emoji.vala Normal file
View file

@ -0,0 +1,21 @@
using Gtk;
using Gdk;
public class Tooth.Widgets.Emoji : Adw.Bin {
protected Image image;
construct {
image = new Gtk.Image ();
child = image;
}
public Emoji (string emoji_url) {
image_cache.request_paintable (emoji_url, on_cache_response);
}
void on_cache_response (bool is_loaded, owned Paintable? data) {
(child as Image).paintable = data;
}
}

View file

@ -0,0 +1,130 @@
using Gtk;
using Gee;
public class Tooth.Widgets.RichLabelContainer : Adw.Bin {
Button widget;
Box button_child;
string on_click_url = null;
public weak ArrayList<API.Mention>? mentions;
construct {
widget = new Button ();
button_child = new Box(Orientation.HORIZONTAL, 0);
widget.child = button_child;
widget.halign = Align.START;
child = widget;
widget.clicked.connect (on_click);
}
public void set_label (string text, string? url, Gee.HashMap<string, string>? emojis) {
if (text.contains(":") && emojis != null) {
string[] labelss = text.split (":");
// Whether the last item was an emoji
bool was_emoji = false;
// Whether its the first item
bool is_first = labelss[0].get_char() != ':';
// The last created label
Label? last_label = null;
foreach (unowned string str in labelss) {
// If str is an available emoji
if (emojis.has_key(str)) {
button_child.append(new Widgets.Emoji(emojis.get(str)));
was_emoji = true;
} else {
// The label
// if the last item was not an emoji
// and its not the first item, prefix
// it with a ":"
// This way if someone has a name that
// includes ":" (and its not an emoji)
// we re-create the original string
string txt = (!was_emoji && !is_first ? ":" : "") + str;
// If the last label was not an emoji
// append the new label to it
// instead of creating a new one
if (last_label != null && !was_emoji) {
last_label.label = last_label.label + txt;
} else {
var tmp_child = new Label ("") {
xalign = 0,
wrap = true,
wrap_mode = Pango.WrapMode.WORD_CHAR,
justify = Justification.LEFT,
single_line_mode = false,
use_markup = false
};
tmp_child.label = txt;
button_child.append(tmp_child);
}
was_emoji = false;
}
is_first = false;
}
} else {
var tmp_child = new Label ("") {
xalign = 0,
wrap = true,
wrap_mode = Pango.WrapMode.WORD_CHAR,
justify = Justification.LEFT,
single_line_mode = false,
use_markup = true // allow markup
};
tmp_child.label = text;
button_child.append(tmp_child);
}
// if there's no url
// make the button look
// like a label
if (url ==null) {
widget.add_css_class("ttl-label-emoji-no-click");
}
on_click_url = url;
}
protected void on_click () {
if (on_click_url == null) return;
if (mentions != null){
mentions.@foreach (mention => {
if (on_click_url == mention.url)
mention.open ();
return true;
});
}
if ("/tags/" in on_click_url) {
var encoded = on_click_url.split ("/tags/")[1];
var tag = Soup.URI.decode (encoded);
app.main_window.open_view (new Views.Hashtag (tag));
return;
}
if (should_resolve_url (on_click_url)) {
accounts.active.resolve.begin (on_click_url, (obj, res) => {
try {
accounts.active.resolve.end (res).open ();
}
catch (Error e) {
warning (@"Failed to resolve URL \"$on_click_url\":");
warning (e.message);
Host.open_uri (on_click_url);
}
});
}
else {
Host.open_uri (on_click_url);
}
return;
}
public static bool should_resolve_url (string url) {
return settings.aggressive_resolving || "@" in url || "user" in url;
}
}

View file

@ -31,11 +31,11 @@ public class Tooth.Widgets.Status : ListBoxRow {
[GtkChild] protected unowned Grid grid;
[GtkChild] protected unowned Image header_icon;
[GtkChild] protected unowned Widgets.RichLabel header_label;
[GtkChild] protected unowned Widgets.RichLabelContainer header_label;
[GtkChild] public unowned Image thread_line;
[GtkChild] public unowned Widgets.Avatar avatar;
[GtkChild] protected unowned Widgets.RichLabel name_label;
[GtkChild] protected unowned Widgets.RichLabelContainer name_label;
[GtkChild] protected unowned Label handle_label;
[GtkChild] protected unowned Box indicators;
[GtkChild] protected unowned Label date_label;
@ -122,20 +122,23 @@ public class Tooth.Widgets.Status : ListBoxRow {
protected virtual void change_kind () {
string icon = null;
string descr = null;
accounts.active.describe_kind (this.kind, out icon, out descr, this.kind_instigator);
string label_url = null;
accounts.active.describe_kind (this.kind, out icon, out descr, this.kind_instigator, out label_url);
header_icon.visible = header_label.visible = (icon != null);
if (icon == null) return;
header_icon.icon_name = icon;
header_label.label = descr;
header_label.set_label(descr, label_url, this.kind_instigator.emojis_map);
}
protected virtual void bind () {
// Content
bind_property ("spoiler-text", spoiler_label, "label", BindingFlags.SYNC_CREATE);
status.formal.bind_property ("content", content, "content", BindingFlags.SYNC_CREATE);
bind_property ("title_text", name_label, "label", BindingFlags.SYNC_CREATE);
// bind_property ("title_text", name_label, "label", BindingFlags.SYNC_CREATE);
// title_text
name_label.set_label(title_text, null, this.kind_instigator.emojis_map);
bind_property ("subtitle_text", handle_label, "label", BindingFlags.SYNC_CREATE);
bind_property ("date", date_label, "label", BindingFlags.SYNC_CREATE);
status.formal.bind_property ("pinned", pin_indicator, "visible", BindingFlags.SYNC_CREATE);