[GtkTemplate (ui = "/dev/geopjr/Tuba/ui/dialogs/composer.ui")]
public class Tuba.Dialogs.Composer.Dialog : Adw.Dialog {
	const string LOCAL_ONLY_GLITCH_SUFFIX = " 👁️";

	~Dialog () {
		debug ("Destroying Composer");
		if (quote_paste_cancellable != null) quote_paste_cancellable.cancel ();
		if (inline_quote_widget != null) inline_quote_widget = null;
		if (inline_quote_widget_animation != null) inline_quote_widget_animation = null;
	}

	[GtkChild] private unowned Gtk.Label counter_label;
	[GtkChild] private unowned PostButton post_btn;
	[GtkChild] private unowned Gtk.Box btns_box;
	[GtkChild] private unowned Gtk.Box dropdowns_box;
	[GtkChild] private unowned Gtk.Grid grid;
	[GtkChild] private unowned Adw.ToastOverlay toast_overlay;
	[GtkChild] private unowned Adw.NavigationView nav_view;
	[GtkChild] private unowned Adw.ToolbarView toolbar_view;
	[GtkChild] private unowned Composer.Components.DropOverlay drop_overlay;
	[GtkChild] private unowned Adw.NavigationPage nav_page;

	[GtkChild] private unowned Gtk.ScrolledWindow scroller;
	[GtkChild] private unowned Adw.HeaderBar headerbar;
	[GtkChild] private unowned Gtk.Revealer cw_revealer;

	[GtkChild] private unowned Gtk.MenuButton native_emojis_button;
	[GtkChild] private unowned Gtk.MenuButton custom_emojis_button;
	[GtkChild] private unowned Gtk.ToggleButton cw_button;
	[GtkChild] private unowned Gtk.Entry cw_entry;
	[GtkChild] private unowned Gtk.ToggleButton poll_button;
	[GtkChild] private unowned Gtk.ToggleButton sensitive_media_button;
	[GtkChild] private unowned Gtk.Button add_media_button;

	public class PostButton : Adw.Bin {
		public bool allow_dynamic_name { get; set; default = false; }
		public signal void clicked ();

		~PostButton () {
			debug ("Destroying PostButton");
		}

		string original_label = "";
		Gtk.Widget? label_widget = null;
		public void install (string label, bool with_menu, bool can_update_draft = true, bool can_schedule = true, bool can_have_drafts = true) {
			if (this.child != null) return;
			original_label = label;

			if (with_menu) {
				var menu_model = new GLib.Menu ();
				menu_model.append (
					can_update_draft
					// translators: 'Draft' is a noun; entry in composer post menu to
					//				edit an already draft post
					? _("Edit Draft Post")

					// translators: 'Draft' is a verb; entry in composer post menu
					: _("Draft Post"),
					"composer.draft"
				);

				if (can_schedule) {
					// translators: 'Schedule' is a verb; entry in composer post menu
					menu_model.append (_("Schedule Post…"), "composer.schedule");
				}

				if (can_have_drafts && !can_update_draft) {
					// translators: plural of "Drafted Posts"; you may change it to "Drafted Posts…"; entry in composer post menu; opens a list of all draft posts
					menu_model.append (_("Drafts…"), "composer.drafted-posts");
				}

				label_widget = new Adw.SplitButton () {
					label = label,
					menu_model = menu_model,
					css_classes = { "pill", "suggested-action" }
				};
				((Adw.SplitButton) label_widget).clicked.connect (on_clicked);
				this.child = label_widget;
			} else {
				label_widget = new Gtk.Label (label) {
					ellipsize = END
				};
				var btn = new Gtk.Button () {
					css_classes = { "small-pill", "suggested-action" },
					child = label_widget
				};
				btn.clicked.connect (on_clicked);
				this.child = btn;
			}
		}

		public void update_label (string? new_label) {
			if (label_widget == null || !allow_dynamic_name) return;
			if (label_widget is Adw.SplitButton) {
				((Adw.SplitButton) label_widget).label = new_label == null ? original_label : new_label;
			} else if (label_widget is Gtk.Label) {
				((Gtk.Label) label_widget).label = new_label == null ? original_label : new_label;
			}
		}

		bool drafts_default = false;
		public void show_drafts_default () {
			if (this.child == null || !(label_widget is Adw.SplitButton) || drafts_default) return;
			this.child = new Gtk.Button () {
				action_name = "composer.drafted-posts",
				css_classes = { "small-pill", "suggested-action" },
				child = new Gtk.Label (_("Drafts…")) {
					ellipsize = END
				}
			};
			drafts_default = true;
			this.sensitive = true;
		}

		private void on_sensitive_changed () {
			if (!drafts_default || this.child == label_widget || !(label_widget is Adw.SplitButton)) return;
			drafts_default = false;
			this.child = label_widget;
			this.sensitive = !this.sensitive; // trigger
		}

		// we are 'overriding' the sensitivity setter
		// so if show_drafts_default was ran, we will
		// remove it here and instead use the normal
		// button
		public void set_sensitive_real (bool val) {
			if (drafts_default) on_sensitive_changed ();
			this.sensitive = val;
		}

		private void on_clicked () {
			clicked ();
		}
	}

	public struct Precompose {
		string? content;
		string? spoiler;
		string? quote_id;
		string? scheduled_id;
		string? in_reply_to_id;
		API.Poll? poll;
		Gee.ArrayList<API.Attachment>? media_attachments;
		bool sensitive_media;
		bool local_only;
		bool force_cursor_at_start;
	}

	private bool _is_narrow = false;
	public bool is_narrow {
		get {
			return _is_narrow;
		}
		set {
			Gtk.GridLayout layout_manager = (Gtk.GridLayout) grid.get_layout_manager ();
			Gtk.GridLayoutChild counter_layout_child = (Gtk.GridLayoutChild) layout_manager.get_layout_child (counter_label);
			Gtk.GridLayoutChild post_layout_child = (Gtk.GridLayoutChild) layout_manager.get_layout_child (post_btn);
			Gtk.GridLayoutChild btns_layout_child = (Gtk.GridLayoutChild) layout_manager.get_layout_child (btns_box);

			if (value) {
				post_layout_child.column = 1;
				post_layout_child.row = 1;
				post_layout_child.row_span = 1;

				counter_layout_child.row = 0;
				counter_layout_child.column = 1;

				btns_layout_child.column_span = 1;

				counter_label.margin_end = 10;
				counter_label.margin_start = 0;
				grid.row_spacing = 12;
				editor.margin_start = cw_entry.margin_start = grid.margin_start = 16;
				editor.margin_end = cw_entry.margin_end = grid.margin_end = 16;
			} else {
				post_layout_child.column = 2;
				post_layout_child.row = 0;
				post_layout_child.row_span = 2;

				counter_layout_child.row = 1;
				counter_layout_child.column = 1;

				btns_layout_child.column_span = 2;

				counter_label.margin_end = 0;
				counter_label.margin_start = 12;
				grid.row_spacing = 16;
				editor.margin_start = cw_entry.margin_start = grid.margin_start = 32;
				editor.margin_end = cw_entry.margin_end = grid.margin_end = 32;
			}

			_is_narrow = value;
		}
	}
	protected int64 char_limit { get; set; default = 500; }
	protected int64 cw_count { get; set; default = 0; }
	protected int8 reserved_chars { get; set; default = 0; }

