Tooth/src/Views/Profile.vala

512 lines
16 KiB
Vala

using Gtk;
public class Tooth.Views.Profile : Views.Timeline {
public API.Account profile { get; construct set; }
public API.Relationship rs { get; construct set; }
public bool include_replies { get; set; default = false; }
public bool only_media { get; set; default = false; }
public string source { get; set; default = "statuses"; }
protected Cover cover;
protected Label cover_badge;
protected MenuButton menu_button;
protected Widgets.RelationshipButton rs_button;
protected SimpleAction media_action;
protected SimpleAction replies_action;
protected SimpleAction muting_action;
protected SimpleAction hiding_reblogs_action;
protected SimpleAction blocking_action;
protected SimpleAction domain_blocking_action;
protected SimpleAction ar_list_action;
// protected SimpleAction source_action;
construct {
cover = build_cover ();
cover_badge = cover.cover_badge;
column_view.prepend (cover);
}
public Profile (API.Account acc) {
Object (
profile: acc,
rs: new API.Relationship.for_account (acc),
label: _("Profile"),
is_profile: true,
url: @"/api/v1/accounts/$(acc.id)/statuses"
);
append_pinned(acc.id);
cover.bind (profile);
build_profile_stats(cover.info);
rs.invalidated.connect (on_rs_updated);
}
~Profile () {
message("Destroying Profile view");
}
public void append_pinned(string acc_id) {
new Request.GET (@"/api/v1/accounts/$(acc_id)/statuses")
.with_account (account)
.with_param ("pinned", "true")
.with_ctx (this)
.then ((sess, msg) => {
Network.parse_array (msg, node => {
var e = entity_cache.lookup_or_insert (node, typeof (API.Status));
var e_status = e as API.Status;
if (e_status != null) e_status.pinned = true;
model.append (e); //FIXME: use splice();
});
})
.exec ();
}
[GtkTemplate (ui = "/dev/geopjr/Tooth/ui/views/profile_header.ui")]
protected class Cover : Box {
[GtkChild] unowned Widgets.Background background;
[GtkChild] public unowned Label cover_badge;
[GtkChild] public unowned ListBox info;
[GtkChild] unowned Widgets.EmojiLabel display_name;
[GtkChild] unowned Label handle;
[GtkChild] unowned Widgets.Avatar avatar;
[GtkChild] unowned Widgets.MarkupView note;
public void bind (API.Account account) {
display_name.instance_emojis = account.emojis_map;
display_name.label = account.display_name;
handle.label = account.handle;
avatar.account = account;
note.content = account.note;
if (account.header.contains("/headers/original/missing.png")) {
avatar.bind_property("custom_image", background, "paintable", GLib.BindingFlags.SYNC_CREATE);
} else {
image_cache.request_paintable (account.header, on_cache_response);
}
if (account.fields != null) {
foreach (API.AccountField f in account.fields) {
var row = new Adw.ActionRow ();
var val = new Widgets.RichLabel (HtmlUtils.simplify (f.val));
val.wrap = true;
val.hexpand = true;
val.xalign = 1;
row.title = f.name;
info.append (row);
if (f.verified_at != null) {
var verified_date = f.verified_at.slice (0, f.verified_at.last_index_of ("T"));
var verified_label_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
var verified_checkmark = new Gtk.Image.from_icon_name("tooth-check-round-outline-symbolic") {
tooltip_text = _(@"Ownership of this link was checked on $verified_date")
};
verified_label_box.append(val);
verified_label_box.append(verified_checkmark);
row.add_suffix(verified_label_box);
row.add_css_class("ttl-verified-field");
} else {
row.add_suffix (val);
};
}
}
}
void on_cache_response (bool is_loaded, owned Gdk.Paintable? data) {
background.paintable = data;
}
}
protected void build_profile_stats(ListBox info) {
var row = new Gtk.ListBoxRow ();
var box = new Box (Orientation.HORIZONTAL, 0) {
homogeneous = true
};
var btn = build_profile_stats_button(@"$(profile.statuses_count) " + _("Posts"));
btn.clicked.connect(() => change_timeline_source("statuses"));
box.append(btn);
btn = build_profile_stats_button(@"$(profile.following_count) " + _("Following"));
btn.clicked.connect(() => change_timeline_source("following"));
box.append(btn);
btn = build_profile_stats_button(@"$(profile.followers_count) " + _("Followers"));
btn.clicked.connect(() => change_timeline_source("followers"));
box.append(btn);
row.activatable = false;
row.child = box;
info.append (row);
}
protected Button build_profile_stats_button(string btn_label) {
var btn = new Button.with_label(btn_label);
btn.add_css_class("flat");
btn.add_css_class("ttl-profile-stat-button");
var child_label = btn.child as Label;
child_label.wrap = true;
child_label.justify = Justification.CENTER;
return btn;
}
protected void change_timeline_source (string t_source) {
source = t_source;
accepts = t_source == "statuses" ? typeof (API.Status) : typeof (API.Account);
url = @"/api/v1/accounts/$(profile.id)/$t_source";
invalidate_actions (true);
}
protected override void build_header () {
base.build_header ();
menu_button = new MenuButton ();
var menu_builder = new Builder.from_resource (@"$(Build.RESOURCES)ui/menus.ui");
var menu = "profile-menu";
menu_button.menu_model = menu_builder.get_object (menu) as MenuModel;
menu_button.popover.width_request = 250;
menu_button.icon_name = "tooth-view-more-symbolic";
header.pack_end (menu_button);
rs_button = new Widgets.RelationshipButton () {
rs = this.rs
};
if (profile.id != accounts.active.id)
header.pack_end (rs_button);
}
protected virtual Cover build_cover () {
return new Cover ();
}
protected override void build_actions () {
base.build_actions ();
media_action = new SimpleAction.stateful ("only-media", null, false);
media_action.change_state.connect (v => {
media_action.set_state (only_media = v.get_boolean ());
invalidate_actions (true);
});
actions.add_action (media_action);
replies_action = new SimpleAction.stateful ("include-replies", null, false);
replies_action.change_state.connect (v => {
replies_action.set_state (include_replies = v.get_boolean ());
invalidate_actions (true);
});
actions.add_action (replies_action);
// source_action = new SimpleAction.stateful ("source", VariantType.STRING, source);
// source_action.change_state.connect (v => {
// source = v.get_string ();
// source_action.set_state (source);
// accepts = (source == "statuses" ? typeof (API.Status) : typeof (API.Account));
// url = @"/api/v1/accounts/$(profile.id)/$source";
// invalidate_actions (true);
// });
// actions.add_action (source_action);
ar_list_action = new SimpleAction ("ar_list", null);
ar_list_action.activate.connect (v => {
create_ar_list_dialog().show();
});
actions.add_action (ar_list_action);
var mention_action = new SimpleAction ("mention", VariantType.STRING);
mention_action.activate.connect (v => {
var status = new API.Status.empty ();
status.visibility = v.get_string ();
status.content = @"$(profile.handle) ";
new Dialogs.Compose (status);
});
actions.add_action (mention_action);
var copy_handle_action = new SimpleAction ("copy_handle", null);
copy_handle_action.activate.connect (v => {
Host.copy (profile.full_handle);
});
actions.add_action (copy_handle_action);
muting_action = new SimpleAction.stateful ("muting", null, false);
muting_action.change_state.connect (v => {
var state = v.get_boolean ();
rs.modify (state ? "mute" : "unmute");
});
actions.add_action (muting_action);
hiding_reblogs_action = new SimpleAction.stateful ("hiding_reblogs", null, false);
hiding_reblogs_action.change_state.connect (v => {
if (!rs.following) {
warning ("Trying to hide boosts while not following an account.");
return;
}
var state = !v.get_boolean ();
rs.modify ("follow", "reblogs", @"$state");
});
actions.add_action (hiding_reblogs_action);
blocking_action = new SimpleAction.stateful ("blocking", null, false);
blocking_action.change_state.connect (v => {
var block = v.get_boolean ();
var q = block ? _("Block \"%s\"?") : _("Unblock \"%s\"?");
warning (q);
var confirmed = app.question (
q.printf (profile.handle),
null,
app.main_window,
block ? _("Block") : _("Unblock"),
Adw.ResponseAppearance.DESTRUCTIVE
);
confirmed.response.connect(res => {
if (res == "yes") {
rs.modify (block ? "block" : "unblock");
}
confirmed.destroy();
});
confirmed.present ();
});
actions.add_action (blocking_action);
domain_blocking_action = new SimpleAction.stateful ("domain_blocking", null, false);
domain_blocking_action.change_state.connect (v => {
var block = v.get_boolean ();
var q = block ? _("Block Entire \"%s\"?") : _("Unblock Entire \"%s\"?");
warning (q);
var confirmed = app.question (
q.printf (profile.domain),
_("Blocking a domain will:\n\n• Remove its public posts and notifications from your timelines\n• Remove its followers from your account\n• Prevent you from following its users"),
app.main_window,
block ? _("Block") : _("Unblock"),
Adw.ResponseAppearance.DESTRUCTIVE
);
confirmed.response.connect(res => {
if (res == "yes") {
var req = new Request.POST ("/api/v1/domain_blocks")
.with_account (accounts.active)
.with_param ("domain", profile.domain)
.then (() => {
rs.request ();
});
if (!block) req.method = "DELETE";
req.exec ();
}
confirmed.destroy();
});
confirmed.present ();
});
actions.add_action (domain_blocking_action);
invalidate_actions (false);
}
void invalidate_actions (bool refresh) {
replies_action.set_enabled (accepts == typeof (API.Status));
media_action.set_enabled (accepts == typeof (API.Status));
muting_action.set_state (rs.muting);
hiding_reblogs_action.set_state (!rs.showing_reblogs);
hiding_reblogs_action.set_enabled (rs.following);
blocking_action.set_state (rs.blocking);
domain_blocking_action.set_state (rs.domain_blocking);
domain_blocking_action.set_enabled (accounts.active.domain != profile.domain);
ar_list_action.set_enabled(profile.id != accounts.active.id && rs.following);
if (refresh) {
page_next = null;
on_refresh ();
}
}
void on_rs_updated () {
var label = "";
if (rs_button.sensitive = rs != null) {
if (rs.requested)
label = _("Sent follow request");
else if (rs.followed_by && rs.following)
label = _("Mutuals");
else if (rs.followed_by)
label = _("Follows you");
}
cover_badge.label = label;
cover_badge.visible = label != "";
invalidate_actions (false);
}
public override Request append_params (Request req) {
if (page_next == null && source == "statuses") {
req.with_param ("exclude_replies", @"$(!include_replies)");
req.with_param ("only_media", @"$(only_media)");
return base.append_params (req);
}
else return req;
}
public static void open_from_id (string id) {
var msg = new Soup.Message ("GET", @"$(accounts.active.instance)/api/v1/accounts/$id");
network.queue (msg, (sess, mess) => {
var node = network.parse_node (mess);
var acc = API.Account.from (node);
app.main_window.open_view (new Views.Profile (acc));
},
network.on_error);
}
public class RowButton : Button {
public bool remove { get; set; default = false; }
}
public Adw.Window create_ar_list_dialog() {
var spinner = new Spinner() {
spinning = true,
halign = Align.CENTER,
valign = Align.CENTER,
vexpand = true,
hexpand = true,
width_request = 32,
height_request = 32
};
var box = new Box(Orientation.VERTICAL, 6);
var headerbar = new Adw.HeaderBar();
var toast_overlay = new Adw.ToastOverlay() {
vexpand = true,
valign = Align.CENTER
};
toast_overlay.child = spinner;
box.append(headerbar);
box.append(toast_overlay);
var dialog = new Adw.Window() {
title = _("Add or remove \"%s\" to or from a list").printf (profile.handle),
modal = true,
transient_for = app.main_window,
content = box,
default_width = 600,
default_height = 550
};
spinner.start();
var preferences_page = new Adw.PreferencesPage();
var preferences_group = new Adw.PreferencesGroup() {
title = _("Select the list to add or remove \"%s\" to or from:").printf (profile.handle)
};
var no_lists_page = new Adw.StatusPage() {
icon_name = "tooth-error-symbolic",
vexpand = true,
title = _("You don't have any lists")
};
new Request.GET (@"/api/v1/lists/")
.with_account (accounts.active)
.with_ctx (this)
.on_error (on_error)
.then ((sess, msg) => {
if (Network.get_array_size(msg) > 0) {
new Request.GET (@"/api/v1/accounts/$(profile.id)/lists")
.with_account (accounts.active)
.with_ctx (this)
.on_error (on_error)
.then ((sess2, msg2) => {
var added = false;
var in_list = new Gee.ArrayList<string>();
Network.parse_array (msg2, node => {
var list = API.List.from (node);
in_list.add(list.id);
});
Network.parse_array (msg, node => {
var list = API.List.from (node);
var is_already = in_list.contains(list.id);
var add_button = new RowButton() {
icon_name = is_already ? "tooth-minus-large-symbolic" : "tooth-plus-large-symbolic",
tooltip_text = is_already ? _("Remove \"%s\" from \"%s\"").printf (profile.handle, list.title) : _("Add \"%s\" to \"%s\"").printf (profile.handle, list.title),
halign = Align.CENTER,
valign = Align.CENTER
};
add_button.add_css_class("flat");
add_button.add_css_class("circular");
add_button.remove = is_already;
var row = new Adw.ActionRow() {
title = list.title
};
row.add_suffix(add_button);
add_button.clicked.connect(() => {
handle_list_edit(list, row, toast_overlay, add_button);
});
preferences_group.add(row);
added = true;
});
if (added) {
preferences_page.add(preferences_group);
toast_overlay.child = preferences_page;
toast_overlay.valign = Align.FILL;
} else {
toast_overlay.child = no_lists_page;
}
})
.exec();
} else {
toast_overlay.child = no_lists_page;
}
})
.exec ();
return dialog;
}
public void handle_list_edit(API.List list, Adw.ActionRow row, Adw.ToastOverlay toast_overlay, RowButton button) {
row.sensitive = false;
var endpoint = @"/api/v1/lists/$(list.id)/accounts/?account_ids[]=$(profile.id)";
var req = button.remove ? new Request.DELETE (endpoint) : new Request.POST (endpoint);
req
.with_account (accounts.active)
.with_ctx (this)
.on_error (on_error)
.then ((sess, msg) => {
var toast_msg = "";
if (button.remove) {
// translators: First variable is a handle, second variable is a list name
toast_msg = _("User \"%s\" got removed from \"%s\"").printf (profile.handle, list.title);
button.icon_name = "tooth-plus-large-symbolic";
// translators: First variable is a handle, second variable is a list name
button.tooltip_text = _("Add \"%s\" to \"%s\"").printf (profile.handle, list.title);
} else {
// translators: First variable is a handle, second variable is a list name
toast_msg = _("User \"%s\" got added to \"%s\"").printf (profile.handle, list.title);
button.icon_name = "tooth-minus-large-symbolic";
// translators: First variable is a handle, second variable is a list name
button.tooltip_text = _("Remove \"%s\" from \"%s\"").printf (profile.handle, list.title);
}
button.remove = !button.remove;
row.sensitive = true;
var toast = new Adw.Toast(toast_msg);
toast_overlay.add_toast(toast);
})
.exec();
}
}