Skip to content

Commit 3f38e4f

Browse files
authored
feat(status): reaction posting (#1145)
1 parent 858d6e8 commit 3f38e4f

14 files changed

+513
-195
lines changed

data/style.css

+4
Original file line numberDiff line numberDiff line change
@@ -766,3 +766,7 @@ popover.mini-profile > contents {
766766
list.uniform-border-color row {
767767
border-bottom-color: @borders;
768768
}
769+
770+
.emoji-reaction-expander > box > list, .emoji-reaction-expander > box > list > row {
771+
border-radius: inherit;
772+
}

data/ui/widgets/announcement.ui

+2-13
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</object>
2828
</child>
2929
<child>
30-
<object class="GtkBox">
30+
<object class="GtkBox" id="mainbox">
3131
<property name="orientation">vertical</property>
3232
<child>
3333
<object class="GtkBox">
@@ -140,17 +140,6 @@
140140
</child>
141141
</object>
142142
</child>
143-
144-
<child>
145-
<object class="GtkFlowBox" id="emoji_reactions">
146-
<property name="margin-top">16</property>
147-
<property name="visible">0</property>
148-
<property name="column_spacing">6</property>
149-
<property name="row_spacing">6</property>
150-
<!-- Lower values leave space between items -->
151-
<property name="max_children_per_line">100</property>
152-
</object>
153-
</child>
154143
</object>
155144
</child>
156145
</object>
@@ -159,4 +148,4 @@
159148
<class name="ttl-post" />
160149
</style>
161150
</template>
162-
</interface>
151+
</interface>

src/API/EmojiReaction.vala

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
1-
public class Tuba.API.EmojiReaction : Entity {
1+
public class Tuba.API.EmojiReaction : Entity, Widgetizable {
22
public int64 count { get; set; default = 0;}
33
public string? url { get; set; default = null; }
44
public string? name { get; set; default = null; }
55
public bool me { get; set; default = false; }
6+
public Gee.ArrayList<API.Account>? accounts { get; set; default = null; }
7+
public Gee.ArrayList<string>? account_ids { get; set; default = null; }
8+
9+
public override Type deserialize_array_type (string prop) {
10+
switch (prop) {
11+
case "accounts":
12+
return typeof (API.Account);
13+
case "account-ids":
14+
return Type.STRING;
15+
}
16+
17+
return base.deserialize_array_type (prop);
18+
}
19+
20+
public override Gtk.Widget to_widget () {
21+
return new Widgets.EmojiReactionAccounts (this);
22+
}
23+
24+
public override void open () {
25+
}
626
}

src/API/Instance.vala

+10
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ public class Tuba.API.Instance : Entity {
186186
}
187187
}
188188

189+
public int64 compat_status_reactions_max {
190+
get {
191+
if (configuration != null && configuration.reactions != null) {
192+
return configuration.reactions.max_reactions;
193+
}
194+
195+
return 0;
196+
}
197+
}
198+
189199
public static API.Instance from (Json.Node node) throws Error {
190200
return Entity.from_json (typeof (API.Instance), node) as API.Instance;
191201
}

src/API/Notification.vala

+3-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public class Tuba.API.Notification : Entity, Widgetizable {
6262
public string? kind { get; set; default = null; }
6363
public string? created_at { get; set; default = null; }
6464
public API.Status? status { get; set; default = null; }
65+
public string? emoji { get; set; default = null; }
66+
public string? emoji_url { get; set; default = null; }
6567
public API.Admin.Report? report { get; set; default = null; }
6668

6769
// the docs claim that 'relationship_severance_event'
@@ -157,7 +159,7 @@ public class Tuba.API.Notification : Entity, Widgetizable {
157159
kind_actor_name = _("%s (& %d others)").printf (account.display_name, others);
158160
}
159161

160-
issuer.describe_kind (kind, out res_kind, kind_actor_name);
162+
issuer.describe_kind (kind, out res_kind, kind_actor_name, emoji);
161163
var toast = new GLib.Notification (res_kind.description);
162164
if (status != null) {
163165
var body = "";

src/Services/Accounts/InstanceAccount.vala

+24-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class Tuba.InstanceAccount : API.Account, Streamable {
2020
public const string KIND_ADMIN_REPORT = "admin.report";
2121
public const string KIND_ADMIN_SIGNUP = "admin.sign_up";
2222
public const string KIND_STATUS = "status";
23+
public const string KIND_PLEROMA_REACTION = "pleroma:emoji_reaction";
24+
public const string KIND_REACTION = "reaction";
2325

2426
public string uuid { get; set; }
2527
public bool admin_mode { get; set; default=false; }
@@ -220,7 +222,8 @@ public class Tuba.InstanceAccount : API.Account, Streamable {
220222
string? kind,
221223
out Kind result,
222224
string? actor_name = null,
223-
string? callback_url = null
225+
string? callback_url = null,
226+
string? other_data = null
224227
) {
225228
switch (kind) {
226229
case KIND_MENTION:
@@ -347,6 +350,26 @@ public class Tuba.InstanceAccount : API.Account, Streamable {
347350
_("%s just posted").printf (actor_name),
348351
};
349352
break;
353+
case KIND_PLEROMA_REACTION:
354+
case KIND_REACTION:
355+
string body;
356+
if (other_data == null) {
357+
// translators: the variable is a string user name,
358+
// this is used for notifications
359+
body = _("%s reacted to your post").printf (actor_name);
360+
} else {
361+
// translators: the first variable is a string user name,
362+
// the second variable is an emoji,
363+
// this is used for notifications
364+
body = _("%s reacted to your post with %s").printf (actor_name, other_data);
365+
}
366+
367+
result = {
368+
"tuba-smile-symbolic",
369+
body,
370+
callback_url
371+
};
372+
break;
350373
default:
351374
result = {
352375
null,

src/Views/StatusStats.vala

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
public class Tuba.Views.StatusStats : Views.TabbedBase {
22
Views.ContentBase favorited;
33
Views.ContentBase boosted;
4+
Views.ContentBase reacted;
45

56
construct {
67
label = _("Post Stats");
78
}
89

9-
public StatusStats (string status_id) {
10+
public StatusStats (string status_id, bool has_reactors = false) {
1011
favorited = add_timeline_tab (
1112
// translators: title for a list of people that favorited a post
1213
_("Favorited By"),
@@ -26,5 +27,17 @@ public class Tuba.Views.StatusStats : Views.TabbedBase {
2627
_("No Boosts"),
2728
"tuba-heart-broken-symbolic"
2829
);
30+
31+
if (has_reactors && accounts.active.instance_info != null && accounts.active.instance_info.pleroma != null) {
32+
reacted = add_timeline_tab (
33+
// translators: title for a list of people that have reacted to a post
34+
_("Reactions"),
35+
"tuba-smile-symbolic",
36+
@"/api/v1/pleroma/statuses/$(status_id)/reactions",
37+
typeof (API.EmojiReaction),
38+
_("No Reactions"),
39+
"tuba-heart-broken-symbolic"
40+
);
41+
}
2942
}
3043
}

src/Widgets/Announcement.vala

+6-131
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,5 @@
11
[GtkTemplate (ui = "/dev/geopjr/Tuba/ui/widgets/announcement.ui")]
22
public class Tuba.Widgets.Announcement : Gtk.ListBoxRow {
3-
public class ReactButton : Gtk.Button {
4-
private Gtk.Label reactions_label;
5-
public string shortcode { get; private set; }
6-
public signal void reaction_toggled ();
7-
8-
private bool _has_reacted = false;
9-
public bool has_reacted {
10-
get {
11-
return _has_reacted;
12-
}
13-
set {
14-
_has_reacted = value;
15-
update_reacted (value);
16-
}
17-
}
18-
19-
private int64 _reactions = 0;
20-
public int64 reactions {
21-
get {
22-
return _reactions;
23-
}
24-
set {
25-
_reactions = value;
26-
reactions_label.label = value.to_string ();
27-
}
28-
}
29-
30-
public ReactButton (API.EmojiReaction reaction) {
31-
this.set_accessible_role (Gtk.AccessibleRole.TOGGLE_BUTTON);
32-
33-
// translators: the variable is the emoji or its name if it's custom
34-
tooltip_text = _("React with %s").printf (reaction.name);
35-
shortcode = reaction.name;
36-
37-
var badge = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
38-
if (reaction.url != null) {
39-
badge.append (new Widgets.Emoji (reaction.url));
40-
} else {
41-
badge.append (new Gtk.Label (reaction.name));
42-
}
43-
44-
reactions_label = new Gtk.Label (null);
45-
reactions = reaction.count;
46-
47-
badge.append (reactions_label);
48-
this.child = badge;
49-
50-
_has_reacted = reaction.me;
51-
if (reaction.me == true) {
52-
this.add_css_class ("accent");
53-
this.update_state (Gtk.AccessibleState.PRESSED, Gtk.AccessibleTristate.TRUE, -1);
54-
}
55-
56-
this.clicked.connect (on_clicked);
57-
}
58-
59-
public void update_reacted (bool reacted = true) {
60-
if (reacted) {
61-
this.add_css_class ("accent");
62-
reactions = reactions + 1;
63-
this.update_state (Gtk.AccessibleState.PRESSED, Gtk.AccessibleTristate.TRUE, -1);
64-
} else {
65-
this.remove_css_class ("accent");
66-
reactions = reactions - 1;
67-
this.update_state (Gtk.AccessibleState.PRESSED, Gtk.AccessibleTristate.FALSE, -1);
68-
}
69-
_has_reacted = reacted;
70-
}
71-
72-
private void on_clicked () {
73-
reaction_toggled ();
74-
}
75-
}
76-
77-
private API.Announcement announcement { get; private set; }
783
public signal void open ();
794

805
[GtkChild] protected unowned Adw.Avatar avatar;
@@ -84,7 +9,7 @@ public class Tuba.Widgets.Announcement : Gtk.ListBoxRow {
849
[GtkChild] protected unowned Gtk.Image attention_indicator;
8510
[GtkChild] protected unowned Gtk.Label date_label;
8611
[GtkChild] protected unowned Widgets.MarkupView content;
87-
[GtkChild] protected unowned Gtk.FlowBox emoji_reactions;
12+
[GtkChild] protected unowned Gtk.Box mainbox;
8813

8914
private void aria_describe_status () {
9015
// translators: This is an accessibility label.
@@ -142,37 +67,6 @@ public class Tuba.Widgets.Announcement : Gtk.ListBoxRow {
14267
);
14368
}
14469

145-
private Gee.ArrayList<API.EmojiReaction>? reactions {
146-
set {
147-
if (value == null) return;
148-
149-
var i = 0;
150-
Gtk.FlowBoxChild? fb_child = null;
151-
while ((fb_child = emoji_reactions.get_child_at_index (i)) != null) {
152-
emoji_reactions.remove (fb_child);
153-
i = i + 1;
154-
}
155-
156-
foreach (API.EmojiReaction p in value) {
157-
if (p.count <= 0) return;
158-
159-
var badge_button = new ReactButton (p);
160-
badge_button.reaction_toggled.connect (on_reaction_toggled);
161-
162-
// emoji_reactions.append(badge_button); // GTK >= 4.5
163-
emoji_reactions.insert (
164-
new Gtk.FlowBoxChild () {
165-
child = badge_button,
166-
focusable = false
167-
},
168-
-1
169-
);
170-
}
171-
172-
emoji_reactions.visible = value.size > 0;
173-
}
174-
}
175-
17670
void settings_updated () {
17771
Tuba.toggle_css (this, settings.larger_font_size, "ttl-status-font-large");
17872
Tuba.toggle_css (this, settings.larger_line_height, "ttl-status-line-height-large");
@@ -199,8 +93,6 @@ public class Tuba.Widgets.Announcement : Gtk.ListBoxRow {
19993
string announcement_date;
20094
int reactions_count = 0;
20195
public Announcement (API.Announcement t_announcement) {
202-
announcement = t_announcement;
203-
20496
content.instance_emojis = t_announcement.emojis_map;
20597
content.content = t_announcement.content;
20698
attention_indicator.visible = !t_announcement.read;
@@ -226,34 +118,17 @@ public class Tuba.Widgets.Announcement : Gtk.ListBoxRow {
226118
if (instance_title != "") avatar.show_initials = true;
227119
if (instance_thumbnail != "") Tuba.Helper.Image.request_paintable (instance_thumbnail, null, false, on_cache_response);
228120

229-
reactions = t_announcement.reactions;
230121
reactions_count = t_announcement.reactions.size;
122+
if (reactions_count > 0)
123+
mainbox.append (new Widgets.ReactionsRow (t_announcement.id, t_announcement.reactions, true) {
124+
margin_top = 16
125+
});
231126

232-
announcement.bind_property ("read", attention_indicator, "visible", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.INVERT_BOOLEAN);
127+
t_announcement.bind_property ("read", attention_indicator, "visible", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.INVERT_BOOLEAN);
233128
aria_describe_status ();
234129
}
235130

236131
void on_cache_response (Gdk.Paintable? data) {
237132
avatar.custom_image = data;
238133
}
239-
240-
private void on_reaction_toggled (ReactButton btn) {
241-
var endpoint = @"/api/v1/announcements/$(announcement.id)/reactions/$(btn.shortcode)";
242-
var req = btn.has_reacted ? new Request.DELETE (endpoint) : new Request.PUT (endpoint);
243-
244-
btn.sensitive = false;
245-
req
246-
.with_account (accounts.active)
247-
.then (() => {
248-
btn.update_reacted (!btn.has_reacted);
249-
btn.sensitive = true;
250-
})
251-
.on_error ((code, message) => {
252-
warning (@"Error while reacting to announcement: $code $message");
253-
btn.sensitive = true;
254-
255-
app.toast ("%s: %s".printf (_("Error"), message));
256-
})
257-
.exec ();
258-
}
259134
}

0 commit comments

Comments
 (0)