	private int64 _remaining_chars = 500;
	protected int64 remaining_chars {
		get {
			return _remaining_chars;
		}
		set {
			_remaining_chars = value;
			counter_label.label = counter_label.tooltip_text = char_limit >= 1000 ? value.to_string () : @"$value / $char_limit";
			validate_post_button ();

			if (value < 0) {
				counter_label.add_css_class ("error");
				counter_label.remove_css_class ("accent");

				// translators: screen reader announcement when the composer
				//				passed the character limit. The variable is
				//				the amount of characters the user exceeded
				//				the limit by.
				this.announce (_("Exceeded character limit by %lld").printf (value.abs ()), HIGH);
			} else if (counter_label.has_css_class ("error")) {
				counter_label.remove_css_class ("error");
				counter_label.add_css_class ("accent");
			}
		}
	}

	private bool edit_mode { get; set; default = false; }
	private string? schedule_iso8601 { get; set; default=null; }
	private string? in_reply_to_id { get; set; default = null; }
	public string? quote_id { get; set; default = null; }
	public string? scheduled_id { get; set; default = null; }
	public string? edit_status_id { get; set; default = null; }
	private bool quote_limited { get; set; default = false; }

	private void install_emoji_pickers () {
		var emoji_picker = new Gtk.EmojiChooser ();
		native_emojis_button.popover = emoji_picker;
		emoji_picker.emoji_picked.connect (editor.insert_string_at_cursor);

		if (accounts.active.instance_emojis != null && accounts.active.instance_emojis.size > 0) {
			var custom_emoji_picker = new Widgets.CustomEmojiChooserV2 ();
			custom_emojis_button.popover = custom_emoji_picker;
			custom_emoji_picker.emoji_picked.connect (editor.insert_string_at_cursor);
		}
	}

	private Composer.Components.Editor editor;
	private void install_editor () {
		editor = new Dialogs.Composer.Components.Editor () {
			margin_end = 32,
			margin_start = 32
		};

		scroller.overflow = HIDDEN;
		scroller.child = editor;

		editor.edit_mode = this.edit_mode;
		editor.toast.connect (on_toast);
		editor.push_subpage.connect (on_push_subpage);
		editor.pop_subpage.connect (on_pop_subpage);
		editor.paste_clipboard.connect (on_paste);
		editor.ctrl_return_pressed.connect (on_commit);
		editor.notify["char-count"].connect (update_remaining_chars);
		this.focus_widget = editor;
	}

	private void update_remaining_chars () {
		remaining_chars = this.char_limit - editor.char_count - this.cw_count - this.reserved_chars;
	}

	protected Gtk.DropDown visibility_button;
	protected Gtk.DropDown language_button;
	protected Gtk.DropDown? content_type_button = null;
	protected Gtk.DropDown? quote_policy_button = null;
	protected Gtk.ToggleButton? local_only_button = null;

	private void append_dropdown (Gtk.DropDown dropdown) {
		var togglebtn = dropdown.get_first_child ();
		if (togglebtn != null) {
			togglebtn.add_css_class ("flat");
		}

		dropdowns_box.append (dropdown);
	}

	protected void install_visibility (string default_visibility) {
		visibility_button = new Gtk.DropDown (accounts.active.visibility_list, null) {
			expression = new Gtk.PropertyExpression (typeof (InstanceAccount.Visibility), null, "name"),
			factory = new Gtk.BuilderListItemFactory.from_resource (null, @"$(Build.RESOURCES)gtk/dropdown/icon.ui"),
			list_factory = new Gtk.BuilderListItemFactory.from_resource (null, @"$(Build.RESOURCES)gtk/dropdown/full.ui"),
			// translators: composer dropdown tooltip text
			tooltip_text = _("Post Privacy"),
			valign = Gtk.Align.CENTER,
			sensitive = !this.edit_mode
		};
		visibility_button.add_css_class ("dropdown-circular");
		visibility_button.notify["selected"].connect (visibility_changed);

		var safe_visibility = accounts.active.visibility.has_key (default_visibility) ? default_visibility : "public";
		uint default_visibility_index;
		if (
			accounts.active.visibility_list.find (
				accounts.active.visibility[safe_visibility],
				out default_visibility_index
			)
		) {
			visibility_button.selected = default_visibility_index;
		}

		append_dropdown (visibility_button);
	}

	private void visibility_changed () {
		string vis = ((InstanceAccount.Visibility) visibility_button.selected_item).id;
		post_btn.update_label (
			vis == "direct"
			// translators: post compositor post button label when the visibility is 'mentioned only'
			? _("Send")
			: null
		);

		if (quote_policy_button != null) quote_policy_button.sensitive = vis == "public" || vis == "unlisted";
	}

	private void install_languages (string? locale_iso) {
		language_button = new Gtk.DropDown (app.app_locales.list_store, null) {
			expression = new Gtk.PropertyExpression (typeof (Utils.Locales.Locale), null, "en-name"),
			factory = new Gtk.BuilderListItemFactory.from_resource (null, @"$(Build.RESOURCES)gtk/dropdown/language_title.ui"),
			list_factory = new Gtk.BuilderListItemFactory.from_resource (null, @"$(Build.RESOURCES)gtk/dropdown/language.ui"),
			// translators: composer dropdown tooltip text
			tooltip_text = _("Post Language"),
			enable_search = true,
			valign = Gtk.Align.CENTER
		};
		language_button.add_css_class ("dropdown-circular");

		if (locale_iso != null) {
			uint default_lang_index;
			if (
				app.app_locales.list_store.find_with_equal_func (
					new Utils.Locales.Locale (locale_iso, null, null),
					Utils.Locales.Locale.compare,
					out default_lang_index
				)
			) {
				language_button.selected = default_lang_index;
			}
		}

		language_button.notify["selected"].connect (on_language_changed);
		append_dropdown (language_button);
	}

	private void on_language_changed () {
		if (language_button.selected == Gtk.INVALID_LIST_POSITION) return;

		var locale_obj = language_button.selected_item as Utils.Locales.Locale;
		if (locale_obj == null || locale_obj.locale == null) return;

		editor.locale = locale_obj.locale;
		cw_changed_with_locale (locale_obj.locale);
	}

	private void cw_changed () {
		string locale_icu = "en";
		if (
			language_button != null
			&& ((Utils.Locales.Locale) language_button.selected_item) != null
			&& ((Utils.Locales.Locale) language_button.selected_item).locale != null
		) {
			locale_icu = ((Utils.Locales.Locale) language_button.selected_item).locale;
		}

		cw_changed_with_locale (locale_icu);
	}

	private void cw_changed_with_locale (string locale) {
		int cw_count = 0;
		if (cw_button.active) cw_count = Utils.Counting.chars (cw_entry.text, locale);
		if (cw_count != this.cw_count) {
			this.cw_count = cw_count;
			update_remaining_chars ();
		}
	}

	public delegate void SuccessCallback (API.Status? cb_status);
	protected SuccessCallback? cb;

	static construct {
		typeof (Composer.Components.DropOverlay).ensure ();
		typeof (PostButton).ensure ();
	}

