mirror of
https://github.com/TakeV-Lambda/Tooth.git
synced 2024-09-26 12:43:00 +00:00
feat: partial emoji support
This commit is contained in:
parent
5f3dca2cef
commit
46f591c232
10
data/app.css
10
data/app.css
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
4
src/API/Emoji.vala
Normal file
|
@ -0,0 +1,4 @@
|
|||
public class Tooth.API.Emoji : Entity {
|
||||
public string shortcode { get; set; }
|
||||
public string url { get; set; }
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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) {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
21
src/Widgets/Emoji.vala
Normal 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;
|
||||
}
|
||||
}
|
130
src/Widgets/RichLabelContainer.vala
Normal file
130
src/Widgets/RichLabelContainer.vala
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue