Skip to content

Commit

Permalink
feat: advanced boost dialog (#629)
Browse files Browse the repository at this point in the history
* feat: advanced boost dialog

* fix(ActionsRow): memory leak

bindings as always

* feat: basic quote posts

these do not use quote_id on supported backend

* fix(preferences): set option title as translatable
  • Loading branch information
GeopJr authored Nov 14, 2023
1 parent 43b2580 commit e6f3227
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 36 deletions.
3 changes: 3 additions & 0 deletions data/dev.geopjr.Tuba.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
<key name="group-push-notifications" type="b">
<default>false</default>
</key>
<key name="advanced-boost-dialog" type="b">
<default>false</default>
</key>

<key name="window-x" type="i">
<default>-1</default>
Expand Down
6 changes: 6 additions & 0 deletions data/ui/dialogs/preferences.ui
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@
<property name="subtitle" translatable="yes">It can lead to broken links</property>
</object>
</child>
<child>
<object class="AdwSwitchRow" id="advanced_boost_dialog">
<property name="title" translatable="yes">Advanced boost dialog</property>
<property name="subtitle" translatable="yes">Change boost visibility, quote and confirm boosting</property>
</object>
</child>
</object>
</child>
<child>
Expand Down
13 changes: 13 additions & 0 deletions src/API/Status.vala
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ public class Tuba.API.Status : Entity, Widgetizable {
return "";
}
}

public static ReblogVisibility? from_string (string id) {
switch (id) {
case "public":
return PUBLIC;
case "unlisted":
return UNLISTED;
case "private":
return PRIVATE;
default:
return null;
}
}
}