	construct {
		var condition = new Adw.BreakpointCondition.length (
			Adw.BreakpointConditionLengthType.MAX_WIDTH,
			430, Adw.LengthUnit.SP
		);
		var breakpoint = new Adw.Breakpoint ((owned) condition);
		breakpoint.add_setter (this, "is-narrow", true);
		add_breakpoint (breakpoint);

		var schedule_action = new SimpleAction ("schedule", null);
		schedule_action.activate.connect (on_schedule_action_activated);

		var draft_action = new SimpleAction ("draft", null);
		draft_action.activate.connect (on_draft_action_activated);

		var drafted_posts_action = new SimpleAction ("drafted-posts", null);
		drafted_posts_action.activate.connect (on_drafted_posts_action_activated);

		var action_group = new GLib.SimpleActionGroup ();
		action_group.add_action (schedule_action);
		action_group.add_action (draft_action);
		action_group.add_action (drafted_posts_action);

		this.insert_action_group ("composer", action_group);

		var char_limit_api = accounts.active.instance_info.compat_status_max_characters;
		if (char_limit_api > 0)
			char_limit = char_limit_api;

		var dnd_controller = new Gtk.DropTarget (typeof (Gdk.FileList), Gdk.DragAction.COPY) {
			propagation_phase = CAPTURE
		};
		dnd_controller.enter.connect (on_drag_enter);
		dnd_controller.leave.connect (on_drag_leave);
		dnd_controller.drop.connect (on_drag_drop);
		toolbar_view.add_controller (dnd_controller);
		sensitive_media_button.toggled.connect (update_attachmentsbin_sensitivity);
		this.close_attempt.connect (on_exit);

		if (accounts.active.tuba_api_versions.mastodon < 10 )
			poll_button.bind_property ("sensitive", add_media_button, "sensitive", SYNC_CREATE | INVERT_BOOLEAN);
		cw_revealer.notify["child-revealed"].connect (on_cw_revealed);
	}

	public class QuotationPolicy : Object {
		public API.Status.QuotePolicy policy { get; construct set; }
		public string title { owned get { return this.policy.to_title (); } }
		public string subtitle { owned get { return this.policy.to_subtitle (); } }
		public string icon {
			get {
				switch (this.policy) {
					case NOBODY: return "tuba-quotation-disabled-symbolic";
					case FOLLOWERS: return "tuba-quotation-locked-symbolic";
					default: return "tuba-quotation-symbolic";
				}
			}
		}

		public QuotationPolicy (string policy_name) {
			this.policy = API.Status.QuotePolicy.from_string (policy_name);
		}

		public QuotationPolicy.from_policy (API.Status.QuotePolicy policy) {
			this.policy = policy;
		}

		public static EqualFunc<string> compare = (a, b) => {
			return ((QuotationPolicy) a).policy == ((QuotationPolicy) b).policy;
		};
	}

	private void install_quote_policy_button (string? quote_policy = null, bool sensitive = true) {
		GLib.ListStore supported_quotation_policies = new GLib.ListStore (typeof (QuotationPolicy));
		supported_quotation_policies.append (new QuotationPolicy.from_policy (API.Status.QuotePolicy.PUBLIC));
		supported_quotation_policies.append (new QuotationPolicy.from_policy (API.Status.QuotePolicy.FOLLOWERS));
		supported_quotation_policies.append (new QuotationPolicy.from_policy (API.Status.QuotePolicy.NOBODY));

		quote_policy_button = new Gtk.DropDown (supported_quotation_policies, null) {
			expression = new Gtk.PropertyExpression (typeof (QuotationPolicy), null, "title"),
			factory = new Gtk.BuilderListItemFactory.from_resource (null, @"$(Build.RESOURCES)gtk/dropdown/quotation_policy_title.ui"),
			list_factory = new Gtk.BuilderListItemFactory.from_resource (null, @"$(Build.RESOURCES)gtk/dropdown/quotation_policy.ui"),
			tooltip_text = _("Quote Policy"),
			enable_search = false,
			sensitive = sensitive
		};

		if (quote_policy != null) {
			uint default_quote_policy_index;
			if (
				supported_quotation_policies.find_with_equal_func (
					new QuotationPolicy (quote_policy),
					QuotationPolicy.compare,
					out default_quote_policy_index
				)
			) {
				quote_policy_button.selected = default_quote_policy_index;
			}
		}

		unowned Gtk.Widget? actual_button = quote_policy_button.get_first_child () as Gtk.ToggleButton;
		if (actual_button != null) actual_button.add_css_class ("flat");
		headerbar.pack_end (quote_policy_button);
	}

	private void install_local_only_button (bool active) {
		local_only_button = new Gtk.ToggleButton () {
			icon_name = "tuba-user-home-symbolic",
			// translators: tooltip button on composer dialog headerbar button
			//				that makes the post visible only on the current
			//				instance. You might be able to find it on
			//				https://github.com/glitch-soc/mastodon/tree/5c6133c20bbe734f1380f4c3d75ce84baed82b74/app/javascript/flavours/glitch/locales
			tooltip_text = _("Local Only"),
			active = active,
			sensitive = !this.edit_mode
		};
		local_only_button.toggled.connect (local_only_button_toggled);
		visibility_button.sensitive = !active && !this.edit_mode;
		this.reserved_chars = active ? 2 : 0;

		headerbar.pack_end (local_only_button);
	}

	private void local_only_button_toggled () {
		update_local_only_button_state ();
	}

	private void update_local_only_button_state (bool skip_remaining_char_recalc = false) {
		if (accounts.active.instance_info.pleroma != null && !(InstanceAccount.InstanceFeatures.ICESHRIMP in accounts.active.tuba_instance_features)) {
			visibility_button.sensitive = !local_only_button.active && !this.edit_mode;
		} else if (
			accounts.active.tuba_api_versions.mastodon < 11
			&& InstanceAccount.InstanceFeatures.GLITCH in accounts.active.tuba_instance_features
		) {
			this.reserved_chars = local_only_button.active && !editor.buffer.text.has_suffix (LOCAL_ONLY_GLITCH_SUFFIX) ? 2 : 0;
			if (!skip_remaining_char_recalc) update_remaining_chars ();
		}
	}

	private void install_content_types (string? content_type) {
		content_type_button = new Gtk.DropDown (accounts.active.supported_mime_types, null) {
			expression = new Gtk.PropertyExpression (typeof (Tuba.InstanceAccount.StatusContentType), null, "title"),
			factory = new Gtk.BuilderListItemFactory.from_resource (null, @"$(Build.RESOURCES)gtk/dropdown/content_type_title.ui"),
			list_factory = new Gtk.BuilderListItemFactory.from_resource (null, @"$(Build.RESOURCES)gtk/dropdown/content_type.ui"),
			// translators: composer dropdown tooltip text
			tooltip_text = _("Post Content Type"),
			enable_search = false
		};

		if (content_type != null) {
			uint default_content_type_index;
			if (
				accounts.active.supported_mime_types.find_with_equal_func (
					new Tuba.InstanceAccount.StatusContentType (content_type),
					Tuba.InstanceAccount.StatusContentType.compare,
					out default_content_type_index
				)
			) {
				content_type_button.selected = default_content_type_index;
			}
		}

		unowned Gtk.Widget? actual_button = content_type_button.get_first_child () as Gtk.ToggleButton;
		if (actual_button != null) actual_button.add_css_class ("flat");

		headerbar.pack_start (content_type_button);
		content_type_button.notify["selected-item"].connect (on_content_type_changed);
		on_content_type_changed ();
	}

	private void on_content_type_changed () {
		if (content_type_button.selected == Gtk.INVALID_LIST_POSITION) return;

		var ct_obj = content_type_button.selected_item as InstanceAccount.StatusContentType;
		if (ct_obj == null || ct_obj.syntax == null) return;

		editor.content_type = ct_obj.syntax;
	}

	private void on_vadjustment_value_changed () {
		headerbar.show_title = scroller.vadjustment.value > 0;
	}

