[GtkTemplate (ui = "/dev/geopjr/Tuba/ui/widgets/status.ui")]
#if USE_LISTVIEW
	public class Tuba.Widgets.Status : Adw.Bin {
#else
	public class Tuba.Widgets.Status : Gtk.ListBoxRow {
#endif

	API.Status? _bound_status = null;
	public API.Status? status {
		get { return _bound_status; }
		set {
			if (_bound_status != null)
				warning ("Trying to rebind a Status widget! This is not supposed to happen!");

			_bound_status = value;
			if (_bound_status != null) {
				bind ();
			}
			if (context_menu == null) {
				create_actions ();
			}
		}
	}

	public API.Account? kind_instigator { get; set; default = null; }
	private Gtk.Button? quoted_status_btn { get; set; default = null; }
	private Gtk.Label? quote_warning { get; set; default = null; }
	public API.Translation? translation { get; private set; default = null; }
	private Adw.Bin? emoji_reactions { get; set; default = null; }
	protected string? other_data { get; set; default = null; }

	private bool _enable_thread_lines = false;
	public bool enable_thread_lines {
		get { return _enable_thread_lines; }
		set {
			if (_enable_thread_lines != value) {
				_enable_thread_lines = value;
				update_collapse ();
			}
		}
	}

	private bool _can_be_opened = true;
	public bool can_be_opened {
		get {
			return _can_be_opened;
		}

		set {
			_can_be_opened = value;

			if (value) {
				this.add_css_class ("activatable");
			} else {
				this.remove_css_class ("activatable");
			}
		}
	}

	private bool _is_quote = false;
	public bool is_quote {
		get { return _is_quote; }
		set {
			_is_quote = value;

			Gtk.Widget?[] widgets_to_toggle = {
				menu_button,
				emoji_reactions,
				prev_card,
				collection_card
			};

			foreach (var widget in widgets_to_toggle) {
				if (widget != null) {
					widget.visible = !value;
				}
			}

			toggle_actions_visibility (!value);
		}
	}

	string? _kind = null;
	public string? kind {
		get { return _kind; }
		set {
			if (value != _kind) {
				_kind = value;
				change_kind ();
				if (status != null)
					aria_describe_status ();
			}
		}
	}

	private bool _change_background_on_direct = true;
	public bool change_background_on_direct {
		get {
			return _change_background_on_direct;
		}
		set {
			_change_background_on_direct = value;
			if (!value) remove_css_class ("direct");
		}
	}

	public Dialogs.Composer.Dialog.SuccessCallback? reply_cb;

	[GtkChild] protected unowned Gtk.Box status_box;
	[GtkChild] protected unowned Gtk.Box avatar_side;
	[GtkChild] protected unowned Gtk.Box title_box;
	[GtkChild] protected unowned Gtk.Box content_side;
	[GtkChild] protected unowned Adw.WrapBox name_wrapbox;
	[GtkChild] public unowned Gtk.MenuButton menu_button;

	[GtkChild] protected unowned Gtk.Image header_icon;
	[GtkChild] protected unowned Widgets.RichLabel header_label;
	[GtkChild] protected unowned Gtk.Button header_button;

	[GtkChild] public unowned Widgets.Avatar avatar;
	[GtkChild] public unowned Gtk.Overlay avatar_overlay;
	[GtkChild] protected unowned Gtk.Button name_button;
	[GtkChild] protected unowned Widgets.RichLabel name_label;
	[GtkChild] protected unowned Gtk.Label handle_label;
	[GtkChild] public unowned Gtk.Box indicators;
	[GtkChild] public unowned Gtk.Label date_label;
	[GtkChild] protected unowned Gtk.Image pin_indicator;
	[GtkChild] protected unowned Gtk.Image edited_indicator;
	[GtkChild] protected unowned Gtk.Image visibility_indicator;

	// TODO: move to function
	[GtkChild] public unowned Gtk.Box content_column;
	[GtkChild] protected unowned Gtk.Stack spoiler_stack;
	[GtkChild] protected unowned Gtk.Box content_box;
	[GtkChild] public unowned Widgets.MarkupView content;
	[GtkChild] protected unowned Gtk.Button spoiler_button;
	[GtkChild] protected unowned Gtk.Label spoiler_label;
	[GtkChild] protected unowned Gtk.Label spoiler_label_rev;
	[GtkChild] protected unowned Gtk.Box spoiler_status_con;

	[GtkChild] protected unowned Widgets.FadeBin fade_bin;

	[GtkChild] public unowned Gtk.Stack filter_stack;
	[GtkChild] protected unowned Gtk.Label filter_label;

	private ActionsRow actions { get; private set; }
	protected Gtk.PopoverMenu context_menu { get; set; }
	private const GLib.ActionEntry[] ACTION_ENTRIES = {
		{"copy-url", copy_url},
		{"open-in-browser", open_in_browser},
		{"report", report_status}
	};
	private GLib.SimpleActionGroup action_group;
	private SimpleAction edit_history_simple_action;
	private SimpleAction stats_simple_action;
	private SimpleAction toggle_pinned_simple_action;
	private SimpleAction translate_simple_action;
	private SimpleAction show_original_simple_action;
	private SimpleAction? mute_conversation_action = null;
	private SimpleAction? unmute_conversation_action = null;
	private SimpleAction? change_quote_policy_action = null;

	public void toggle_actions_visibility (bool visible) {
		this.actions.visible = visible;
		if (visible) {
			status_box.add_css_class ("has-actions");
		} else {
			status_box.remove_css_class ("has-actions");
		}
	}

	void settings_updated () {
		Tuba.toggle_css (this, settings.larger_line_height, "ttl-status-line-height-large");
		Tuba.toggle_css (this, settings.scale_emoji_hover, "lww-scale-emoji-hover");
	}

	void font_size_updated () {
		foreach (string style in this.css_classes) {
			if (style.has_prefix ("ttl-status-font-large")) {
				this.remove_css_class (style);
				break;
			}
		}
		this.add_css_class (@"ttl-status-font-large-$((int) (settings.status_font_size * 100))");
	}

	static construct {
		typeof (Widgets.Avatar).ensure ();
		typeof (Widgets.RichLabel).ensure ();
		typeof (Widgets.MarkupView).ensure ();
		typeof (Widgets.FadeBin).ensure ();
	}

	private void update_collapse () {
		fade_bin.reveal = !settings.collapse_long_posts || expanded || enable_thread_lines;
	}

	private void on_mobile_size () {
		if (avatar.visible) {
			if (avatar.size == 48 && app.is_mobile) {
				avatar.size = 36;
				if (actor_avatar != null) actor_avatar.size = 24;
			} else if (avatar.size == 36 && !app.is_mobile) {
				avatar.size = 48;
				if (actor_avatar != null) actor_avatar.size = 34;
			}
		}
	}

	construct {
		pin_indicator.update_property (Gtk.AccessibleProperty.LABEL, pin_indicator.tooltip_text, -1);
		edited_indicator.update_property (Gtk.AccessibleProperty.LABEL, edited_indicator.tooltip_text, -1);

		name_label.use_markup = false;
		avatar_overlay.set_measure_overlay (avatar, true);
		open.connect (on_open);
		if (settings.status_font_size != 1.0)
			this.add_css_class (@"ttl-status-font-large-$((int) (settings.status_font_size * 100))");

		if (settings.larger_line_height)
			add_css_class ("ttl-status-line-height-large");

		if (settings.scale_emoji_hover)
			add_css_class ("lww-scale-emoji-hover");

		settings.notify["status-font-size"].connect (font_size_updated);
		settings.notify["larger-line-height"].connect (settings_updated);
		settings.notify["scale-emoji-hover"].connect (settings_updated);
		settings.notify["collapse-long-posts"].connect (update_collapse);
		app.notify["is-mobile"].connect (on_mobile_size);
		on_mobile_size ();

		edit_history_simple_action = new SimpleAction ("edit-history", null);
		edit_history_simple_action.activate.connect (view_edit_history);

		stats_simple_action = new SimpleAction ("status-stats", null);
		stats_simple_action.activate.connect (view_stats);

		action_group = new GLib.SimpleActionGroup ();
		action_group.add_action_entries (ACTION_ENTRIES, this);
		action_group.add_action (stats_simple_action);
		action_group.add_action (edit_history_simple_action);

		this.insert_action_group ("status", action_group);
		stats_simple_action.set_enabled (false);

		name_button.clicked.connect (on_name_button_clicked);

		update_collapse ();
	}

	private void on_name_button_clicked () {
		status.formal.account.open ();
	}

	private bool has_stats { get { return status.formal.reblogs_count != 0 || status.formal.favourites_count != 0 || status.formal.quotes_count != 0; } }
	private void show_view_stats_action () {
		stats_simple_action.set_enabled (has_stats || has_reactions ());
	}

	private bool has_reactions () {
		bool res = false;

		var reactions = status.formal.compat_status_reactions;
		if (reactions != null && reactions.size > 0) {
			res = reactions[0].account_ids != null && reactions[0].account_ids.size > 0;
		}

		return res;
	}

	public Status (API.Status status) {
		Object (
			kind_instigator: status.account,
			status: status
		);

		if (kind == null) {
			if (status.reblog != null) {
				kind = InstanceAccount.KIND_REMOTE_REBLOG;
			} else if (status.in_reply_to_id != null || status.in_reply_to_account_id != null) {
				if (status.formal.in_reply_to_account_id != null && status.formal.mentions != null && status.formal.mentions.size > 0) {
					status.formal.mentions.@foreach (mention => {
						if (mention.id == status.formal.in_reply_to_account_id) {
							forced_kind_instigator_name = mention.acct;
							return false;
						}

						return true;
					});
				}

				kind = InstanceAccount.KIND_REPLY;
			}
		}

		init_menu_button ();
	}
	~Status () {
		debug ("Destroying Status widget");
		if (context_menu != null) {
			context_menu.menu_model = null;
			context_menu.dispose ();
		}
	}

	protected void init_menu_button () {
		if (context_menu == null) {
			create_actions ();
		}

		menu_button.popover = context_menu;
		menu_button.visible = true;
	}

	protected void create_actions () {
		create_context_menu ();

		mute_conversation_action = new SimpleAction ("mute-conversation", null);
		mute_conversation_action.activate.connect (toggle_mute_conversation);
		action_group.add_action (mute_conversation_action);

		unmute_conversation_action = new SimpleAction ("unmute-conversation", null);
		unmute_conversation_action.activate.connect (toggle_mute_conversation);
		action_group.add_action (unmute_conversation_action);

		if (status.formal.account.is_self ()) {
			if (status.formal.visibility != "direct") {
				toggle_pinned_simple_action = new SimpleAction ("toggle-pinned", null);
				toggle_pinned_simple_action.activate.connect (toggle_pinned);
				toggle_pinned_simple_action.set_enabled (false);
				action_group.add_action (toggle_pinned_simple_action);

				if (
					accounts.active.tuba_api_versions.mastodon >= 7
					&& (status.formal.visibility == "public" || status.formal.visibility == "unlisted")
					&& status.formal.quote_approval != null
				) {
					change_quote_policy_action = new SimpleAction.stateful (
						"change-quote-policy",
						GLib.VariantType.STRING,
						status.formal.quote_approval.to_quote_policy ().to_string ()
					);
					change_quote_policy_action.activate.connect (change_quote_policy);
					action_group.add_action (change_quote_policy_action);
				}
			}

			var edit_status_simple_action = new SimpleAction ("edit-status", null);
			edit_status_simple_action.activate.connect (edit_status);
			action_group.add_action (edit_status_simple_action);

			var delete_status_simple_action = new SimpleAction ("delete-status", null);
			delete_status_simple_action.activate.connect (delete_status);
			action_group.add_action (delete_status_simple_action);
		} else if ((accounts.active.instance_info != null && accounts.active.instance_info.tuba_can_translate) || InstanceAccount.InstanceFeatures.TRANSLATION in accounts.active.tuba_instance_features) {
			translate_simple_action = new SimpleAction ("translate", null);
			translate_simple_action.activate.connect (translate);
			translate_simple_action.set_enabled (true);

			show_original_simple_action = new SimpleAction ("show-original", null);
			show_original_simple_action.activate.connect (translate);
			show_original_simple_action.set_enabled (false);

			action_group.add_action (translate_simple_action);
			action_group.add_action (show_original_simple_action);
		}

		if (
			status.formal.quote != null && status.formal.quote.tuba_has_quote
			&& accounts.active.tuba_api_versions.mastodon >= 7
			&& status.formal.quote.account.is_self ()
		) {
			var revoke_quote_simple_action = new SimpleAction ("revoke-quote", null);
			revoke_quote_simple_action.activate.connect (revoke_quote);
			action_group.add_action (revoke_quote_simple_action);
		}

		update_mute_conversation_actions_enabled_status ();
	}

	private GLib.MenuItem pin_menu_item;
	protected void create_context_menu () {
		var menu_model = new GLib.Menu ();
		menu_model.append (_("Open in Browser"), "status.open-in-browser");
		menu_model.append (_("Copy URL"), "status.copy-url");

		// translators: as in post stats (who liked and boosted)
		var stats_menu_item = new MenuItem (_("View Stats"), "status.status-stats");
		stats_menu_item.set_attribute_value ("hidden-when", "action-disabled");
		menu_model.append_item (stats_menu_item);

		var edit_history_menu_item = new MenuItem (_("View Edit History"), "status.edit-history");
		edit_history_menu_item.set_attribute_value ("hidden-when", "action-disabled");
		menu_model.append_item (edit_history_menu_item);

		var mute_menu_item = new GLib.MenuItem (_("Mute"), "status.mute-conversation");
		mute_menu_item.set_attribute_value ("hidden-when", "action-disabled");

		var unmute_menu_item = new GLib.MenuItem (_("Unmute"), "status.unmute-conversation");
		unmute_menu_item.set_attribute_value ("hidden-when", "action-disabled");

		if (status.formal.account.is_self ()) {
			pin_menu_item = new GLib.MenuItem (_("Pin"), "status.toggle-pinned");
			update_toggle_pinned_label ();
			pin_menu_item.set_attribute_value ("hidden-when", "action-disabled");

			menu_model.append_item (pin_menu_item);
			menu_model.append_item (mute_menu_item);
			menu_model.append_item (unmute_menu_item);

			if (
				accounts.active.tuba_api_versions.mastodon >= 7
				&& (status.formal.visibility == "public" || status.formal.visibility == "unlisted")
				&& status.formal.quote_approval != null
			) {
				var quote_policy_submenu = new GLib.Menu ();
				quote_policy_submenu.append (
					API.Status.QuotePolicy.PUBLIC.to_title (),
					@"status.change-quote-policy('$(API.Status.QuotePolicy.PUBLIC.to_string ())')"
				);
				quote_policy_submenu.append (
					API.Status.QuotePolicy.FOLLOWERS.to_title (),
					@"status.change-quote-policy('$(API.Status.QuotePolicy.FOLLOWERS.to_string ())')"
				);
				quote_policy_submenu.append (
					API.Status.QuotePolicy.NOBODY.to_title (),
					@"status.change-quote-policy('$(API.Status.QuotePolicy.NOBODY.to_string ())')"
				);

				// translators: quote policy menu in post menus. You may translate it as
				//				"quote privacy"
				menu_model.append_submenu (_("Quote Policy"), quote_policy_submenu);
			}

			menu_model.append (_("Edit"), "status.edit-status");
			menu_model.append (_("Delete"), "status.delete-status");
		} else {
			menu_model.append_item (mute_menu_item);
			menu_model.append_item (unmute_menu_item);

			if (
				(
					(accounts.active.instance_info != null && accounts.active.instance_info.tuba_can_translate)
					|| InstanceAccount.InstanceFeatures.TRANSLATION in accounts.active.tuba_instance_features
				)
				&& status.formal.tuba_translatable
				&& (
					status.formal.visibility == "public"
					|| status.formal.visibility == "unlisted"
				)
				&& Tuba.default_locale != status.formal.language
				&& (
					Tuba.default_locale != "en-US"
					|| status.formal.language != "en"
				)
			) {
				var translate_menu_item = new GLib.MenuItem (_("Translate"), "status.translate");
				translate_menu_item.set_attribute_value ("hidden-when", "action-disabled");

				// translators: Post menu items to revert a translation
				var show_original_menu_item = new GLib.MenuItem (_("Show Original"), "status.show-original");
				show_original_menu_item.set_attribute_value ("hidden-when", "action-disabled");

				// We need 2 items because we apparently
				// can't update labels
				menu_model.append_item (translate_menu_item);
				menu_model.append_item (show_original_menu_item);
			}

			// translators: menu item on posts, shown when someone has quoted a post
			//				of yours and clicking it will remove your post from their
			//				quote. You may translate it as 'Revoke quote' otherwise
			var revoke_quote_menu_item = new GLib.MenuItem (_("Remove my post"), "status.revoke-quote");
			revoke_quote_menu_item.set_attribute_value ("hidden-when", "action-disabled");
			menu_model.append_item (revoke_quote_menu_item);
			menu_model.append (C_("verb", "Report"), "status.report");
		}

		context_menu = new Gtk.PopoverMenu.from_model (menu_model);
	}

	private void copy_url () {
		if (status.formal.url != null && settings.copy_private_link_reminder && (status.formal.visibility == "direct" || status.formal.visibility == "private")) {
			string body;
			if (status.formal.visibility == "direct") {
				// translators: dialog subtitle visibility warning when copying a link to a
				//				post that is private or mentioned-only
				body = _("Only mentioned people will be able to view it.");
			} else if (status.formal.account.is_self ()) {
				// translators: dialog subtitle visibility warning when copying a link to a
				//				post that is private or mentioned-only
				body = _("Only mentioned people and your followers will be able to view it.");
			} else {
				// translators: dialog subtitle visibility warning when copying a link to a post
				//				that is private or mentioned-only, the variable is a handle
				body = _("Only mentioned people and people that follow %s will be able to view it.").printf (status.formal.account.full_handle);
			}

			app.question.begin (
				// translators: confirmation dialog title, the variable is the post visibility e.g. Public, Unlisted...
				{_("Copy \"%s\" Post Link?").printf (accounts.active.visibility[status.formal.visibility].name), false},
				{ body, false },
				app.main_window,
				{ { _("Copy"), Adw.ResponseAppearance.SUGGESTED }, { _("Don't remind me again"), Adw.ResponseAppearance.DEFAULT } },
				null,
				false,
				(obj, res) => {
					if (app.question.end (res) == Tuba.Application.QuestionAnswer.NO) {
						settings.copy_private_link_reminder = false;
					}

					Utils.Host.copy (status.formal.url);
					// translators: toast shown when copying a post link to clipboard successfully
					app.toast (_("Copied post url to clipboard"));
				}
			);
		} else {
			Utils.Host.copy (status.formal.url ?? status.formal.account.url);
			app.toast (_("Copied post url to clipboard"));
		}
	}

	private void open_in_browser () {
		string final_url = status.formal.url ?? status.formal.account.url;
		#if WEBKIT
			if (settings.use_in_app_browser_if_available && Views.Browser.can_handle_url (final_url)) {
				(new Views.Browser.with_url (final_url)).present (app.main_window);
				return;
			}
		#endif

		Utils.Host.open_url.begin (final_url);
	}

	private void report_status () {
		new Dialogs.Report (status.formal.account, status.formal.id, status.formal.mentions);
	}

	private void view_edit_history () {
		app.main_window.open_view (new Views.EditHistory (status.formal.id));
	}

	private void view_stats () {
		app.main_window.open_view (new Views.StatusStats (status.formal.id, has_reactions ()));
	}

	public void on_edit (API.Status? x) {
		if (x == null) return;
		if (this.status != null) this.status.patch (x);
		bind ();
	}

	private void change_quote_policy (GLib.SimpleAction action, GLib.Variant? value) {
		if (value == null) return;
		string new_quote_policy = value.get_string ();
		change_quote_policy_real.begin (new_quote_policy, action.get_state ().get_string ());
		action.set_state (new_quote_policy);
	}

	private async void change_quote_policy_real (string new_policy, string old_policy) {
		try {
			var req = new RequestV2 (@"/api/v1/statuses/$(status.formal.id)/interaction_policy", PUT) { account = accounts.active };
			req.add_form_data ("quote_approval_policy", new_policy);
			var in_stream = yield req.exec (null);
			Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);
			var node = network.parse_node (parser);
			on_edit (API.Status.from (node));
		} catch (Error e) {
			change_quote_policy_action.set_state (old_policy);
			warning (@"Couldn't change quote policy for $(status.formal.id): $(e.code) $(e.message)");
			app.toast ("%s %s".printf (_("Error"), e.message));
		}
	}

	public signal void pin_changed ();
	private void toggle_pinned () {
		toggle_pinned_real.begin ();
	}

	private async void toggle_pinned_real () {
		var p_action = status.formal.pinned ? "unpin" : "pin";
		var req = new RequestV2 (@"/api/v1/statuses/$(status.formal.id)/$p_action", POST) { account = accounts.active };

		try {
			yield req.exec (null);
			this.status.formal.pinned = p_action == "pin";
			pin_changed ();
		} catch (Error e) {
			warning (@"Error while pin toggling: $(e.code) $(e.message)");
			app.toast ("%s: %s".printf (_("Error"), e.message));
		}
	}

	private void edit_status () {
		edit_status_real.begin ();
	}

	private async void edit_status_real (bool redraft = false) {
		RequestV2 req;
		if (redraft) {
			req = this.status.formal.annihilate ();
		} else {
			req = new RequestV2 (@"/api/v1/statuses/$(status.formal.id)/source");
		}
		req.account = accounts.active;

		try {
			var in_stream = yield req.exec (null);
			Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);
			var node = network.parse_node (parser);
			var source = API.StatusSource.from (node);

			if (redraft) {
				new Dialogs.Composer.Dialog.edit (status.formal, source, null, true);
			} else {
				new Dialogs.Composer.Dialog.edit (status.formal, source, on_edit);
			}
		} catch (Error e) {
			if (redraft) {
				warning (@"Error while deleting status for redrafting: $(e.code) $(e.message)");
				app.toast (e.message, 0);
			} else {
				new Dialogs.Composer.Dialog.edit (status.formal, null, on_edit);
			}
		}
	}

	private void revoke_quote () {
		revoke_quote_real.begin ();
	}

	private async void revoke_quote_real () {
		var res = yield app.question (
			// translators: dialog title shown when attempting to revoke your post
			//				from someone else's quote
			{_("Remove your post from the quote?"), false},
			null, app.main_window,
			{ { _("Remove"), Adw.ResponseAppearance.DESTRUCTIVE }, { _("Cancel"), Adw.ResponseAppearance.DEFAULT } },
			null, false
		);
		if (!res.truthy ()) return;

		try {
			var req = new RequestV2 (@"/api/v1/statuses/$(status.formal.quote.id)/quotes/$(status.formal.id)/revoke", POST) { account = accounts.active };
			var in_stream = yield req.exec (null);
			Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);
			var node = network.parse_node (parser);
			on_edit (API.Status.from (node));
		} catch (Error e) {
			warning (@"Couldn't revoke quote: $(e.code) $(e.message)");
			app.toast ("%s %s".printf (_("Error"), e.message));
		}
	}


	private void delete_status () {
		// translators: dialog title asking the user to confirm if they really want to remove a post
		var dlg = new Adw.AlertDialog (_("Delete Post?"), null) {
			heading_use_markup = false,
			body_use_markup = false,
			default_response = "delete",
			close_response = "cancel"
		};

		dlg.add_responses (
			"cancel", _("Cancel"),
			// translators: button on the delete post dialog that
			//				deletes the post and fills the composer
			//				with it so you can edit it
			"redraft", _("Redraft"),

			"delete", _("Delete")
		);
		dlg.set_response_appearance ("delete", Adw.ResponseAppearance.DESTRUCTIVE);
		dlg.set_response_appearance ("redraft", Adw.ResponseAppearance.DESTRUCTIVE);
		dlg.choose.begin (app.main_window, null, (obj, res) => {
			switch (dlg.choose.end (res)) {
				case "delete":
					delete_status_real.begin ();
					break;
				case "redraft":
					edit_status_real.begin (true);
					break;
				default:
					break;
			}
		});
	}

	private async void delete_status_real () {
		try {
			string s_id = this.status.formal.id;
			yield this.status.formal.annihilate ().exec (null);
			app.remove_status_widget (s_id);
		} catch (Error e) {
			warning (@"Couldn't delete status: $(e.code) $(e.message)");
			app.toast ("%s %s".printf (_("Error"), e.message));
		}
	}

	private void toggle_mute_conversation () {
		toggle_mute_conversation_real.begin ();
	}

	private async void toggle_mute_conversation_real () {
		string api_action = status.formal.muted ? "unmute" : "mute";
		var req = new RequestV2 (@"/api/v1/statuses/$(status.formal.id)/$api_action", POST) { account = accounts.active };

		try {
			yield req.exec (null);
			status.formal.muted = !status.formal.muted;
			update_mute_conversation_actions_enabled_status ();
		} catch (Error e) {
			warning (@"Couldn't $api_action $(status.formal.id): $(e.code) $(e.message)");
			app.toast (
				api_action == "mute"
				// translators: toast shown when muting a post
				? _("Couldn't Mute Conversation: %s").printf (e.message)
				// translators: toast shown when unmuting a post
				: _("Couldn't Unmute Conversation: %s").printf (e.message)
			);
		}
	}

	private void update_mute_conversation_actions_enabled_status () {
		if (mute_conversation_action == null || unmute_conversation_action == null) return;

		bool can_mute_convo = status.formal.account.is_self ();
		if (!can_mute_convo && status.formal.mentions != null && status.formal.mentions.size > 0) {
			foreach (var mention in status.formal.mentions) {
				if (mention.id == accounts.active.id) {
					can_mute_convo = true;
					break;
				}
			}
		}

		if (can_mute_convo) {
			mute_conversation_action.set_enabled (!status.formal.muted);
			unmute_conversation_action.set_enabled (status.formal.muted);
		} else {
			mute_conversation_action.set_enabled (false);
			unmute_conversation_action.set_enabled (false);
		}
	}

	private void translate () {
		translate_real.begin ();
	}

	private async void translate_real () {
		if (translation != null) {
			translation = null;
			bind ();

			translate_simple_action.set_enabled (true);
			show_original_simple_action.set_enabled (false);
			return;
		}

		if (accounts.active.instance_info.pleroma != null) {
			var req = new RequestV2 (@"/api/v1/statuses/$(status.formal.id)/translations/$(Tuba.default_locale)") {
				account = accounts.active
			};

			try {
				var in_stream = yield req.exec (null);
				Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);
				var node = network.parse_node (parser);
				var akkotrans = API.AkkomaTranslation.from (node);
				translation = new API.Translation () {
					content = akkotrans.text,
					detected_source_language = akkotrans.detected_language
				};
				bind ();

				translate_simple_action.set_enabled (false);
				show_original_simple_action.set_enabled (true);
			} catch (Error e) {
				warning (@"Couldn't translate $(status.formal.id): $(e.code) $(e.message)");
				app.toast (_("Couldn't translate: %s").printf (e.message));
			}

			return;
		}

		var req = new RequestV2 (@"/api/v1/statuses/$(status.formal.id)/translate", POST) {
			account = accounts.active
		};
		req.add_form_data ("lang", Tuba.default_locale);

		try {
			var in_stream = yield req.exec (null);
			Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);
			var node = network.parse_node (parser);
			translation = API.Translation.from (node);
			bind ();

			translate_simple_action.set_enabled (false);
			show_original_simple_action.set_enabled (true);
		} catch (Error e) {
			warning (@"Couldn't translate $(status.formal.id): $(e.code) $(e.message)");
			// translators: error toast; the variable is a string error message
			app.toast (_("Couldn't translate: %s").printf (e.message));
		}
	}

	protected string spoiler_text {
		owned get {
			var text = status.formal.spoiler_text;
			if (text == null || text == "") {
				return _("Show More");
			} else {
				if (translation != null && translation.spoiler_text != null && translation.spoiler_text != "") {
					text = translation.spoiler_text;
				}

				spoiler_text_revealed = text;
				return text;
			}
		}
	}
	public string spoiler_text_revealed { get; set; default = _("Sensitive"); }

	// separator between the bottom bar items
	const string EXPANDED_SEPARATOR = " · ";
	protected string date {
		owned get {
			if (expanded) {
				// translators: this is a "long" date format shown in places like expanded posts or
				//				the profile "Joined" field. You can find all the available specifiers
				//				on https://valadoc.org/glib-2.0/GLib.DateTime.format.html
				//				Please do not stray far from the original and only include day, month
				//				and year.
				//				If unsure, either leave it as-is or set it to %x.
				var date_local = _("%B %e, %Y");

				// Re-parse the date into a MONTH DAY, YEAR (separator) HOUR:MINUTES
				var date_parsed = new GLib.DateTime.from_iso8601 (this.full_date, null);
				date_parsed = date_parsed.to_timezone (new TimeZone.local ());

				return date_parsed.format (@"$date_local $EXPANDED_SEPARATOR %H:%M").replace (" ", ""); // %e prefixes with whitespace on single digits
			} else {
				return Utils.DateTime.humanize (this.full_date);
			}
		}
	}

	public void update_time () {
		if (expanded) return;
		if (date_label.visible) date_label.label = this.date;
		if (this.poll != null) poll.update_time ();
	}

	protected string full_date {
		get {
			return status.formal.edited_at ?? status.formal.created_at;
		}
	}

	private string formatted_full_date () {
		return (new GLib.DateTime.from_iso8601 (this.full_date, null))
			.to_timezone (new TimeZone.local ())
			.format ("%F %T");
	}

	public string title_text {
		owned get {
			return status.formal.account.display_name;
		}
	}

	public string subtitle_text {
		owned get {
			return status.formal.account.handle;
		}
	}

	public string? avatar_url {
		owned get {
			return status.formal.account.avatar;
		}
	}

	public signal void open ();
	public virtual void on_open () {
		if (status.id == "")
			on_avatar_clicked ();
		else
			on_open_unless_reveal ();
	}

	public void on_open_unless_reveal (Gtk.Button? btn = null) {
		if (btn != null
			&& (btn.get_first_child () is Gtk.Stack)
			&& (((Gtk.Stack) btn.get_first_child ()).visible_child_name != "status")
		) {
			((Gtk.Stack) btn.get_first_child ()).visible_child_name = "status";
		} else if (filter_stack.visible_child_name == "filter") {
			toggle_filter ();
		} else {
			status.open ();
		}
	}

	string? forced_kind_instigator_name;
	Widgets.Avatar? actor_avatar = null;
	ulong header_button_activate;
	private Binding actor_avatar_binding;
	string? header_kind_url = null;
	const string[] SHOULD_SHOW_ACTOR_AVATAR = {
		InstanceAccount.KIND_REBLOG,
		InstanceAccount.KIND_REMOTE_REBLOG,
		InstanceAccount.KIND_FAVOURITE
	};
	protected virtual void change_kind () {
		Tuba.InstanceAccount.Kind res_kind;

		string actor_name = this.kind_instigator.display_name;
		if (kind == InstanceAccount.KIND_REPLY) {
			actor_name = forced_kind_instigator_name;
		}

		accounts.active.describe_kind (
			this.kind,
			out res_kind,
			actor_name,
			this.kind_instigator.url,
			this.other_data
		);

		if (header_button_activate > 0) header_button.disconnect (header_button_activate);
		if (res_kind.icon == null) {
			//  status_box.margin_top = 18;
			header_icon.visible = header_button.visible = false;
			return;
		};

		header_icon.visible = header_button.visible = true;
		//  status_box.margin_top = 15;

		if (kind in SHOULD_SHOW_ACTOR_AVATAR) {
			if (actor_avatar == null) {
				actor_avatar = new Widgets.Avatar () {
					size = app.is_mobile ? 24 : 34,
					valign = Gtk.Align.START,
					halign = Gtk.Align.START,
					overflow = Gtk.Overflow.HIDDEN,
					allow_mini_profile = true
				};
				actor_avatar.add_css_class ("ttl-status-avatar-actor");

				string actor_handle;
				if (this.kind_instigator != null) {
					actor_avatar_binding = this.bind_property ("kind_instigator", actor_avatar, "account", BindingFlags.SYNC_CREATE);
					actor_avatar.mini_clicked.connect (open_kind_instigator_account);
					actor_handle = this.kind_instigator.handle;
				} else {
					actor_avatar_binding = status.bind_property ("account", actor_avatar, "account", BindingFlags.SYNC_CREATE);
					actor_avatar.mini_clicked.connect (open_status_account);
					actor_handle = status.account.handle;
				}

				// translators: Tooltip text for avatars in posts.
				//				The variable is a string user handle.
				actor_avatar.tooltip_text = _("Open %s's Mini Profile").printf (actor_handle);
			}
			avatar.add_css_class ("ttl-status-avatar-border");
			avatar_overlay.child = actor_avatar;
		} else if (actor_avatar != null) {
			actor_avatar_binding.unbind ();

			avatar_overlay.child = null;
		}

		header_icon.icon_name = res_kind.icon;
		header_label.instance_emojis = this.kind_instigator.emojis_map;
		header_label.label = res_kind.description;
		header_kind_url = res_kind.url;

		if (header_kind_url != null) {
			header_button_activate = header_button.clicked.connect (on_header_button_clicked);
		} else {
			header_button_activate = header_button.clicked.connect (on_open);
		}
	}

	private void on_header_button_clicked () {
		if (header_kind_url == this.kind_instigator.url) {
			open_kind_instigator_account ();
		} else if (header_kind_url != null) {
			header_label.on_activate_link (header_kind_url);
		}
	}

	private void open_kind_instigator_account () {
		this.kind_instigator.open ();
	}

	private void open_status_account () {
		status.account.open ();
	}

	private void update_spoiler_status () {
		spoiler_status_con.visible = status.formal.tuba_spoiler_revealed && status.formal.has_spoiler;
		spoiler_stack.visible_child_name = status.formal.tuba_spoiler_revealed ? "content" : "spoiler";
	}

	private void update_spoiler_status_no_transition () {
		Gtk.StackTransitionType old_transition = spoiler_stack.transition_type;
		spoiler_stack.transition_type = NONE;
		update_spoiler_status ();
		spoiler_stack.transition_type = old_transition;
	}

	public void show_toggle_pinned_action () {
		if (toggle_pinned_simple_action != null)
			toggle_pinned_simple_action.set_enabled (true);
	}

	private void update_toggle_pinned_label () {
		if (pin_menu_item != null && status != null && status.formal != null)
			pin_menu_item.set_label (status.formal.pinned
				// translators: Unpin post from profile
				? _("Unpin")
				// translators: Pin post on profile
				: _("Pin")
			);
	}

	private void aria_describe_status () {
		if (settings.status_aria_verbosity < 1) return;

		if (filter_stack.visible_child_name == "filter") {
			// translators: This is an accessibility label.
			//				Screen reader users are going to hear this a lot,
			//				please be mindful.
			string aria_filter = _("Filtered post");
			if (this.status.formal.tuba_filter_warn != null) {
				aria_filter += @": $(this.status.formal.tuba_filter_warn)";
			}

			this.update_property (Gtk.AccessibleProperty.LABEL, aria_filter, -1);

			return;
		}

		// translators: This is an accessibility label.
		//				Screen reader users are going to hear this a lot,
		//				please be mindful.
		//				The first variable is the post's visibility (e.g. public).
		//				The second one is the author's name and the third
		//				one is the author's handle.
		string post = _("%s post by %s (%s).").printf (
			visibility_indicator.tooltip_text,
			this.title_text,
			this.subtitle_text
		);

		string aria_date = Utils.DateTime.humanize_aria (this.full_date);
		string aria_date_prefixed = status.formal.is_edited
			// translators: This is an accessibility label.
			//				Screen reader users are going to hear this a lot,
			//				please be mindful.
			//				The variable is a string date.
			? _("Edited: %s.").printf (aria_date)
			// translators: This is an accessibility label.
			//				Screen reader users are going to hear this a lot,
			//				please be mindful.
			//				The variable is a string date.
			: _("Published: %s.").printf (aria_date);

		string aria_pinned = "";
		if (settings.status_aria_verbosity >= 2 && status.formal.pinned) {
			// translators: This is an accessibility label.
			//				Screen reader users are going to hear this a lot,
			//				please be mindful.
			aria_pinned = _("The post is pinned.");
		}

		string aria_quote = "";
		if (settings.status_aria_verbosity >= 2 && status.formal.quote != null) {
			// translators: This is an accessibility label.
			//				Screen reader users are going to hear this a lot,
			//				please be mindful.
			aria_quote = _("Contains a quoted post.");
		}

		string aria_attachments = "";
		if (settings.status_aria_verbosity >= 3 && status.formal.has_media) {
			aria_attachments = GLib.ngettext (
				// translators: This is an accessibility label.
				//				Screen reader users are going to hear this a lot,
				//				please be mindful.
				//				The variable is the number of attachments the post has.
				"Contains %d attachment.", "Contains %d attachments.",
				(ulong) status.formal.media_attachments.size
			).printf (status.formal.media_attachments.size);
		}

		string aria_poll = "";
		if (settings.status_aria_verbosity >= 3 && status.formal.poll != null) {
			aria_poll = status.formal.poll.expired
				// translators: This is an accessibility label.
				//				Screen reader users are going to hear this a lot,
				//				please be mindful.
				? _("Contains an expired poll.")
				// translators: This is an accessibility label.
				//				Screen reader users are going to hear this a lot,
				//				please be mindful.
				: _("Contains a poll.");
		}

		string aria_spoiler = "";
		if (settings.status_aria_verbosity >= 2 && status.formal.has_spoiler) {
			if (status.formal.spoiler_text == null || status.formal.spoiler_text == "") {
				// translators: This is an accessibility label.
				//				Screen reader users are going to hear this a lot,
				//				please be mindful.
				aria_spoiler = _("The post has a spoiler.");
			} else {
				// translators: This is an accessibility label.
				//				Screen reader users are going to hear this a lot,
				//				please be mindful.
				//				The variable is the spoiler title.
				aria_spoiler = _("The post has the following spoiler: %s.").printf (this.spoiler_text);
			}
		}

		string aria_app = "";
		if (settings.status_aria_verbosity >= 3 && status.formal.application != null) {
			// translators: This is an accessibility label.
			//				Screen reader users are going to hear this a lot,
			//				please be mindful.
			//				The variable is the name of the app the post was
			//				made with (e.g. Tuba).
			aria_app = _("Posted using %s.").printf (status.formal.application.name);
		}

		string aria_reactions = "";
		if (settings.status_aria_verbosity >= 2 && status.formal.compat_status_reactions != null && status.formal.compat_status_reactions.size > 0) {
			aria_reactions = GLib.ngettext (
				// translators: This is an accessibility label.
				//				Screen reader users are going to hear this a lot,
				//				please be mindful.
				//				The variable is the amount of reactions the post
				//				has.
				"Contains %d reaction.", "Contains %d reactions.",
				(ulong) status.formal.compat_status_reactions.size
			).printf (status.formal.compat_status_reactions.size);
		}

		string aria_stats = "";
		if (settings.status_aria_verbosity >= 3) {
			// translators: This is an accessibility label.
			//				Screen reader users are going to hear this a lot,
			//				please be mindful.
			//				The variables are strings <amount> replies,
			//				<amount> boosts, <amount> favorites.
			aria_stats = _("Post stats: %s, %s, %s, %s.").printf (
				// translators: This is an accessibility label. Screen reader users are going to hear this a lot, please be mindful;
				//				the variable is a string representation of the amount of replies
				GLib.ngettext (
					"%s reply",
					"%s replies",
					(ulong) status.formal.replies_count
				).printf (status.formal.replies_count.to_string ()),
				// translators: This is an accessibility label. Screen reader users are going to hear this a lot, please be mindful;
				//				the variable is a string representation of the amount of boosts
				GLib.ngettext (
					"%s boost",
					"%s boosts",
					(ulong) status.formal.reblogs_count
				).printf (status.formal.reblogs_count.to_string ()),
				// translators: This is an accessibility label. Screen reader users are going to hear this a lot, please be mindful;
				//				the variable is a string representation of the amount of quotes
				GLib.ngettext (
					"%s quote",
					"%s quotes",
					(ulong) status.formal.quotes_count
				).printf (status.formal.quotes_count.to_string ()),
				// translators: This is an accessibility label. Screen reader users are going to hear this a lot, please be mindful;
				//				the variable is a string representation of the amount of favorites
				GLib.ngettext (
					"%s favorite",
					"%s favorites",
					(ulong) status.formal.favourites_count
				).printf (status.formal.favourites_count.to_string ())
			);
		}

		string aria_translation = "";
		if (settings.status_aria_verbosity >= 3 && translation_label != null) aria_translation = translation_label.get_text ();

		this.update_property (
			Gtk.AccessibleProperty.LABEL,
			"%s %s %s %s %s %s %s %s %s %s %s %s.".printf (
				header_label.accessible_text,
				post,
				aria_date_prefixed,
				aria_pinned,
				aria_quote,
				aria_attachments,
				aria_poll,
				aria_spoiler,
				aria_stats,
				aria_reactions,
				aria_app,
				aria_translation
			),
			-1
		);

		spoiler_status_con.update_property (Gtk.AccessibleProperty.LABEL, aria_spoiler, -1);

		if (quoted_status_btn != null) {
			// translators: This is an accessibility label.
			//				Screen reader users are going to hear this a lot,
			//				please be mindful.
			//				This is used to announce the quoted post
			quoted_status_btn.update_property (Gtk.AccessibleProperty.DESCRIPTION, _("Quoted Post"), -1);
		}
	}

	private Widgets.HashtagBar hashtag_bar;
	protected Widgets.PreviewCard prev_card;
	private Widgets.CollectionButton collection_card;
	private Widgets.Attachment.Box attachments;
	private Gtk.Label translation_label;
	public Widgets.VoteBox? poll = null;
	private Gtk.Image? local_only_indicator = null;
	const string[] ALLOWED_CARD_TYPES = { "link", "video" };
	ulong[] formal_handler_ids = {};
	ulong[] this_handler_ids = {};
	Binding[] bindings = {};
	protected virtual void bind () {
		soft_unbind ();

		if (this.status.formal.filtered != null && this.status.formal.filtered.size > 0 && !expanded) {
			filter_stack.visible_child_name = "filter";

			string? filter_warn = this.status.formal.tuba_filter_warn;
			if (filter_warn != null) {
				// translators: post filter message; the variable is a string filter title
				filter_label.label = _("Filtered: %s").printf (filter_warn);
			} else {
				// translators: post default filter message
				filter_label.label = _("Filtered");
			}
		}

		if (actions != null) {
			actions.unbind ();
			content_column.remove (actions);
		}
		actions = new ActionsRow (this.status.formal);
		actions.reply.connect (on_reply_button_clicked);
		content_column.append (actions);
		toggle_actions_visibility (true);

		this.content.bold_text_regex = status.tuba_search_query_regex;
		bool has_quote = status.formal.quote != null;// && status.formal.quote.tuba_has_quote;
		this.content.has_quote = has_quote || status.formal.tuba_had_quote;
		this.content.mentions = status.formal.mentions;
		this.content.instance_emojis = status.formal.emojis_map;

		if (translation != null && translation.content != null && translation.content != "") {
			this.content.content = translation.content;
		} else {
			this.content.content = status.formal.content;
		}

		if (quoted_status_btn != null) content_box.remove (quoted_status_btn);
		if (quote_warning != null) content_box.remove (quote_warning);
		if ((has_quote && !is_quote) || status.formal.tuba_had_quote) {
			if (status.formal.quote != null && status.formal.quote.tuba_has_quote) {
				var quoted_status = (Widgets.Status) status.formal.quote.to_widget ();
				quoted_status.is_quote = true;
				quoted_status.add_css_class ("frame");
				quoted_status.add_css_class ("ttl-quote");
				quoted_status.overflow = HIDDEN;

				var qstack = new Gtk.Stack () {
					vhomogeneous = false,
					hhomogeneous = false,
					transition_type = Gtk.StackTransitionType.CROSSFADE,
					interpolate_size = true
				};
				qstack.add_named (quoted_status, "status");

				quoted_status_btn = new Gtk.Button () {
					child = qstack,
					css_classes = { "ttl-flat-button", "flat" }
				};
				quoted_status_btn.clicked.connect (quoted_status.on_open_unless_reveal);
				content_box.append (quoted_status_btn);

				if (status.formal.quote.state != "accepted" || (status.formal.quote.account != null && status.formal.quote.account.limited)) {
					// translators: placeholder message when a quote has
					//				been hidden for an unknown reason;
					//				should never be visible to users
					string label = _("Quote is hidden");
					if (status.formal.quote.state != "accepted") {
						switch (status.formal.quote.state) {
							case "blocked_account":
							// translators: label shown when the quote is hidden
							//				inside a post for the described reason;
							//				the variable is a string account handle
							//				or domain
								label = _("This quote is hidden because you've blocked %s").printf (status.formal.quote.account.handle);
								break;
							case "blocked_domain":
								label = _("This quote is hidden because you've blocked %s").printf (status.formal.quote.account.domain);
								break;
							case "muted_account":
							// translators: label shown when the quote is hidden
							//				inside a post for the described reason;
							//				the variable is a string account handle
								label = _("This quote is hidden because you've muted %s").printf (status.formal.quote.account.handle);
								break;
						}
					} else {
						// translators: label shown when a quote has been hidden
						//				because the user's instance limits the
						//				author of the quote. moderators refers
						//				to the user instance's admins
						label = _("This quote is hidden because the account has been hidden by the moderators");
					}

					var qbtn = new Gtk.Label (label) {
						wrap = true,
						wrap_mode = CHAR,
						tooltip_text = _("Show Anyway"),
						css_classes = {"border-radius-6", "frame", "ttl-quote-state"}
					};
					qstack.add_named (qbtn, "hidden-btn");
					qstack.visible_child_name = "hidden-btn";
				}
			} else {
				// translators: label shown when the quote can't be displayed
				//				inside a post
				string label = _("Quote Unavailable");
				if (status.formal.tuba_had_quote) {
					// translators: label shown when a quote is quoting a post
					//				that we won't display
					label = _("Quoted a post");
				} else {
					switch (status.formal.quote.state) {
						case "pending":
						// translators: label shown when the quote can't be displayed
						//				inside a post
							label = _("Quote Pending");
							break;
						case "revoked":
						case "deleted":
						// translators: label shown when the quote can't be displayed
						//				inside a post
							label = _("Quote removed by author");
							break;
					}
				}

				quote_warning = new Gtk.Label (label) {
					wrap = true,
					wrap_mode = Pango.WrapMode.WORD_CHAR,
					css_classes = {"warning", "border-radius-6", "frame", "ttl-quote-state"}
				};
				content_box.append (quote_warning);
			}
		}

		if (emoji_reactions != null) content_column.remove (emoji_reactions);
		if (status.formal.compat_status_reactions != null) {
			emoji_reactions = new ReactionsRow (status.formal.id, status.formal.compat_status_reactions);
			content_column.insert_child_after (emoji_reactions, spoiler_stack);
		}

		spoiler_label.label = this.spoiler_text;
		spoiler_label_rev.label = this.spoiler_text_revealed;

		status.formal.tuba_spoiler_revealed = !status.formal.has_spoiler || status.formal.tuba_spoiler_revealed;
		update_spoiler_status_no_transition ();

		handle_label.label = handle_label.tooltip_text = this.subtitle_text;
		date_label.label = this.date;

		if (!expanded) {
			date_label.tooltip_text = formatted_full_date ();
			date_label.update_property (Gtk.AccessibleProperty.LABEL, date_label.tooltip_text, -1);
		}

		pin_indicator.visible = status.formal.pinned;
		update_toggle_pinned_label ();
		edited_indicator.visible = status.formal.is_edited;
		edit_history_simple_action.set_enabled (status.formal.is_edited);

		if (accounts.active.visibility.has_key (status.formal.visibility)) {
			visibility_indicator.visible = true;
			var t_visibility = accounts.active.visibility[status.formal.visibility];
			visibility_indicator.icon_name = t_visibility.small_icon_name;
			visibility_indicator.tooltip_text = t_visibility.name;
			visibility_indicator.update_property (Gtk.AccessibleProperty.LABEL, t_visibility.name, -1);
		} else {
			visibility_indicator.visible = false;
		}

		if (change_background_on_direct && status.formal.visibility == "direct") {
			this.add_css_class ("direct");
		} else {
			this.remove_css_class ("direct");
		}

		if (local_only_indicator != null) indicators.remove (local_only_indicator);
		if (status.formal.compat_local_only) {
			this.add_css_class ("local");

			if (local_only_indicator == null) local_only_indicator = new Gtk.Image.from_icon_name ("tuba-important-small-symbolic") {
				css_classes = {"dim-label"},
				tooltip_text = _("Local Only")
			};
			indicators.prepend (local_only_indicator);
		} else {
			this.remove_css_class ("local");
		}

		avatar.account = status.formal.account;
		// translators: Tooltip text for avatars in posts.
		//				The variable is a string user handle.
		name_button.tooltip_text = avatar.tooltip_text = _("Open %s's Profile").printf (status.formal.account.handle);

		name_label.instance_emojis = status.formal.account.emojis_map;
		name_label.label = title_text;

		if (poll != null) content_box.remove (poll);
		if (status.formal.poll != null) {
			poll = new Widgets.VoteBox ();
			poll.translation = translation;
			bindings += status.formal.bind_property ("poll", poll, "poll", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
			content_box.append (poll);
		}

		if (attachments != null) content_box.remove (attachments);
		if (status.formal.has_media) {
			if (translation != null && translation.media_attachments != null && translation.media_attachments.size > 0) {
				var translated_media = new Gee.HashMap<string, string> ();
				translation.media_attachments.@foreach (e => {
					translated_media.set (e.id, e.description);
					return true;
				});

				status.formal.media_attachments.@foreach (item => {
					if (translated_media.has_key (item.id)) {
						item.tuba_translated_alt_text = translated_media.get (item.id);
					}

					return true;
				});
			} else {
				status.formal.media_attachments.@foreach (item => {
					item.tuba_translated_alt_text = null;
					return true;
				});
			}

			attachments = new Widgets.Attachment.Box ();
			attachments.has_spoiler = status.formal.sensitive;
			attachments.list = status.formal.media_attachments;

			#if GSTREAMER
				if (attachments != null && attachments.has_thumbnailess_audio) {
					bindings += avatar.bind_property ("custom-image", attachments, "audio-fallback-paintable", BindingFlags.SYNC_CREATE);
				}
			#endif

			content_box.append (attachments);
		}

		if (translation_label != null) content_box.remove (translation_label);
		if (translation != null && translation.provider != "") {
			string translation_label_content;
			if (translation.detected_source_language != "") {
				// translatiors: the first variable is a language name,
				// 				 the second variable is a service name e.g. DeepL
				translation_label_content = _("Translated from %s using %s").printf (translation.detected_source_language.up (), translation.provider);
			} else {
				// translatiors: the variable is a service name e.g. DeepL
				translation_label_content = _("Translated using %s").printf (translation.provider);
			}

			translation_label = new Gtk.Label (translation_label_content) {
				wrap = true,
				wrap_mode = Pango.WrapMode.WORD_CHAR,
				xalign = 1.0f,
				css_classes = {"subtitle"}
			};
			content_box.append (translation_label);
		}

		if (prev_card != null) content_box.remove (prev_card);
		if (collection_card != null) content_box.remove (collection_card);
		if (status.formal.tagged_collections != null && status.formal.tagged_collections.size > 0) {
			collection_card = new Widgets.CollectionButton (status.formal.tagged_collections.get (0));
			content_box.append (collection_card);
		} else if (settings.show_preview_cards && !status.formal.has_media && quoted_status_btn == null && status.formal.card != null && status.formal.card.kind in ALLOWED_CARD_TYPES) {
			prev_card = (Widgets.PreviewCard) status.formal.card.to_widget ();
			prev_card.button.clicked.connect (open_card_url);
			content_box.append (prev_card);
		}

		if (hashtag_bar != null) content_box.remove (hashtag_bar);
		if (this.content.get_extracted_tags () != null && this.content.get_extracted_tags ().length > 0) {
			hashtag_bar = new Widgets.HashtagBar (this.content.get_extracted_tags ());
			content_box.append (hashtag_bar);
		}

		show_view_stats_action ();
		formal_handler_ids += status.formal.notify["reblogs-count"].connect (show_view_stats_action);
		formal_handler_ids += status.formal.notify["favourites-count"].connect (show_view_stats_action);
		formal_handler_ids += status.formal.notify["tuba-thread-role"].connect (install_thread_line);
		formal_handler_ids += status.formal.notify["tuba-spoiler-revealed"].connect (update_spoiler_status);

		aria_describe_status ();
		update_mute_conversation_actions_enabled_status ();
	}

	public void soft_unbind () {
		filter_stack.visible_child_name = "status";

		foreach (var handler_id in formal_handler_ids) {
			status.formal.disconnect (handler_id);
		}
		formal_handler_ids = {};

		foreach (var handler_id in this_handler_ids) {
			this.disconnect (handler_id);
		}
		this_handler_ids = {};

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

	void open_card_url () {
		status.formal.card.open_special_card ();
	}

	private void on_reply (API.Status? x) {
		if (x == null) return;
		if (reply_cb != null)
			reply_cb (x);
	}

	private void on_reply_button_clicked () {
		new Dialogs.Composer.Dialog.reply (status.formal, on_reply);
	}

	[GtkCallback] public void on_fade_reveal () {
		fade_bin.reveal_animated ();
	}

	[GtkCallback] public void toggle_spoiler () {
		status.formal.tuba_spoiler_revealed = !status.formal.tuba_spoiler_revealed;
		if (status.formal.tuba_spoiler_revealed) {
			content.grab_focus ();
		} else {
			spoiler_button.grab_focus ();
		}
	}

	[GtkCallback] public void toggle_filter () {
		if (this.status.formal.filtered != null && this.status.formal.filtered.size > 0) {
			filter_stack.visible_child_name = filter_stack.visible_child_name == "filter" ? "status" : "filter";
			aria_describe_status ();
		}
	}

	[GtkCallback] public void on_avatar_clicked () {
		status.formal.account.open ();
	}

	public void to_display_only () {
		if (poll != null) {
			poll.usable = false;
			poll.can_target = false;
			poll.focusable = false;
			poll.can_focus = false;
		}
		if (hashtag_bar != null) hashtag_bar.to_display_only ();
		if (attachments != null) attachments.usable = false;
		if (emoji_reactions != null) emoji_reactions.visible = false;
		this.can_be_opened = false;
		toggle_actions_visibility (false);
		this.menu_button.visible = false;
		this.activatable = false;
		this.avatar.allow_mini_profile = false;
		this.avatar.can_target = false;
		this.avatar.focusable = false;
		this.avatar.can_focus = false;
		name_button.can_target = false;
		name_button.can_focus = false;
		name_button.focusable = false;
		this.content.selectable = true;
		this.content.can_open = false;
		if (quoted_status_btn != null) {
			quoted_status_btn.can_target = false;
			quoted_status_btn.can_focus = false;
			quoted_status_btn.focusable = false;
		}
		if (prev_card != null) prev_card.visible = false;
		if (collection_card != null) collection_card.visible = false;
		this.avatar.accessible_role = Gtk.AccessibleRole.PRESENTATION;
		this.date_label.accessible_role = Gtk.AccessibleRole.PRESENTATION;
		name_button.accessible_role = Gtk.AccessibleRole.PRESENTATION;
		//  this.fade_bin.reveal = true;
		this.reset_relation (DESCRIBED_BY);
		this.reset_property (LABEL);
		this.reset_property (DESCRIPTION);
	}

	bool expanded = false;
	public void expand_root () {
		if (expanded) return;

		expanded = true;
		content.selectable = true;
		content.add_css_class ("ttl-large-body");
		filter_stack.visible_child_name = "status";

		// Move the avatar & thread line into the name box
		status_box.remove (avatar_side);
		title_box.prepend (avatar_side);
		title_box.spacing = 14;

		// Make the name box take 2 rows
		name_wrapbox.child_spacing = 600;
		content_side.spacing = 10;

		// Remove the date & indicators
		indicators.remove (date_label);
		if (status.formal.is_edited)
			indicators.remove (edited_indicator);
		indicators.remove (visibility_indicator);
		if (local_only_indicator != null) local_only_indicator.visible = false;

		date_label.label = this.date;
		date_label.wrap = true;
		date_label.tooltip_text = null;

		// The bottom bar
		var bottom_info = new Adw.WrapBox () {
			margin_top = 6
		};

		// Insert it after the post content
		content_column.insert_child_after (bottom_info, spoiler_stack);
		bottom_info.append (date_label);
		if (status.formal.is_edited)
			append_to_bottom_bar (bottom_info, edited_indicator);
		append_to_bottom_bar (bottom_info, visibility_indicator);

		edited_indicator.valign = Gtk.Align.CENTER;
		visibility_indicator.valign = Gtk.Align.CENTER;

		// If the application used to make the post is available
		if (status.formal.application != null) {
			var has_link = status.formal.application.website != null;
			// Make it an anchor if it has a website
			var application_link = has_link
				? @"<a href=\"$(status.formal.application.website)\">$(status.formal.application.name)</a>"
				: status.formal.application.name;
			var application_label = new Gtk.Label (application_link) {
				wrap = true,
				use_markup = has_link,
				halign = Gtk.Align.START,
				css_classes = { "body" }
			};

			// If it's not an anchor, it should follow the styling of the other items
			if (!has_link) application_label.add_css_class ("dim-label");

			append_to_bottom_bar (bottom_info, application_label);
		}
	}

	private void append_to_bottom_bar (Adw.WrapBox wrap_box, Gtk.Widget child, string separator = EXPANDED_SEPARATOR) {
		wrap_box.append (new Gtk.Label (separator) { css_classes = {"dim-label"}, halign = Gtk.Align.START });
		wrap_box.append (child);
	}

	public void patch_stats (API.Status x) {
		if (status.formal.favourites_count != x.formal.favourites_count)
			status.formal.favourites_count = x.formal.favourites_count;
		if (status.formal.reblogs_count != x.formal.reblogs_count)
			status.formal.reblogs_count = x.formal.reblogs_count;
		if (status.formal.replies_count != x.formal.replies_count)
			status.formal.replies_count = x.formal.replies_count;
		if (status.formal.quotes_count != x.formal.quotes_count)
			status.formal.quotes_count = x.formal.quotes_count;
	}

	const float THREAD_WIDTH = 4f;
	public override void snapshot (Gtk.Snapshot snapshot) {
		if (!expanded && enable_thread_lines && status.formal.tuba_thread_role != NONE && filter_stack.visible_child_name == "status") {
			float y;
			float height;

			// Get the avatar's position
			Graphene.Point avatar_point;
			avatar.compute_point (
				this,
				Graphene.Point () { x = 0.0f, y=0.0f },
				out avatar_point
			);

			// NOTE: we need the thread line to be > status height as
			//		 it looks better if it reaches the status' bounds
			//       so the sizes below are either always bigger or
			//		 start at a negative point
			switch (status.formal.tuba_thread_role) {
				// Thread starter line needs to start from the
				// center of the avatar and end at the end of
				// the status
				case START:
					y = avatar_point.y + avatar.get_height () / 2f;
					height = (float) this.get_height ();
					break;
				// Thread in-between line needs to start from the
				// top and end at the end of the status
				case MIDDLE:
					y = -4f;
					height = this.get_height () * 1.2f;
					break;
				// Thread end line needs to start from the
				// status top and end at the center of the
				// avatar
				case END:
					y = -4f;
					height = (avatar_point.y + avatar.get_height () / 2f) + 4f;
					break;
				default:
					assert_not_reached ();
			}

			var line_rect = Graphene.Rect () {
				// we need the center of the avatar for the x point minus half the thread line width
				origin = Graphene.Point () { x = avatar_point.x + avatar.get_width () / 2f - THREAD_WIDTH / 2f, y = y },
				size = Graphene.Size () { width = THREAD_WIDTH, height = height }
			};

			snapshot.push_opacity (0.1);
			snapshot.append_color (this.get_color (), line_rect);
			snapshot.pop ();
		}

		base.snapshot (snapshot);
	}

	// Threads
	public void install_thread_line () {
		this.queue_draw ();
	}
}