public Request reblog_req (ReblogVisibility? visibility = null) {
Expand Down
10 changes: 7 additions & 3 deletions src/Dialogs/Composer/Dialog.vala
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public class Tuba.Dialogs.Compose : Adw.Window {

public BasicStatus original_status { get; construct set; }
public BasicStatus status { get; construct set; }
public bool force_cursor_at_start { get; construct set; default=false; }

public delegate void SuccessCallback (API.Status cb_status);
protected SuccessCallback? cb;
Expand Down Expand Up @@ -224,7 +225,9 @@ public class Tuba.Dialogs.Compose : Adw.Window {

private ComposerPage[] t_pages = {};
protected virtual signal void build () {
var p_edit = new EditorPage ();
var p_edit = new EditorPage () {
force_cursor_at_start = force_cursor_at_start
};
var p_attach = new AttachmentsPage ();
var p_poll = new PollPage ();

Expand Down Expand Up @@ -277,12 +280,13 @@ public class Tuba.Dialogs.Compose : Adw.Window {

[GtkChild] unowned Adw.ViewStack stack;

public Compose (API.Status template = new API.Status.empty ()) {
public Compose (API.Status template = new API.Status.empty (), bool t_force_cursor_at_start = false) {
Object (
status: new BasicStatus.from_status (template),
original_status: new BasicStatus.from_status (template),
button_label: _("_Publish"),
button_class: "suggested-action"
button_class: "suggested-action",
force_cursor_at_start: t_force_cursor_at_start
);
}

Expand Down
9 changes: 8 additions & 1 deletion src/Dialogs/Composer/EditorPage.vala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
public class Tuba.EditorPage : ComposerPage {

public bool force_cursor_at_start { get; construct set; default=false; }
protected int64 char_limit { get; set; default = 500; }
protected int64 remaining_chars { get; set; default = 0; }
public signal void ctrl_return_pressed ();
Expand All @@ -24,6 +24,12 @@ public class Tuba.EditorPage : ComposerPage {
remaining_chars = char_limit;
}

public void set_cursor_at_start () {
Gtk.TextIter star_iter;
editor.buffer.get_start_iter (out star_iter);
editor.buffer.place_cursor (star_iter);
}

public override void on_build () {
base.on_build ();
bool supports_mime_types = accounts.active.supported_mime_types.n_items > 1;
Expand Down Expand Up @@ -210,6 +216,7 @@ public class Tuba.EditorPage : ComposerPage {

content.prepend (overlay);
editor.buffer.text = t_content;
if (force_cursor_at_start) set_cursor_at_start ();
}

protected Gtk.EmojiChooser emoji_picker;
Expand Down
2 changes: 2 additions & 0 deletions src/Dialogs/Preferences.vala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class Tuba.Dialogs.Preferences : Adw.PreferencesWindow {
[GtkChild] unowned Adw.SwitchRow enlarge_custom_emojis;
[GtkChild] unowned Adw.SwitchRow use_blurhash;
[GtkChild] unowned Adw.SwitchRow group_push_notifications;
[GtkChild] unowned Adw.SwitchRow advanced_boost_dialog;

[GtkChild] unowned Adw.SwitchRow new_followers_notifications_switch;
[GtkChild] unowned Adw.SwitchRow new_follower_requests_notifications_switch;
Expand Down Expand Up @@ -125,6 +126,7 @@ public class Tuba.Dialogs.Preferences : Adw.PreferencesWindow {
settings.bind ("enlarge-custom-emojis", enlarge_custom_emojis, "active", SettingsBindFlags.DEFAULT);
settings.bind ("use-blurhash", use_blurhash, "active", SettingsBindFlags.DEFAULT);
settings.bind ("group-push-notifications", group_push_notifications, "active", SettingsBindFlags.DEFAULT);
settings.bind ("advanced-boost-dialog", advanced_boost_dialog, "active", SettingsBindFlags.DEFAULT);

post_visibility_combo_row.notify["selected-item"].connect (on_post_visibility_changed);

Expand Down
4 changes: 3 additions & 1 deletion src/Services/Settings.vala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class Tuba.Settings : GLib.Settings {
public string default_content_type { get; set; default = "text/plain"; }
public bool use_blurhash { get; set; }
public bool group_push_notifications { get; set; }
public bool advanced_boost_dialog { get; set; }

public string[] muted_notification_types { get; set; default = {}; }
private static string[] keys_to_init = {
Expand All @@ -43,7 +44,8 @@ public class Tuba.Settings : GLib.Settings {
"muted-notification-types",
"default-content-type",
"use-blurhash",
"group-push-notifications"
"group-push-notifications",
"advanced-boost-dialog"
};

public Settings () {
Expand Down
175 changes: 144 additions & 31 deletions src/Widgets/Status/ActionsRow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,19 @@ public class Tuba.Widgets.ActionsRow : Gtk.Box {
}

Binding[] bindings = {};
ulong[] status_notify_signals = {};
public void bind () {
if (bindings.length != 0) return;

bindings += this.status.bind_property ("replies-count", reply_button, "amount", GLib.BindingFlags.SYNC_CREATE);
bindings += this.status.bind_property ("in-reply-to-id", reply_button, "default_icon_name", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
target.set_string (src.get_string () != null ? "tuba-reply-all-symbolic" : "tuba-reply-sender-symbolic");
return true;
});

bindings += this.status.bind_property ("can-be-boosted", reblog_button, "sensitive", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
bool src_val = src.get_boolean ();
target.set_boolean (src_val);
status_notify_signals += this.status.notify["in-reply-to-id"].connect (in_reply_to_id_notify_func);
in_reply_to_id_notify_func ();

if (src_val) {
reblog_button.tooltip_text = _("Boost");
reblog_button.default_icon_name = "tuba-media-playlist-repeat-symbolic";
} else {
reblog_button.tooltip_text = _("This post can't be boosted");
reblog_button.default_icon_name = accounts.active.visibility[this.status.visibility].icon_name;
}
status_notify_signals += this.status.notify["can-be-boosted"].connect (can_be_boosted_notify_func);
can_be_boosted_notify_func ();

return true;
});
bindings += this.status.bind_property ("can-be-boosted", reblog_button, "sensitive", BindingFlags.SYNC_CREATE);
bindings += this.status.bind_property ("reblogged", reblog_button, "active", GLib.BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
bindings += this.status.bind_property ("reblogs-count", reblog_button, "amount", GLib.BindingFlags.SYNC_CREATE);

Expand All @@ -50,17 +40,40 @@ public class Tuba.Widgets.ActionsRow : Gtk.Box {
bindings += this.status.bind_property ("bookmarked", bookmark_button, "active", GLib.BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
}

private void in_reply_to_id_notify_func () {
reply_button.default_icon_name = this.status.in_reply_to_id != null ? "tuba-reply-all-symbolic" : "tuba-reply-sender-symbolic";
}

private void can_be_boosted_notify_func () {
bool src_val = this.status.can_be_boosted;
reblog_button.sensitive = src_val;

if (src_val) {
reblog_button.tooltip_text = _("Boost");
reblog_button.default_icon_name = "tuba-media-playlist-repeat-symbolic";
} else {
reblog_button.tooltip_text = _("This post can't be boosted");
reblog_button.default_icon_name = accounts.active.visibility[this.status.visibility].icon_name;
}
}

public void unbind () {
foreach (var binding in bindings) {
binding.unbind ();
}

foreach (var signal_id in status_notify_signals) {
if (GLib.SignalHandler.is_connected (this.status, signal_id))
this.status.disconnect (signal_id);
}

bindings = {};
status_notify_signals = {};
}

construct {
this.add_css_class ("ttl-post-actions");
this.spacing = 6;
construct {
this.add_css_class ("ttl-post-actions");
this.spacing = 6;

reply_button = new StatusActionButton.with_icon_name ("tuba-reply-sender-symbolic") {
active = false,
Expand Down Expand Up @@ -100,7 +113,7 @@ public class Tuba.Widgets.ActionsRow : Gtk.Box {
};
bookmark_button.clicked.connect (on_bookmark_button_clicked);
this.append (bookmark_button);
}
}

private void on_reply_button_clicked (Gtk.Button btn) {
reply (btn);
Expand Down Expand Up @@ -154,21 +167,121 @@ public class Tuba.Widgets.ActionsRow : Gtk.Box {
if (status_btn.working) return;

status_btn.block_clicked ();
status_btn.active = !status_btn.active;

string action;
Request req;
if (status_btn.active) {
action = "reblog";
req = this.status.reblog_req ();
if (!status_btn.active && settings.advanced_boost_dialog) {
Gtk.ListBox visibility_box = new Gtk.ListBox () {
css_classes = {"content"},
selection_mode = Gtk.SelectionMode.NONE
};

Gtk.CheckButton? group = null; // hashmap is not ordered
Gee.HashMap<API.Status.ReblogVisibility, Gtk.CheckButton> check_buttons = new Gee.HashMap<API.Status.ReblogVisibility, Gtk.CheckButton> ();
for (int i = 0; i < accounts.active.visibility_list.n_items; i++) {
var visibility = (InstanceAccount.Visibility) accounts.active.visibility_list.get_item (i);
var reblog_visibility = API.Status.ReblogVisibility.from_string (visibility.id);
if (reblog_visibility == null) continue;

var checkbutton = new Gtk.CheckButton () {
css_classes = {"selection-mode"},
active = settings.default_post_visibility == visibility.id
};
check_buttons.set (reblog_visibility, checkbutton);

if (group != null) {
checkbutton.group = group;
} else {
group = checkbutton;
}

var visibility_row = new Adw.ActionRow () {
title = visibility.name,
subtitle = visibility.description,
activatable_widget = checkbutton
};
visibility_row.add_prefix (new Gtk.Image.from_icon_name (visibility.icon_name));
visibility_row.add_prefix (checkbutton);

visibility_box.append (visibility_row);
}

var dlg = new Adw.MessageDialog (
app.main_window,
_("Boost with Visibility"),
null
) {
extra_child = visibility_box
};
dlg.add_responses (
"no", _("Cancel"),
"quote", _("Quote"),
"yes", _("Boost")
);
dlg.set_response_appearance ("yes", Adw.ResponseAppearance.SUGGESTED);
dlg.transient_for = app.main_window;

dlg.response.connect (res => {
dlg.destroy ();

switch (res) {
case "yes":
case "quote":
API.Status.ReblogVisibility? reblog_visibility = null;
check_buttons.foreach (e => {
if (((Gtk.CheckButton) e.value).active) {
reblog_visibility = (API.Status.ReblogVisibility) e.key;
return false;
}

return true;
});

switch (res) {
case "yes":
commit_boost (status_btn, reblog_visibility);
break;
case "quote":
// TODO: use quote_id for supported backends
new Dialogs.Compose (new API.Status.empty () {
visibility = reblog_visibility == null ? settings.default_post_visibility : reblog_visibility.to_string (),
content = @"\n\nRE: $(status.formal.url ?? status.formal.account.url)"
}, true);
status_btn.unblock_clicked ();
break;
default:
assert_not_reached ();
}
break;
default:
status_btn.unblock_clicked ();
break;
}

group = null;
check_buttons.clear ();
});

dlg.present ();
} else {
action = "unreblog";
req = this.status.unreblog_req ();
commit_boost (status_btn);
}
status_btn.amount += status_btn.active ? 1 : -1;
}

debug (@"Performing status action '$action'…");
mastodon_action (status_btn, req, action, "reblogs-count");
private void commit_boost (StatusActionButton status_btn, API.Status.ReblogVisibility? visibility = null) {
status_btn.active = !status_btn.active;

string action;
Request req;
if (status_btn.active) {
action = "reblog";
req = this.status.reblog_req (visibility);
} else {
action = "unreblog";
req = this.status.unreblog_req ();
}

status_btn.amount += status_btn.active ? 1 : -1;
debug (@"Performing status action '$action'…");
mastodon_action (status_btn, req, action, "reblogs-count");
}

private void mastodon_action (StatusActionButton status_btn, Request req, string action, string? count_property = null) {
Expand Down

0 comments on commit e6f3227

Please sign in to comment.