	uint unique_state = 0;
	public Dialog (
		Precompose? precompose = null,
		string default_visibility = settings.default_post_visibility,
		string default_language = settings.default_language,
		// translators: composer post button label
		string post_button_label = _("Post"),
		bool edit_mode = false,
		bool can_schedule = true,
		string default_quote_policy = settings.default_quote_policy,
		bool can_update_draft = false
	) {
		Object ();

		this.edit_mode = edit_mode;
		install_editor ();
		install_emoji_pickers ();
		post_btn.install (
			post_button_label,
			!this.edit_mode && (can_schedule || can_update_draft),
			can_update_draft,
			can_schedule,
			precompose == null || (precompose.quote_id == null && precompose.in_reply_to_id == null)
		);
		post_btn.clicked.connect (on_commit);
		post_btn.allow_dynamic_name = post_button_label == _("Post");
		install_visibility (default_visibility);
		install_languages (default_language);
		if (accounts.active.supported_mime_types.n_items > 1)
			install_content_types (settings.default_content_type);

		if (accounts.active.tuba_api_versions.mastodon >= 7) {
			install_quote_policy_button (default_quote_policy, default_visibility == "public" || default_visibility == "unlisted");
		}

		if (InstanceAccount.InstanceFeatures.LOCAL_ONLY in accounts.active.tuba_instance_features) {
			install_local_only_button (default_visibility == "local" || (precompose != null && precompose.local_only));
			if (
				local_only_button.active
				&& InstanceAccount.InstanceFeatures.GLITCH in accounts.active.tuba_instance_features
				&& accounts.active.tuba_api_versions.mastodon < 11
				&& precompose != null && precompose.content != null && precompose.content.has_suffix (LOCAL_ONLY_GLITCH_SUFFIX)
			) {
				precompose.content = precompose.content.substring (0, precompose.content.length - LOCAL_ONLY_GLITCH_SUFFIX.length);
			}
			local_only_button.sensitive = !this.edit_mode;
		}

		cw_entry.changed.connect (cw_changed);
		cw_button.toggled.connect (cw_changed);
		on_language_changed ();

		update_remaining_chars ();

		scroller.vadjustment.value_changed.connect (on_vadjustment_value_changed);
		poll_button.toggled.connect (toggle_poll_component);
		toggle_poll_component ();

		add_media_button.clicked.connect (on_add_media_clicked);
		// translators: composer title
		this.set_editor_title (_("New Post"), null);

		if (precompose != null) {
			if (precompose.content != null) editor.buffer.text = precompose.content;
			if (precompose.spoiler != null) {
				cw_button.active = precompose.spoiler != "";
				cw_entry.text = precompose.spoiler;
			}
			if (precompose.quote_id != null) {
				this.quote_id = precompose.quote_id;
				update_quote_id_state ();
			}
			if (precompose.scheduled_id != null) this.scheduled_id = precompose.scheduled_id;
			if (precompose.in_reply_to_id != null) this.in_reply_to_id = precompose.in_reply_to_id;

			if (precompose.force_cursor_at_start) {
				Gtk.TextIter star_iter;
				editor.buffer.get_start_iter (out star_iter);
				editor.buffer.place_cursor (star_iter);
			}

			if (precompose.poll != null) {
				init_polls_component (precompose.poll);
				poll_button.active = true;
			}

			if (precompose.media_attachments != null && precompose.media_attachments.size > 0) {
				create_attachmentsbin (precompose.media_attachments);
				editor.add_bottom_child (attachmentsbin_component);
				sensitive_media_button.active = precompose.sensitive_media;
			}
		} else if (post_btn.allow_dynamic_name) {
			// when precompose is empty, instead of the disabled post button
			// show a "Drafts" button which will open the available drafts so
			// the user can choose one
			post_btn.show_drafts_default ();
		}

		unique_state = generate_unique_state ();
		present (app.main_window);
	}

	private bool ensure_focus_and_vadjustment () {
		if (editor == null) return false;
		if (this.scroller.vadjustment.value != editor.top_margin) scroller_mapped ();
		if (!editor.has_focus) return editor.grab_focus ();
		return true;
	}

	public override bool grab_focus () {
		bool base_focus = base.grab_focus ();
		bool ensure = ensure_focus_and_vadjustment ();
		return ensure || base_focus;
	}

	public Dialog.reply (API.Status to, owned SuccessCallback? t_cb = null) {
		string final_visibility = to.visibility;
		var default_visibility = API.Status.Visibility.from_string (settings.default_post_visibility);
		var to_visibility = API.Status.Visibility.from_string (to.visibility);
		if (default_visibility != null && to_visibility != null && default_visibility.privacy_rate () > to_visibility.privacy_rate ()) {
			final_visibility = settings.default_post_visibility;
		}

		string filled_content = "";
		{
			string joined_mentions;
			if (to.formal.get_reply_mentions (out joined_mentions)) {
				filled_content = @"$joined_mentions ";
			}
		}

		// translators: composer post button label
		this ({filled_content, to.spoiler_text, null, null, to.id, null, null, false, to.local_only, false}, final_visibility, to.language, _("Reply"));

		var sample = new API.Status.empty () {
			poll = to.poll,
			sensitive = to.sensitive,
			media_attachments = to.media_attachments,
			visibility = to.visibility,
			tuba_spoiler_revealed = true,
			content = to.content,
			spoiler_text = to.spoiler_text,
			account = to.account,
			created_at = to.created_at,
			quote = to.quote
		};

		if (sample.formal.has_media) {
			sample.formal.media_attachments.foreach (e => {
				e.tuba_is_report = true;

				return true;
			});
		}

		Widgets.Status widget_status = (Widgets.Status?) sample.to_widget ();
		widget_status.add_css_class ("card");
		widget_status.add_css_class ("initial-font-size");
		widget_status.to_display_only ();
		// translators: composer embedded replying-to post, aria label
		widget_status.update_property (Gtk.AccessibleProperty.LABEL, _("The post you are replying to."), -1);

		var lbox = new Gtk.ListBox () {
			selection_mode = Gtk.SelectionMode.NONE
		};
		lbox.append (widget_status);

		// translators: composer title. The variable is a string username
		this.set_editor_title (_("Reply to @%s").printf (to.account.username), lbox);
		this.scroller.map.connect (scroller_mapped);
		this.cb = (owned) t_cb;
	}

	public Dialog.quote (API.Status to, API.Status.Visibility? reblog_visibility = null, bool supports_quotes = false) {
		string final_visibility = to.visibility;
		if (reblog_visibility == null) {
			var default_visibility = API.Status.Visibility.from_string (settings.default_post_visibility);
			var to_visibility = API.Status.Visibility.from_string (to.visibility);
			if (default_visibility != null && to_visibility != null && default_visibility.privacy_rate () > to_visibility.privacy_rate ()) {
				final_visibility = settings.default_post_visibility;
			}
		} else {
			final_visibility = reblog_visibility.to_string ();
		}

		this (
			{
				supports_quotes ? null : @"\n\nRE: $(to.formal.url ?? to.formal.account.url)",
				to.spoiler_text,
				supports_quotes ? to.id : null,
				null,
				null,
				null,
				null,
				false,
				to.compat_local_only,
				!supports_quotes
			},
			final_visibility,
			to.language,
			// translators: composer post button label, verb
			_("Quote"),
			false
		);

		var sample = new API.Status.empty () {
			poll = to.poll,
			sensitive = to.sensitive,
			media_attachments = to.media_attachments,
			visibility = to.visibility,
			tuba_spoiler_revealed = true,
			content = to.content,
			spoiler_text = to.spoiler_text,
			account = to.account,
			created_at = to.created_at,
			quote = to.quote
		};

		if (sample.formal.has_media) {
			sample.formal.media_attachments.foreach (e => {
				e.tuba_is_report = true;

				return true;
			});
		}

		Widgets.Status widget_status = (Widgets.Status?) sample.to_widget ();
		widget_status.add_css_class ("card");
		widget_status.add_css_class ("initial-font-size");
		widget_status.to_display_only ();
		// translators: composer embedded quoting post, aria label
		widget_status.update_property (Gtk.AccessibleProperty.LABEL, _("The post you are quoting."), -1);

		var lbox = new Gtk.ListBox () {
			selection_mode = Gtk.SelectionMode.NONE
		};
		lbox.append (widget_status);

		// translators: composer title. The variable is a string username
		this.set_editor_title (_("Quoting @%s").printf (to.account.username), lbox);
		this.scroller.map.connect (scroller_mapped);
	}

	public Dialog.edit (API.Status t_status, API.StatusSource? source = null, owned SuccessCallback? t_cb = null, bool redraft = false) {
		this (
			{
				source == null ? Utils.Htmlx.remove_tags (t_status.content) : source.text,
				source == null ? t_status.spoiler_text : source.spoiler_text,
				null, null, redraft ? t_status.in_reply_to_id : null,
				t_status.poll,
				t_status.media_attachments,
				t_status.sensitive,
				t_status.compat_local_only,
				false
			},
			t_status.visibility,
			t_status.language,
			// translators: composer post button label
			redraft ? _("Redraft") : _("Edit"),
			!redraft,
			redraft,
			t_status.quote_approval == null ? settings.default_quote_policy : t_status.quote_approval.to_quote_policy ().to_string (),
			redraft
		);

		if (!redraft) {
			this.edit_status_id = t_status.id;
			// translators: composer title
			this.set_editor_title (_("Edit Post"), null);
		}

		this.cb = (owned) t_cb;
	}

	public Dialog.from_scheduled (API.ScheduledStatus scheduled_status, bool posting_draft, API.Poll? poll = null, owned SuccessCallback? t_cb = null) {
		this (
			{
				scheduled_status.props.text,
				scheduled_status.props.spoiler_text,
				scheduled_status.props.quoted_status_id != null ? scheduled_status.props.quoted_status_id : null,
				posting_draft ? null : scheduled_status.id,
				scheduled_status.props.in_reply_to_id > 0 ? scheduled_status.props.in_reply_to_id.to_string () : null,
				poll,
				scheduled_status.media_attachments,
				scheduled_status.props.sensitive,
				scheduled_status.props.local_only || scheduled_status.props.visibility == "local",
				false
			},
			scheduled_status.props.visibility,
			scheduled_status.props.language,
			// translators: composer post button label
			posting_draft ? _("Post") : _("Edit"),
			false,
			posting_draft,
			scheduled_status.props.quote_approval_policy,
			posting_draft
		);

		if (!posting_draft) {
			// translators: composer title
			this.set_editor_title (_("Edit Post"), null);
			this.schedule_iso8601 = scheduled_status.scheduled_at;
		}

		load_replied_to_or_quote_status_async.begin (!posting_draft);

		this.cb = (owned) t_cb;
	}

	private async void load_replied_to_or_quote_status_async (bool editing) {
		if (this.quote_id == null && this.in_reply_to_id == null) return;

		bool is_reply = this.in_reply_to_id != null;
		string status_id = is_reply ? this.in_reply_to_id : this.quote_id;

		var req = new RequestV2 (@"/api/v1/statuses/$status_id") {
			account = accounts.active,
			ctx = this
		};

		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 api_status = API.Status.from (node);

			api_status.tuba_spoiler_revealed = true;
			if (api_status.formal.has_media) {
				api_status.formal.media_attachments.foreach (e => {
					e.tuba_is_report = true;

					return true;
				});
			}

			Widgets.Status widget_status = (Widgets.Status?) api_status.to_widget ();
			widget_status.add_css_class ("card");
			widget_status.add_css_class ("initial-font-size");
			widget_status.to_display_only ();
			if (is_reply) {
				widget_status.update_property (Gtk.AccessibleProperty.LABEL, _("The post you are replying to."), -1);
			} else {
				widget_status.update_property (Gtk.AccessibleProperty.LABEL, _("The post you are quoting."), -1);
			}

			var lbox = new Gtk.ListBox () {
				selection_mode = Gtk.SelectionMode.NONE
			};
			lbox.append (widget_status);

			string composer_title = (is_reply ? _("Reply to @%s") : _("Quoting @%s")).printf (api_status.account.username);
			if (editing) composer_title = this.title;
			this.set_editor_title (composer_title, lbox);

			this.scroller.map.disconnect (scroller_mapped);
			this.scroller.map.connect (scroller_mapped);
		} catch (GLib.IOError.CANCELLED e) {
			debug ("Message is cancelled.");
		} catch (Error e) {
			warning (@"Couldn't get $(is_reply ? "reply" : "quote") $status_id: $(e.message)");
		}

		if (is_reply && this.quote_id != null) {
			req = new RequestV2 (@"/api/v1/statuses/$(this.quote_id)") {
				account = accounts.active,
				ctx = this
			};

			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 api_status = API.Status.from (node);
				add_quote_inline_widget (api_status);
			} catch (GLib.IOError.CANCELLED e) {
				debug ("Message is cancelled.");
			} catch (Error e) {
				warning (@"Couldn't get quote $(this.quote_id): $(e.message)");
			}
		}
	}

	private inline void set_editor_title (string new_title, Gtk.Widget? widget_status) {
		this.title = new_title;
		nav_page.title = new_title;
		editor.set_title (new_title, widget_status);
	}

	private void scroller_mapped () {
		this.scroller.vadjustment.value = editor.top_margin;
	}

	Composer.Components.Polls? polls_component = null;
	Adw.TimedAnimation? polls_animation = null;
	private void toggle_poll_component () {
		if (!poll_button.active) {
			if (polls_animation != null) {
				polls_animation.reverse = true;
				polls_animation.play ();
			} else if (polls_component != null && editor.has_bottom_child (polls_component)) {
				editor.remove_bottom_child (polls_component);
			}
			return;
		}

		if (polls_component == null) {
			init_polls_component ();
		} else if (polls_animation.state == PLAYING) polls_animation.skip ();

		editor.add_bottom_child (polls_component);
		polls_animation.reverse = false;
		polls_animation.play ();
	}

	private void init_polls_component (API.Poll? poll_obj = null) {
		if (polls_component != null) return;

		polls_component = new Composer.Components.Polls (poll_obj) {
			opacity = 0
		};
		polls_component.notify["is-valid"].connect (validate_post_button);
		polls_animation = new Adw.TimedAnimation (polls_component, 0, 1, 250, new Adw.PropertyAnimationTarget (polls_component, "opacity"));
		polls_animation.done.connect (on_poll_component_animation_end);
		this.bind_property ("is-narrow", polls_component, "is-narrow", SYNC_CREATE);
	}

	Composer.Components.AttachmentsBin? attachmentsbin_component = null;
	private void on_add_media_clicked () {
		create_attachmentsbin ();
		attachmentsbin_component.show_file_selector ();
		editor.add_bottom_child (attachmentsbin_component);
	}

	private void update_attachmentsbin_meta () {
		if (attachmentsbin_component == null) return;

		bool is_used = attachmentsbin_component.working || !attachmentsbin_component.is_empty;
		sensitive_media_button.visible = !attachmentsbin_component.is_empty;
		poll_button.sensitive = (accounts.active.tuba_api_versions.mastodon >= 10 || !is_used) && !this.quote_limited;
		if (!is_used) editor.remove_bottom_child (attachmentsbin_component);
		validate_post_button ();
		update_attachmentsbin_sensitivity ();
	}

	private bool on_drag_drop (Value val, double x, double y) {
		drop_overlay.dropping = false;
		if (!add_media_button.sensitive) return false;

		var file_list = val as Gdk.FileList;
		if (file_list == null) return false;

		var files = file_list.get_files ();
		if (files.length () == 0) return false;

		File[] files_to_upload = {};
		foreach (var file in files) {
			files_to_upload += file;
		}
		if (files_to_upload.length == 0) return false;

		create_attachmentsbin ();
		attachmentsbin_component.upload_files.begin (files_to_upload);
		editor.add_bottom_child (attachmentsbin_component);

		return true;
	}

	private Gdk.DragAction on_drag_enter (double x, double y) {
		drop_overlay.dropping = true;
		return Gdk.DragAction.COPY;
	}

	private void on_drag_leave () {
		drop_overlay.dropping = false;
	}

	private async void on_clipboard_paste_async (Gdk.Clipboard clipboard) {
		File[] files = {};

		try {
			var copied_value = yield clipboard.read_value_async (typeof (File), 0, null);

			if (copied_value != null) {
				var copied_file = copied_value as File;
				if (copied_file != null) {
					files += copied_file;
				}
			}
		} catch (Error e) {}

		if (files.length == 0) {
			try {
				var copied_texture = yield clipboard.read_texture_async (null);
				if (copied_texture == null) return;

				FileIOStream stream;
				files += yield File.new_tmp_async ("tuba-XXXXXX.png", GLib.Priority.DEFAULT, null, out stream);

				OutputStream ostream = stream.output_stream;
				yield ostream.write_bytes_async (copied_texture.save_to_png_bytes ());
			} catch (Error e) {
				warning (@"Couldn't get texture from clipboard: $(e.message)");
			}
		}

		yield attachmentsbin_component.upload_files (files);
	}

	private void create_attachmentsbin (Gee.ArrayList<API.Attachment>? attachments_obj = null) {
		if (attachmentsbin_component != null) return;
		attachmentsbin_component = new Composer.Components.AttachmentsBin ();
		attachmentsbin_component.notify["working"].connect (update_attachmentsbin_meta);
		attachmentsbin_component.notify["is-empty"].connect (update_attachmentsbin_meta);
		attachmentsbin_component.bind_property ("can-add-media", add_media_button, "sensitive", SYNC_CREATE);

		if (attachments_obj != null && attachments_obj.size > 0) {
			foreach (var attachment_obj in attachments_obj) {
				attachmentsbin_component.preload_attachment (attachment_obj, this.edit_mode);
			}
		}
	}

	GLib.Cancellable? quote_paste_cancellable = null;
	private void handle_potential_quote_paste (Gdk.Clipboard clipboard) {
		if (
			this.edit_mode
			|| this.quote_id != null
			|| ((InstanceAccount.Visibility) visibility_button.selected_item).id == "direct"
			|| accounts.active.tuba_api_versions.mastodon < 7
			|| (polls_component != null && editor.has_bottom_child (polls_component))
			|| (attachmentsbin_component != null && editor.has_bottom_child (attachmentsbin_component) && !attachmentsbin_component.is_empty)
		) return;

		if (quote_paste_cancellable != null) quote_paste_cancellable.cancel ();
		quote_paste_cancellable = new GLib.Cancellable ();

		string original_text = editor.buffer.text;
		handle_potential_quote_paste_real.begin (original_text, clipboard);
	}

	private async void handle_potential_quote_paste_real (string original_text, Gdk.Clipboard clipboard) {
		try {
			string? clipboard_text = yield clipboard.read_text_async (quote_paste_cancellable);
			if (clipboard_text == null) return;
			clipboard_text = clipboard_text.strip ();
			if (clipboard_text == "") return;

			GLib.Uri uri;
			try {
				uri = GLib.Uri.parse (clipboard_text, GLib.UriFlags.NONE);
			} catch (Error e) {
				return;
			}

			if (uri.get_scheme () != "http" && uri.get_scheme () != "https") return;
			string path = uri.get_path ();
			if (
				path == ""
				|| !(
					path.has_prefix ("/@")
					|| path.has_prefix ("/notes/")
					|| path.has_prefix ("/notice/")
					|| path.has_prefix ("/objects/")
				)
			) return;

			var req = new RequestV2 ("/api/v2/search") { account = accounts.active, cancellable = quote_paste_cancellable };
			req.add_parameter ("resolve", "true");
			req.add_parameter ("limit", "1");
			req.add_parameter ("type", "statuses");
			req.add_parameter ("q", uri.to_string ());

			var in_stream = yield req.exec (null);
			Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);
			var results = API.SearchResults.from (network.parse_node (parser));
			if (results.statuses.size == 0) return;
			var status = results.statuses[0];
			if (!status.can_be_quoted) return;
			Widgets.Status? status_widget = status.to_widget () as Widgets.Status;
			if (status_widget == null) return;
			status_widget.to_display_only ();
			status_widget.add_css_class ("card");
			status_widget.add_css_class ("initial-font-size");
			status_widget.overflow = HIDDEN;

			var qs = yield app.question (
				// translators: dialog title shown when the user pastes a post url in the composer;
				//				the variable is a uri string of a quote
				{_("Quote %s?").printf (clipboard_text), false},
				null,
				this,
				{ { _("Quote"), Adw.ResponseAppearance.SUGGESTED }, { _("Cancel"), Adw.ResponseAppearance.DEFAULT } },
				status_widget,
				false
			);
			if (qs.truthy ()) {
				editor.buffer.text = original_text;
				this.quote_id = status.id;
				update_quote_id_state ();
				add_quote_inline_widget (status);
			}
		} catch (GLib.IOError.CANCELLED e) {
			debug ("Message is cancelled.");
		} catch (Error e) {
			debug (@"Couldn't fetch quote from clipboard: $(e.message)");
		}
	}

	private void update_quote_id_state () {
		bool has_quote = this.quote_id != null;
		if (accounts.active.tuba_api_versions.mastodon >= 7) {
			this.quote_limited = has_quote;
			poll_button.sensitive = !has_quote;
			add_media_button.sensitive = !has_quote;
		}
	}

	Gtk.Widget? inline_quote_widget = null;
	Adw.TimedAnimation? inline_quote_widget_animation = null;
	private void add_quote_inline_widget (API.Status quote_status) {
		if (inline_quote_widget != null) return;
		Widgets.Status status_widget = (Widgets.Status) quote_status.to_widget ();
		status_widget.to_display_only ();
		status_widget.add_css_class ("card");
		status_widget.add_css_class ("initial-font-size");
		status_widget.margin_bottom = 8;
		status_widget.overflow = HIDDEN;
		status_widget.opacity = 0;

		var remove_quote_button = new Gtk.Button.from_icon_name ("user-trash-symbolic") {
			css_classes = { "circular", "error" },
			// translators: in the composer, inside a quote post, top right button
			//				with a trash can icon that when clicked removes the
			//				quote; tooltip text
			tooltip_text = _("Remove Quote"),
			valign = Gtk.Align.CENTER
		};
		remove_quote_button.clicked.connect (remove_quote_inline_widget);
		status_widget.indicators.append (remove_quote_button);

		inline_quote_widget = status_widget;
		editor.add_bottom_child (inline_quote_widget, true);

		// the combination of the confirmation dialog getting hidden and creating
		// bottom widget, makes it feel shorter than 250ms, so double it here
		// and reset when reversing
		inline_quote_widget_animation = new Adw.TimedAnimation (inline_quote_widget, 0, 1, 500, new Adw.PropertyAnimationTarget (inline_quote_widget, "opacity"));
		inline_quote_widget_animation.done.connect (on_inline_quote_widget_animation_done);
		inline_quote_widget_animation.play ();
	}

	private void on_inline_quote_widget_animation_done (Adw.Animation animation) {
		if (animation.value == 0) remove_quote_inline_widget_real ();
	}

	private void remove_quote_inline_widget () {
		if (inline_quote_widget_animation == null) {
			remove_quote_inline_widget_real ();
			return;
		}
		inline_quote_widget_animation.duration = 250;
		inline_quote_widget_animation.reverse = true;
		inline_quote_widget_animation.play ();
	}

	private void remove_quote_inline_widget_real () {
		if (inline_quote_widget == null) return;

		editor.remove_bottom_child (inline_quote_widget);
		this.quote_id = null;
		update_quote_id_state ();
		inline_quote_widget = null;
		inline_quote_widget_animation = null;
	}

	private void on_paste () {
		Gdk.Clipboard clipboard = Gdk.Display.get_default ().get_clipboard ();
		var formats = clipboard.get_formats ();
		bool has_files = formats.contain_mime_type ("text/uri-list");
		if (!has_files) {
			var mime_types = formats.get_mime_types ();
			if (mime_types == null) {
				handle_potential_quote_paste (clipboard);
				return;
			}

			foreach (string mime_type in mime_types) {
				if (mime_type.has_prefix ("image/")) {
					has_files = true;
					break;
				}
			}
		}
		if (!has_files) {
			handle_potential_quote_paste (clipboard);
			return;
		}

		Signal.stop_emission_by_name (editor, "paste-clipboard");

		if (!add_media_button.sensitive) return;
		app.question.begin (
			// translators: media as in picture or files
			{_("Paste Media from Clipboard?"), false},

			// translators: they = media / files from clipboard, instance = server
			{_("They will be uploaded to your instance"), false},
			this,
			// translators: dialog button label when pasting from clipboard, verb
			{ { _("Paste"), Adw.ResponseAppearance.SUGGESTED }, { _("Cancel"), Adw.ResponseAppearance.DEFAULT } },
			null,
			false,
			(obj, res) => {
				if (app.question.end (res).truthy ()) {
					create_attachmentsbin ();
					on_clipboard_paste_async.begin (clipboard);
					editor.add_bottom_child (attachmentsbin_component);
				}
			}
		);
    }

	private void on_schedule_action_activated () {
		if (!post_btn.sensitive) return;

		var schedule_dlg = new Dialogs.Schedule (this.schedule_iso8601);
		schedule_dlg.schedule_picked.connect (on_schedule_picked);
		on_push_subpage (schedule_dlg);
	}

	private void on_draft_action_activated () {
		if (!post_btn.sensitive) return;

		this.schedule_iso8601 = (new GLib.DateTime.now ()).add_years (3000).format_iso8601 ();
		on_commit ();
	}

	private void on_drafted_posts_action_activated () {
		if (!post_btn.sensitive) return;

		var drafts_dlg = new Dialogs.Drafts ();
		drafts_dlg.draft_selected.connect (on_draft_selected);
		on_push_subpage (drafts_dlg);
	}

	private void on_draft_selected (Widgets.ScheduledStatus draft_status_widget) {
		app.question.begin (
			// translators: Dialog title when replacing the current
			//				composer data with those of a draft
			{_("Replace Current Post?"), false},

			// translators: Dialog body when replacing the current
			//				composer data with those of a draft.
			//				This means that the post the user was
			//				typing in the composer will be replaced.
			{_("Your current post will be lost."), false},
			this,
			// translators: dialog button action, verb; replacing current
			//				composer data with those of a draft
			{ { _("Replace"), Adw.ResponseAppearance.DESTRUCTIVE }, { _("Cancel"), Adw.ResponseAppearance.DEFAULT } },
			null,
			// skip this question if
			// the default state is the empty one
			// and the state hasn't changed
			// aka the composer started as and is empty
			this.unique_state == DEFAULT_EMPTY_STATE && !state_changed (),
			(obj, res) => {
				if (app.question.end (res).truthy ()) {
					this.force_close ();
					draft_status_widget.compose_draft ();
				}
			}
		);
	}

	private void on_schedule_picked (string iso8601) {
		this.schedule_iso8601 = iso8601;
		on_commit ();
	}

	private void on_poll_component_animation_end (Adw.Animation animation) {
		if (animation.value == 0) editor.remove_bottom_child (polls_component);
		else if (polls_component != null && editor.has_bottom_child (polls_component)) polls_component.grab_first_row_focus ();
		validate_post_button ();
	}

	private void on_toast (Adw.Toast toast) {
		toast_overlay.add_toast (toast);
	}

	private void on_push_subpage (Adw.NavigationPage page) {
		{
			var sizeable = page as PreferredSizeable;
			if (sizeable != null) {
				sizeable.preferred_height = toolbar_view.get_height ();
				sizeable.preferred_width = toolbar_view.get_width ();
			}
		}
		nav_view.push (page);
	}

	private void on_pop_subpage () {
		nav_view.pop ();
	}

	private void validate_post_button () {
		bool sensitive = remaining_chars >= 0;
		if (sensitive) {
			if (attachmentsbin_component != null && editor.has_bottom_child (attachmentsbin_component)) {
				sensitive = !attachmentsbin_component.is_empty && !attachmentsbin_component.working;
			}

			if (polls_component != null && editor.has_bottom_child (polls_component)) {
				sensitive = sensitive && polls_component.is_valid && remaining_chars < char_limit;
			} else {
				sensitive = sensitive && remaining_chars < char_limit;
			}
		}

		post_btn.set_sensitive_real (sensitive);
	}

	private void update_attachmentsbin_sensitivity () {
		if (attachmentsbin_component == null) return;

		bool has_class = attachmentsbin_component.has_css_class ("spoilered-attachmentsbin");
		if (sensitive_media_button.active && !has_class) {
			attachmentsbin_component.add_css_class ("spoilered-attachmentsbin");
		} else if (!sensitive_media_button.active && has_class) {
			attachmentsbin_component.remove_css_class ("spoilered-attachmentsbin");
		}
	}

	private Json.Builder populate_json_body () {
		bool is_local = local_only_button != null && local_only_button.active;
		bool is_pleroma = accounts.active.instance_info.pleroma != null;
		bool is_glitch = InstanceAccount.InstanceFeatures.GLITCH in accounts.active.tuba_instance_features;
		bool is_shrimp = InstanceAccount.InstanceFeatures.ICESHRIMP in accounts.active.tuba_instance_features;
		bool supports_eye = accounts.active.tuba_api_versions.mastodon < 11;

		var builder = new Json.Builder ();
		builder.begin_object ();

		builder.set_member_name ("status");
		builder.add_string_value (
			is_local && supports_eye && is_glitch && !editor.buffer.text.has_suffix (LOCAL_ONLY_GLITCH_SUFFIX)
			? @"$(editor.buffer.text)$LOCAL_ONLY_GLITCH_SUFFIX"
			: editor.buffer.text
		);

		if (is_local && (!is_glitch || !supports_eye) && (!is_pleroma || is_shrimp)) {
			builder.set_member_name ("local_only");
			builder.add_boolean_value (true);
		}

		string? vis = null;
		if (is_local && is_pleroma && !is_shrimp) {
			builder.set_member_name ("visibility");
			builder.add_string_value ("local");
		} else if (visibility_button.selected != Gtk.INVALID_LIST_POSITION) {
			builder.set_member_name ("visibility");
			vis = ((InstanceAccount.Visibility) visibility_button.selected_item).id;
			builder.add_string_value (vis);
		}

		// Move to editor?
		builder.set_member_name ("language");
		builder.add_string_value (editor.locale);

		if (content_type_button != null && content_type_button.selected != Gtk.INVALID_LIST_POSITION) {
			builder.set_member_name ("content_type");
			builder.add_string_value (((InstanceAccount.StatusContentType) content_type_button.selected_item).mime);
		}

		if (
			quote_policy_button != null && quote_policy_button.selected != Gtk.INVALID_LIST_POSITION
			&& (vis == "public" || vis == "unlisted")
		) {
			builder.set_member_name ("quote_approval_policy");
			builder.add_string_value (((QuotationPolicy) quote_policy_button.selected_item).policy.to_string ());
		}

		if (in_reply_to_id != null && !edit_mode) {
			builder.set_member_name ("in_reply_to_id");
			builder.add_string_value (this.in_reply_to_id);
		}

		builder.set_member_name ("sensitive");
		builder.add_boolean_value (cw_button.active || (sensitive_media_button.visible && sensitive_media_button.active));
		builder.set_member_name ("spoiler_text");
		builder.add_string_value (cw_button.active ? cw_entry.text : "");

		if (polls_component != null && editor.has_bottom_child (polls_component) && polls_component.is_valid && polls_component.has_rows) {
			builder.set_member_name ("poll");
			builder.begin_object ();
				builder.set_member_name ("multiple");
				builder.add_boolean_value (polls_component.multiple_choice);

				builder.set_member_name ("hide_totals");
				builder.add_boolean_value (polls_component.hide_totals);

				builder.set_member_name ("expires_in");
				builder.add_int_value (polls_component.expires_in);

				builder.set_member_name ("options");
				builder.begin_array ();
					foreach (var option in polls_component.get_all_options ()) {
						builder.add_string_value (option);
					}
				builder.end_array ();
			builder.end_object ();
		}

		if (attachmentsbin_component != null && editor.has_bottom_child (attachmentsbin_component) && !attachmentsbin_component.is_empty) {
			builder.set_member_name ("media_ids");
			builder.begin_array ();
				foreach (var m_id in attachmentsbin_component.get_all_media_ids ()) {
					builder.add_string_value (m_id);
				}
			builder.end_array ();

			if (edit_mode) {
				builder.set_member_name ("media_attributes");
				builder.begin_array ();
					foreach (var meta in attachmentsbin_component.get_all_metadata ()) {
						builder.begin_object ();
							builder.set_member_name ("id");
							builder.add_string_value (meta.id);
							builder.set_member_name ("description");
							builder.add_string_value (meta.description);
							builder.set_member_name ("focus");
							builder.add_string_value (meta.focus);
						builder.end_object ();
					}
				builder.end_array ();
			}
		}

		if (this.quote_id != null) {
			builder.set_member_name (accounts.active.tuba_api_versions.mastodon >= 7 ? "quoted_status_id" : "quote_id");
			builder.add_string_value (quote_id);
		}

		if (this.schedule_iso8601 != null) {
			builder.set_member_name ("scheduled_at");
			builder.add_string_value (schedule_iso8601);
		}

		builder.end_object ();
		return builder;
	}

	private void on_commit () {
		if (!this.sensitive) return;
		this.sensitive = false;

		transaction.begin ((obj, res) => {
			try {
				transaction.end (res);
			} catch (Error e) {
				warning (e.message);
				on_toast (new Adw.Toast (e.message) { timeout = 0 });
			} finally {
				this.sensitive = true;
			}
		});
	}

	private async void transaction () throws Error {
		var publish_req = new RequestV2 ("/api/v1/statuses", POST) {
			account = accounts.active
		};

		if (this.edit_status_id != null && this.edit_status_id != "") {
			publish_req = new RequestV2 (@"/api/v1/statuses/$(this.edit_status_id)", PUT) {
				account = accounts.active
			};
		}

		publish_req.set_body_from_json (populate_json_body ());
		var in_stream = yield publish_req.exec (null);

		Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);
		var node = network.parse_node (parser);
		API.Status? status = null;
		if (schedule_iso8601 != null) {
			var scheduled_status = API.ScheduledStatus.from (node);
			debug (@"Scheduled post with id $(scheduled_status.id) at $(scheduled_status.scheduled_at)");
		} else {
			status = API.Status.from (node);
			debug (@"Published post with id $(status.id)");
		}

		if (this.scheduled_id != null) {
			delete_scheduled_post.begin ();
		} else if (cb != null) {
			cb (status);
		} else if (schedule_iso8601 != null) {
			app.refresh_scheduled_statuses ();
		}

		this.force_close ();
	}

	private async void delete_scheduled_post () {
		var req = new RequestV2 (@"/api/v1/scheduled_statuses/$scheduled_id", DELETE) { account = accounts.active };
		try {
			yield req.exec (null);
			if (cb != null) cb (null);
		} catch (Error e) {
			warning (@"Couldn't delete scheduled post: $(e.code) $(e.message)");
			// translators: error taost; the variable is a string error message
			app.toast (_("Couldn't delete scheduled post: %s").printf (e.message));
		}
	}

	// This is used to check if something changed so we
	// can ask the user if they want to quit. In the old
	// composer, it used to check everything. This time,
	// let's just check the important ones only, since
	// asking when just changing trivial properties seems
	// annoying.
	private const uint DEFAULT_EMPTY_STATE = (uint) 2395400144;
	private uint generate_unique_state () {
		GLib.StringBuilder builder = new GLib.StringBuilder (editor.buffer.text);
		builder.append (cw_button.active.to_string ());
		builder.append (cw_entry.text);

		if (attachmentsbin_component != null && editor.has_bottom_child (attachmentsbin_component) && !attachmentsbin_component.is_empty) {
			builder.append (string.joinv ("", attachmentsbin_component.get_all_media_ids ()));

			foreach (var meta in attachmentsbin_component.get_all_metadata ()) {
				builder.append (meta.id);
				builder.append (meta.description);
				builder.append (meta.focus);
			}
		} else {
			builder.append ("none");
		}

		if (polls_component != null && editor.has_bottom_child (polls_component) && polls_component.is_valid && polls_component.has_rows) {
			builder.append (string.joinv ("", polls_component.get_all_options ()));
			builder.append (polls_component.multiple_choice.to_string ());
			builder.append (polls_component.hide_totals.to_string ());
			builder.append (polls_component.expires_in.to_string ());
		} else {
			builder.append ("none");
		}

		return GLib.str_hash (builder.str);
	}

	private bool state_changed () {
		if (unique_state == 0) return false;
		return unique_state != generate_unique_state ();
	}

	private void on_exit () {
		if (state_changed ()) {
			app.question.begin (
				// translators: Dialog title when closing the composer
				{_("Discard Post?"), false},

				// translators: Dialog body when closing the composer.
				//				'progress' of using the composer (e.g.
				//				it shows up if the user typed something
				//				and pressed esc / the composer is not
				//				empty)
				{_("Your progress will be lost."), false},
				this,
				// translators: dialog button action, verb
				{ { _("Discard"), Adw.ResponseAppearance.DESTRUCTIVE }, { _("Cancel"), Adw.ResponseAppearance.DEFAULT } },
				null,
				false,
				(obj, res) => {
					if (app.question.end (res).truthy ()) this.force_close ();
				}
			);
		} else {
			this.force_close ();
		}
	}

	private void on_cw_revealed () {
		if (!cw_revealer.reveal_child) return;
		cw_entry.grab_focus ();
	}
}
