namespace Tuba {
	public errordomain Oopsie {
		USER,
		PARSING,
		INSTANCE,
		INTERNAL,

		HTTP_401 = 401,
		HTTP_403 = 403,
		HTTP_404 = 404,
		HTTP_410 = 410,
		HTTP_422 = 422,
		HTTP_429 = 429,
		HTTP_500 = 500,
		HTTP_503 = 503
	}

	public static Application app;

	public static Settings settings;
	public static AccountStore accounts;
	public static Network network;
	public static Streams streams;
	public static NetworkMonitor network_monitor;

	//  public static EntityCache entity_cache;
	//  public static ImageCache image_cache;
	//  public static BlurhashCache blurhash_cache;

	public static GLib.Regex bookwyrm_regex;
	public static GLib.Regex custom_emoji_regex;
	public static GLib.Regex rtl_regex;
	public static bool is_rtl;

	public static bool start_hidden = false;
	#if UNIFIEDPUSH
		public static bool up_bg = false;
		public static bool up_distributor_found = true;
	#endif
	public static bool is_flatpak = false;
	public static string cache_path;

	public static string default_locale;

	#if ANDROID
		[CCode (cname = "g_io_openssl_load")]
		extern void g_io_openssl_load (GLib.IOModule? module);
	#endif

	public class Application : Adw.Application {

		public GLib.ProxyResolver? proxy { get; set; default=null; }
		public Dialogs.MainWindow? main_window { get; set; }
		public Dialogs.NewAccount? add_account_window { get; set; }
		public bool is_mobile { get; set; default=false; }
		public bool is_online { get; private set; default=true; }

		public Utils.Locales app_locales { get; construct set; }
		#if UNIFIEDPUSH
			public UnifiedPush.UnifiedPush unifiedpush { get; construct set; }
		#endif

		// These are used for the GTK Inspector
		public Settings app_settings { get {return Tuba.settings; } }
		public AccountStore app_accounts { get {return Tuba.accounts; } }
		public Network app_network { get {return Tuba.network; } }
		public Streams app_streams { get {return Tuba.streams; } }

		public signal void refresh ();
		public signal void refresh_scheduled_statuses ();
		public signal void refresh_featured ();
		public signal void remove_status_widget (string id);
		public signal void relationship_invalidated (API.Relationship new_relationship);
		public signal void remove_user_id (string user_id);
		public signal void fav_tags_changed (string tag);
		public signal void time_update ();
		public signal void toast (string title, uint timeout = 5, string? action_name = null, GLib.Variant? action_target = null, string? action_label = null);
		public signal void toast_object (Adw.Toast toast_obj);

		#if DEV_MODE
			public signal void dev_new_post (Json.Node node);
			public signal void dev_new_notification (Json.Node node);
		#endif

		//  public CssProvider css_provider = new CssProvider ();
		//  public CssProvider zoom_css_provider = new CssProvider (); //FIXME: Zoom not working

		public const GLib.OptionEntry[] APP_OPTIONS = {
			{ "hidden", 0, 0, OptionArg.NONE, ref start_hidden, N_("Do not show main window on start"), null
			#if UNIFIEDPUSH
				}, { "unifiedpush-bg", 0, GLib.OptionFlags.HIDDEN, OptionArg.NONE, ref up_bg, "UnifiedPush background service", null
			#endif
			},
			{ null }
		};

		private const GLib.ActionEntry[] APP_ENTRIES = {
			#if DEV_MODE
			 	// vala-lint=block-opening-brace-space-before
				{ "dev-only-window", dev_only_window_activated },
			#endif
			 // vala-lint=block-opening-brace-space-before
			{ "about", about_activated },
			{ "compose", compose_activated },
			{ "back", back_activated },
			{ "refresh", refresh_activated },
			{ "search", search_activated },
			{ "quit", quit_activated },
			{ "back-home", back_home_activated },
			{ "scroll-page-down", scroll_view_page_down },
			{ "scroll-page-up", scroll_view_page_up },
			{ "goto-notifications", goto_notifications },
			{ "open-status-url", open_status_url, "s" },
			{ "answer-follow-request", answer_follow_request, "(ssb)" },
			{ "follow-back", follow_back, "(ss)" },
			{ "reply-to-status-uri", reply_to_status_uri, "(ss)" },
			{ "remove-from-followers", remove_from_followers, "(ss)" },
			{ "open-preferences", open_preferences },
			{ "open-current-account-profile", open_current_account_profile },
			{ "open-announcements", open_announcements },
			{ "open-follow-requests", open_follow_requests },
			{ "open-mutes-blocks", open_mutes_blocks },
			{ "open-scheduled-posts", open_scheduled_posts },
			{ "open-draft-posts", open_draft_posts },
			{ "open-admin-dashboard", open_admin_dashboard },
			{ "open-last-fediwrapped", open_last_fediwrapped },
			{ "open-collections", open_collections },
			{ "open-containing-folder", open_containing_folder, "s" },
			{ "increase-font-size", increase_font_size },
			{ "decrease-font-size", decrease_font_size },
			{ "reset-font-size", reset_font_size }
		};

		#if DEV_MODE
			private void dev_only_window_activated () {
				new Dialogs.Dev ().present (main_window);
			}
		#endif

		private void remove_from_followers (GLib.SimpleAction action, GLib.Variant? value) {
			if (value == null || accounts.active == null) return;

			accounts.active.remove_from_followers (
				value.get_child_value (0).get_string (),
				value.get_child_value (1).get_string ()
			);
		}

		private void reply_to_status_uri (GLib.SimpleAction action, GLib.Variant? value) {
			if (value == null || accounts.active == null) return;

			accounts.active.reply_to_status_uri (
				value.get_child_value (0).get_string (),
				value.get_child_value (1).get_string ()
			);
		}

		private void follow_back (GLib.SimpleAction action, GLib.Variant? value) {
			if (value == null || accounts.active == null) return;

			accounts.active.follow_back (
				value.get_child_value (0).get_string (),
				value.get_child_value (1).get_string ()
			);
		}

		private void goto_notifications () {
			Mastodon.Account.PLACE_NOTIFICATIONS.open_func (app.main_window);
		}

		private void open_status_url (GLib.SimpleAction action, GLib.Variant? value) {
			if (value == null || accounts.active == null) return;

			accounts.active.open_status_url (value.get_string ());
		}

		private void answer_follow_request (GLib.SimpleAction action, GLib.Variant? value) {
			if (value == null || accounts.active == null) return;

			accounts.active.answer_follow_request (
				value.get_child_value (0).get_string (),
				value.get_child_value (1).get_string (),
				value.get_child_value (2).get_boolean ()
			);
		}

		public void handle_web_ap (Uri uri) {
			if (accounts.active == null) return;

			accounts.active.resolve.begin (Utils.WebApHandler.from_uri (uri), (obj, res) => {
				try {
					accounts.active.resolve.end (res).open ();
				} catch (Error e) {
					string msg = @"Failed to resolve URL \"$uri\": $(e.message)";
					warning (msg);
					this.toast (msg);
				}
			});
		}

		private Utils.ShareHandler.ShareResult? to_share = null;
		public void handle_share () {
			if (to_share == null || accounts.active == null || accounts.active.instance_info == null) return;

			new Dialogs.Composer.Dialog ({to_share.text, to_share.cw, null, null, null, null, null, true, false});
			to_share = null;
		}

		construct {
			application_id = Build.DOMAIN;
			flags = ApplicationFlags.HANDLES_OPEN;

			add_main_option_entries (APP_OPTIONS);
			#if UNIFIEDPUSH
				this.handle_local_options.connect (on_handle_local_options);
			#endif
			app_locales = new Utils.Locales ();
		}

		#if UNIFIEDPUSH
			private int on_handle_local_options (GLib.VariantDict options) {
				if (up_bg) {
					this.flags |= GLib.ApplicationFlags.IS_SERVICE;
				}

				return -1;
			}
		#endif

		public static int main (string[] args) {
			#if GEXIV2
				GExiv2.initialize ();
			#endif
			#if GSTREAMER
				Gst.init (ref args);
			#endif
			#if CLAPPER
				Clapper.init (ref args);
				GLib.Environment.set_variable ("CLAPPER_USE_PLAYBIN3", "1", false);
			#endif
			cache_path = GLib.Path.build_path (GLib.Path.DIR_SEPARATOR_S, GLib.Environment.get_user_cache_dir (), Build.NAME.down ());

			try {
				bookwyrm_regex = new GLib.Regex ("/book/\\d+(/?$|/s/[-_a-z0-9]*)", GLib.RegexCompileFlags.OPTIMIZE);
			} catch (GLib.RegexError e) {
				critical (e.message);
			}

			try {
				custom_emoji_regex = new GLib.Regex ("(:[a-zA-Z0-9_]{2,}:)", GLib.RegexCompileFlags.OPTIMIZE);
			} catch (GLib.RegexError e) {
				critical (e.message);
			}

			try {
				rtl_regex = new GLib.Regex (
					"[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]",
					GLib.RegexCompileFlags.OPTIMIZE,
					GLib.RegexMatchFlags.ANCHORED
				);
			} catch (GLib.RegexError e) {
				critical (e.message);
			}

			is_flatpak = GLib.Environment.get_variable ("FLATPAK_ID") != null
			|| GLib.File.new_for_path ("/.flatpak-info").query_exists ();

			Intl.setlocale (LocaleCategory.ALL, "");
			Intl.bindtextdomain (Build.GETTEXT_PACKAGE, Build.LOCALEDIR);
			Intl.bind_textdomain_codeset (Build.GETTEXT_PACKAGE, "UTF-8");
			Intl.textdomain (Build.GETTEXT_PACKAGE);

			GLib.Environment.unset_variable ("GTK_THEME");
			#if WINDOWS || DARWIN || HAIKU || ANDROID
				GLib.Environment.set_variable ("SECRET_BACKEND", "file", false);
				if (GLib.Environment.get_variable ("SECRET_BACKEND") == "file")
					GLib.Environment.set_variable ("SECRET_FILE_TEST_PASSWORD", @"$(GLib.Environment.get_user_name ())$(Build.DOMAIN)", false);
			#endif
			#if ANDROID
				string assets_dir = "/data/data/dev.geopjr.tuba/files";
				g_io_openssl_load (null);
				GLib.Environment.set_variable ("FONTCONFIG_FILE", @"$assets_dir/etc/fonts/fonts.conf", true);
			#endif

			if (GLib.Environment.get_variable ("GSK_RENDERER") == "gl") {
				GLib.Environment.set_variable ("GDK_DEBUG", "gl-no-fractional", false);
			}

			network_monitor = NetworkMonitor.get_default ();

			app = new Application ();
			return app.run (args);
		}

		#if UNIFIEDPUSH
			private void on_new_endpoint (UnifiedPush.PushEndpoint obj) {;
				var req = new RequestV2 ("/api/v1/push/subscription", POST) { account = accounts.active };
				req.add_form_data ("subscription[endpoint]", obj.endpoint);
				req.add_form_data ("subscription[keys][p256dh]", obj.pubkey);
				req.add_form_data ("subscription[keys][auth]", obj.auth);
				req.add_form_data ("subscription[standard]", "true");
				req.add_form_data ("data[alerts][mention]", (!(InstanceAccount.KIND_MENTION in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][quote]", (!(InstanceAccount.KIND_QUOTE in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][status]", "true");
				req.add_form_data ("data[alerts][reblog]", (!(InstanceAccount.KIND_REBLOG in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][follow]", (!(InstanceAccount.KIND_FOLLOW in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][follow_request]", (!(InstanceAccount.KIND_FOLLOW_REQUEST in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][favourite]", (!(InstanceAccount.KIND_FAVOURITE in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][poll]", (!(InstanceAccount.KIND_POLL in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][update]", (!(InstanceAccount.KIND_EDITED in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][quoted_update]", (!(InstanceAccount.KIND_QUOTE_UPDATE in settings.muted_notification_types)).to_string ());
				req.add_form_data ("data[alerts][admin.sign_up]", "true");
				req.add_form_data ("data[alerts][admin.report]", "true");
				req.add_form_data ("data[policy]", "all");

				register_web_push.begin (req, accounts.active);
			}

			private async void register_web_push (RequestV2 msg, InstanceAccount account) {
				// remove existing subscription for user
				yield unregister_real (account);

				try {
					yield msg.exec (null);
				} catch (Error e) {
					warning (@"Couldn't add UnifiedPush subscription for $(account.handle): $(e.code) $(e.message)");
					app.toast ("%s: %s".printf (_("Error"), e.message));
				}

				if (account == accounts.active) {
					settings.unifiedpush_enabled = true;
				} else {
					(new Tuba.Settings.Account (account.uuid)).unifiedpush_enabled = true;
				}
				account.setup_notification_streams ();
			}

			private void on_up_message (UnifiedPush.PushMessage obj) {
				debug (@"UnifiedPush: new message for $(obj.instance) decrypted=$(obj.decrypted)");
				var instance_parts = obj.instance.split (":");
				string account_uuid = GLib.Uri.unescape_string (instance_parts[0]);
				//  string account_instance = GLib.Uri.unescape_string (instance_parts[1]);

				uint8[] raw = obj.content.data;
				uint8[] buf = new uint8[raw.length + 1];
				Memory.copy (buf, raw, raw.length);
				buf[raw.length] = 0;
				string string_payload = (string) buf;

				Json.Parser parser = new Json.Parser ();
				try {
					parser.load_from_data (string_payload);
				} catch (Error e) {
					warning (@"Couldn't parse notification payload: $(e.code) $(e.message)");
					return;
				}

				var payload = Helper.Entity.from_json (parser.get_root (), typeof (API.Push.Payload)) as API.Push.Payload;
				if (!(payload is API.Push.Payload)) return;

				// Don't want to spawn a mainloop and soup servers
				// just for a push while the app is closed, so just
				// send it as is then.
				if (accounts == null) {
					var toast = new GLib.Notification (payload.title);
					if (payload.body != null) {
						var body = Utils.Htmlx.remove_tags (payload.body);
						toast.set_body (body);
					}

					Icon? icon = null;
					var icon_file = GLib.File.new_for_uri (payload.icon);
					if (Tuba.is_flatpak) {
						try {
							// sync is okay here because it's running as a service, no GUI
							Bytes avatar_bytes = icon_file.load_bytes ();
							if (avatar_bytes != null)
								icon = new BytesIcon (avatar_bytes);
						} catch (Error e) {
							warning (@"Couldn't download UnifiedPush icon: $(e.code) $(e.message)");
						}
					} else {
						icon = new FileIcon (icon_file);
					}

					if (icon != null)
						toast.set_icon (icon);

					this.send_notification (@"$(account_uuid)-$(payload.notification_id)", toast);
				} else {
					handle_up_notification.begin (account_uuid, payload);
				}
			}

			private async void handle_up_notification (string account_uuid, API.Push.Payload payload) {
				var account = accounts.find_by_uuid (account_uuid);
				if (account == null) return;

				var req = new RequestV2 (@"/api/v1/notifications/$(payload.notification_id)") {
					account = account
				};

				try {
					var in_stream = yield req.exec (null);
					var parser = yield Network.get_parser_from_inputstream_async (in_stream);
					var node = network.parse_node (parser);
					var notification = Entity.from_json (typeof (API.Notification), node) as API.Notification;
					if (notification != null) account.on_notification_received (notification);
				} catch (Error e) {
					warning (@"Error while trying to fetch unifiedpush notification: $(e.code) $(e.message)");
				}
			}

			private void on_unregistered (string obj) {
				debug (@"UnifiedPush: unregistered $obj");
				unregister_real.begin (accounts.active);
				settings.unifiedpush_enabled = false;
				accounts.active.setup_notification_streams ();
			}

			private async void unregister_real (InstanceAccount account) {
				try {
					yield (new RequestV2 ("/api/v1/push/subscription", DELETE) { account = account }).exec (null);
				} catch (Error e) {
					warning (@"Couldn't remove UnifiedPush subscription for $(account.handle): $(e.code) $(e.message)");
				}
			}
		#endif

		private void on_network_change (bool online) {
			// We really need to avoid triggering it unnecessarily
			// wait 5 seconds before triggering it
			// also don't trust the 'online' status
			GLib.Timeout.add_once (5000, on_network_change_cb);
		}

		private void on_network_change_cb () {
			bool online = network_monitor.network_available;

			// Avoid triggering it as much as possible
			// as it causes websocket reconnects and
			// timeline refreshes
			if (is_online == online) return;
			is_online = online;
		}

		protected override void startup () {
			base.startup ();
#if UNIFIEDPUSH
			unifiedpush = new UnifiedPush.UnifiedPush ("dev.geopjr.Tuba.UnifiedPush");
			unifiedpush.new_endpoint.connect (on_new_endpoint);
			unifiedpush.message.connect (on_up_message);
			unifiedpush.unregistered.connect (on_unregistered);

			// Normally there would be a UI selector, but I believe there's
			// only one distributor for Linux in existence.
			if (!unifiedpush.try_use_default_distributor ()) {
				if (unifiedpush.list_distributors ().length () > 1) {
					unifiedpush.save_distributor (unifiedpush.list_distributors ().nth_data (0));
				} else {
					Tuba.up_distributor_found = false;
				}
			}

			if (!up_bg) {
				startup_real ();
			}
		}

		bool done_startup_real = false;
		private void startup_real () {
			if (done_startup_real) return;
			up_bg = false;
#endif
			try {
				var lines = troubleshooting.split ("\n");
				foreach (unowned string line in lines) {
					debug (line);
				}
				Gtk.Window.set_default_icon_name (Build.DOMAIN);
				Adw.init ();
				GtkSource.init ();

				var t_default_locale = Gtk.get_default_language ().to_string ();
				if (t_default_locale == "c") {
					default_locale = "en-US";
				} else {
					if (t_default_locale.index_of_char ('-') != -1) {
						var tdl_parts = t_default_locale.split ("-", 2);
						t_default_locale = @"$(tdl_parts[0].down ())-$(tdl_parts[1].up ())";
					}
					default_locale = t_default_locale;
				}

				settings = new Settings ();
				streams = new Streams ();
				network = new Network ();
				//  entity_cache = new EntityCache ();
				//  image_cache = new ImageCache () {
				//  	maintenance_secs = 60 * 5
				//  };
				//  blurhash_cache = new BlurhashCache () {
				//  	maintenance_secs = 30
				//  };
				accounts = new SecretAccountStore ();
				accounts.init ();

				//  css_provider.load_from_resource (@"$(Build.RESOURCES)app.css");
				//  StyleContext.add_provider_for_display (Gdk.Display.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
				//  StyleContext.add_provider_for_display (Gdk.Display.get_default (), zoom_css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
			} catch (Error e) {
				var msg = "Could not start application: %s".printf (e.message);
				error (msg);
			}

			var style_manager = Adw.StyleManager.get_default ();
			ColorScheme color_scheme = (ColorScheme) settings.get_enum ("color-scheme");
			style_manager.color_scheme = color_scheme.to_adwaita_scheme ();

			#if DEV_MODE
				set_accels_for_action ("app.dev-only-window", {"F2"});
			#endif
			set_accels_for_action ("app.about", {"F1"});
			set_accels_for_action ("app.open-preferences", {"<Ctrl>comma"});
			set_accels_for_action ("app.compose", {"<Ctrl>T", "<Ctrl>N"});
			set_accels_for_action ("app.back", {"<Alt>BackSpace", "<Alt>KP_Left"});
			set_accels_for_action ("app.refresh", {"<Ctrl>R", "F5"});
			set_accels_for_action ("app.search", {"<Ctrl>F", "<Ctrl>L"});
			set_accels_for_action ("app.quit", {"<Ctrl>Q"});
			set_accels_for_action ("window.close", {"<Ctrl>W"});
			set_accels_for_action ("app.back-home", {"<Alt>Home"});
			set_accels_for_action ("app.scroll-page-down", {"Page_Down"});
			set_accels_for_action ("app.scroll-page-up", {"Page_Up"});
			add_action_entries (APP_ENTRIES, this);

			if (settings.monitor_network)
				network_monitor.network_changed.connect (on_network_change);

			if (settings.proxy != "")
				on_proxy_change ();
			settings.notify ["proxy"].connect (on_proxy_notify);

			if (settings.analytics) app.update_analytics.begin ();
			app.update_contributors.begin ();

			// TODO: remove me next release
			// this handles the old setting and sets the new one
			if (settings.larger_font_size) {
				settings.larger_font_size = false;
				settings.status_font_size = 1.2;
			}
#if UNIFIEDPUSH
			done_startup_real = true;
#endif
		}

		private void on_proxy_change (bool recover = false) {
			if (settings.proxy != "") {
				try {
					if (Uri.is_valid (settings.proxy, UriFlags.NONE)) {
						proxy = new GLib.SimpleProxyResolver (settings.proxy, null);
					}
				} catch (Error e) {
					// translators: Toast that pops up when
					//				an invalid proxy url has
					//				been provided in settings
					app.toast (_("Invalid Proxy URL"));
					warning (e.message);
					return;
				}
			} else {
				proxy = GLib.ProxyResolver.get_default ();
			}

			if (recover && add_account_window == null) {
				GLib.Timeout.add_once (5000, trigger_is_online_notify);
			}
		}

		private void trigger_is_online_notify () {
			this.notify_property ("is-online");
		}

		private void on_proxy_notify () {
			on_proxy_change (true);
		}

		private bool activated = false;
		protected override void activate () {
			activated = true;
			present_window ();

			if (start_hidden) {
				start_hidden = false;
				return;
			}

			settings.delay ();
		}

		protected override void shutdown () {
			#if UNIFIEDPUSH
				if (up_bg) {
					base.shutdown ();
					return;
				}
			#endif

			#if !DEV_MODE
				settings.apply_all ();
			#endif
			network.flush_cache ();

			base.shutdown ();
		}

		public override void open (File[] files, string hint) {
			if (!activated) activate ();

			foreach (File file in files) {
				string unparsed_uri = file.get_uri ();

				try {
					Uri uri = Uri.parse (unparsed_uri, UriFlags.ENCODED);
					string scheme = uri.get_scheme ();

					switch (scheme) {
						case "tuba":
							if (add_account_window == null) {
								if (uri.get_host ().down () == "share") {
									to_share = Utils.ShareHandler.from_uri (uri);
									handle_share ();

									break;
								}

								// translators: the variable is a uri scheme like 'https'
								throw new Error.literal (-1, 1, _("'%s://' may only be used when adding a new account").printf (scheme));
							}
							add_account_window.redirect (uri);

							break;
						case "web+ap":
							// translators: the variable is a uri scheme like 'https'
							if (add_account_window != null)
								throw new Error.literal (-1, 2, _("'%s://' may not be used when adding a new account").printf (scheme));
							handle_web_ap (uri);

							break;
						default:
							// translators: the first variable is the app name ('Tuba'),
							//				the second one is a uri scheme like 'https'
							throw new Error.literal (-1, 3, _("%s does not accept '%s://'").printf (Build.NAME, scheme));
					}
				} catch (GLib.Error e) {
					string msg = @"Couldn't open $unparsed_uri: $(e.message)";
					warning (msg);
					var dlg = inform (_("Error"), msg);
					dlg.present (app.main_window);
				}
			}
		}

		public void present_window (bool destroy_main = false) {
			if (!activated && ApplicationFlags.IS_SERVICE in this.flags) return;
			#if UNIFIEDPUSH
				startup_real ();
			#endif

			if (accounts.saved.is_empty) {
				if (main_window != null && destroy_main)
					main_window.hide ();
				debug ("Presenting NewAccount dialog");
				if (add_account_window == null)
					new Dialogs.NewAccount ();
				add_account_window.present ();
			} else {
				debug ("Presenting MainWindow");
				if (main_window == null) {
					is_rtl = Gtk.Widget.get_default_direction () == Gtk.TextDirection.RTL;
					main_window = new Dialogs.MainWindow (this);
				}
				if (!start_hidden) main_window.present ();
			}

			if (main_window != null)
				main_window.close_request.connect (on_window_closed);
		}

		public bool on_window_closed () {
			if (!settings.work_in_background || accounts.saved.is_empty) {
				main_window.hide_on_close = false;
			} else {
				main_window.hide_on_close = true;
				#if !DEV_MODE
					settings.apply_all ();
				#endif
			}

			return false;
		}

		void compose_activated () {
			if (accounts.active.instance_info == null) return;

			new Dialogs.Composer.Dialog ();
		}

		void back_activated () {
			main_window.back ();
		}

		void search_activated () {
			main_window.open_view (new Views.Search ());
		}

		void quit_activated () {
			app.quit ();
		}

		void refresh_activated () {
			refresh ();
		}

		void back_home_activated () {
			main_window.go_back_to_start ();
		}

		void scroll_view_page_down () {
			main_window.scroll_view_page ();
		}

		void scroll_view_page_up () {
			main_window.scroll_view_page (true);
		}

		void open_preferences () {
			new Dialogs.Preferences ().present (main_window);
		}

		void open_current_account_profile () {
			accounts.active.open ();
			close_sidebar ();
		}

		public void open_announcements () {
			main_window.open_view (new Views.Announcements () {
				dismiss_all_announcements = true // dismiss all by default I guess
			});
			close_sidebar ();
			if (accounts.active != null) accounts.active.unread_announcements = 0;
		}

		public void open_follow_requests () {
			main_window.open_view (new Views.FollowRequests ());
			close_sidebar ();
			if (accounts.active != null) accounts.active.unreviewed_follow_requests = 0;
		}

		public void open_mutes_blocks () {
			main_window.open_view (new Views.MutesBlocks ());
			close_sidebar ();
		}

		public void open_scheduled_posts () {
			main_window.open_view (new Views.ScheduledStatuses ());
			close_sidebar ();
		}

		public void open_draft_posts () {
			main_window.open_view (new Views.DraftStatuses ());
			close_sidebar ();
		}

		public void open_admin_dashboard () {
			new Dialogs.Admin.Window ().present ();
		}

		public void open_last_fediwrapped () {
			accounts.active.open_latest_wrapped ();
		}

		public void open_collections () {
			main_window.open_view (new Views.Collections ());
			close_sidebar ();
		}

		public void open_containing_folder (GLib.SimpleAction action, GLib.Variant? value) {
			if (value == null) return;
			Utils.Host.open_containing_folder.begin (value.get_string ());
		}

		private void increase_font_size () {
			if (settings.status_font_size < 2.0) settings.status_font_size += 0.1;
		}

		private void decrease_font_size () {
			if (settings.status_font_size > 0.8) settings.status_font_size -= 0.1;
		}

		private void reset_font_size () {
			settings.status_font_size = 1.0;
		}

		private void close_sidebar () {
			var split_view = app.main_window.split_view;
			if (split_view.collapsed)
				split_view.show_sidebar = false;
		}

		string troubleshooting = "os: %s %s\nprefix: %s\nflatpak: %s\nversion: %s (%s)\ngtk: %u.%u.%u (%d.%d.%d)\nlibadwaita: %u.%u.%u (%d.%d.%d)\nlibsoup: %u.%u.%u (%d.%d.%d)%s\nlibspelling: %s\nClapper: %s\nGStreamer: %s\nGExiv2: %s".printf ( // vala-lint=line-length
				GLib.Environment.get_os_info ("NAME"), GLib.Environment.get_os_info ("VERSION"),
				Build.PREFIX,
				Tuba.is_flatpak.to_string (),
				Build.VERSION, Build.PROFILE,
				Gtk.get_major_version (), Gtk.get_minor_version (), Gtk.get_micro_version (),
				Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION,
				Adw.get_major_version (), Adw.get_minor_version (), Adw.get_micro_version (),
				Adw.MAJOR_VERSION, Adw.MINOR_VERSION, Adw.MICRO_VERSION,
				Soup.get_major_version (), Soup.get_minor_version (), Soup.get_micro_version (),
				Soup.MAJOR_VERSION, Soup.MINOR_VERSION, Soup.MICRO_VERSION,
				#if GTKSOURCEVIEW_5_7_1
					"\nlibgtksourceview: %u.%u.%u (%d.%d.%d)".printf (
						GtkSource.get_major_version (), GtkSource.get_minor_version (), GtkSource.get_micro_version (),
						GtkSource.MAJOR_VERSION, GtkSource.MINOR_VERSION, GtkSource.MICRO_VERSION
					)
				#else
					""
				#endif
				,
				#if LIBSPELLING
					"true"
				#else
					"false"
				#endif
				,
				#if CLAPPER
					@"$(Clapper.VERSION_S) ($(ClapperGtk.VERSION_S))"
				#else
					"false"
				#endif
				,
				#if GSTREAMER
					@"$(Gst.version_string ()) ($(Gst.VERSION_MAJOR).$(Gst.VERSION_MICRO).$(Gst.VERSION_MINOR).$(Gst.VERSION_NANO))"
				#else
					"false"
				#endif
				,
				#if GEXIV2
					@"$(GExiv2.get_version ()) ($(GExiv2.MAJOR_VERSION).$(GExiv2.MINOR_VERSION).$(GExiv2.MICRO_VERSION))"
				#else
					"false"
				#endif
			);

		void about_activated () {
			const string[] ARTISTS = {
				"Tobias Bernard",
				"Jakub Steiner"
			};

			const string[] DESIGNERS = {
				"Tobias Bernard",
				"Brage Fuglseth"
			};

			const string[] DEVELOPERS = {
				"Evangelos “GeopJr” Paterakis",
				"bleak_grey",
				"Sergey Bugaev"
			};

			const string COPYRIGHT = "© 2018-2022 bleak_grey\n© 2022 Evangelos “GeopJr” Paterakis";

			var dialog = new Adw.AboutDialog.from_appdata ("/dev/geopjr/Tuba/metainfo.xml", Build.PROFILE == "development" ? null : Build.VERSION) {
				version = Build.VERSION,
				copyright = COPYRIGHT,
				developers = DEVELOPERS,
				artists = ARTISTS,
				designers = DESIGNERS,
				debug_info = troubleshooting,
				debug_info_filename = @"$(Build.NAME).txt",
				// translators: Name <email@domain.com> or Name https://website.example
				translator_credits = _("translator-credits")
			};

			// translators: Wiki pages / Guides
			dialog.add_link (_("Wiki"), Build.WIKI_WEBSITE);

			dialog.add_link (_("Translate"), Build.TRANSLATE_WEBSITE);
			dialog.add_link (_("Donate"), Build.DONATE_WEBSITE);

			// translators: Application metainfo for the app "Archives". <https://codeberg.org/GeopJr/Archives/>
			dialog.add_other_app ("dev.geopjr.Archives", _("Archives"), _("Create and view web archives"));
			// translators: Application metainfo for the app "Calligraphy". <https://codeberg.org/GeopJr/Calligraphy>
			dialog.add_other_app ("dev.geopjr.Calligraphy", _("Calligraphy"), _("Turn text into ASCII banners"));
			// translators: Application metainfo for the app "Collision". <https://codeberg.org/GeopJr/Collision>
			dialog.add_other_app ("dev.geopjr.Collision", _("Collision"), _("Check hashes for your files"));
			// translators: Application metainfo for the app "Turntable". <https://codeberg.org/GeopJr/Turntable>
			dialog.add_other_app ("dev.geopjr.Turntable", _("Turntable"), _("Scrobble your music"));

			// For some obscure reason, const arrays produce duplicates in the credits.
			// Static functions seem to avoid this peculiar behavior.
			//  dialog.translator_credits = Build.TRANSLATOR != " " ? Build.TRANSLATOR : null;

			dialog.present (main_window);

			GLib.Idle.add (() => {
				var style = Utils.Celebrate.get_celebration_css_class (new GLib.DateTime.now ());
				if (style != "")
					dialog.add_css_class (style);
				return GLib.Source.REMOVE;
			});
		}

		public Adw.AlertDialog inform (string text, string? msg = null) {
			var dlg = new Adw.AlertDialog (
				text,
				msg
			);

			dlg.add_response ("ok", _("OK"));

			return dlg;
		}

		public struct QuestionButton {
			public string label;
			public Adw.ResponseAppearance appearance;
		}

		public struct QuestionButtons {
			public QuestionButton yes;
			public QuestionButton no;
		}

		public struct QuestionText {
			public string text;
			public bool use_markup;
		}

		public enum QuestionAnswer {
			YES,
			NO,
			CLOSE;

			public static QuestionAnswer from_string (string answer) {
				switch (answer.down ()) {
					case "yes":
						return YES;
					case "no":
						return NO;
					default:
						return CLOSE;
				}
			}

			public bool truthy () {
				return this == YES;
			}

			public bool falsy () {
				return this != YES;
			}
		}

		public async QuestionAnswer question (
			QuestionText title,
			QuestionText? msg = null,
			Gtk.Widget? win = app.main_window,
			QuestionButtons buttons = {
				{ _("Yes"), Adw.ResponseAppearance.DEFAULT },
				{ _("Cancel"), Adw.ResponseAppearance.DEFAULT }
			},
			Gtk.Widget? child = null,
			bool skip = false // skip the dialog, used for preferences to avoid duplicate code
		) {
			if (skip) return QuestionAnswer.YES;

			var dlg = new Adw.AlertDialog (
				title.text,
				msg == null ? null : msg.text
			);

			dlg.heading_use_markup = title.use_markup;
			if (msg != null) dlg.body_use_markup = msg.use_markup;

			if (child != null) dlg.extra_child = child;

			dlg.add_response ("no", buttons.no.label);
			dlg.set_response_appearance ("no", buttons.no.appearance);

			dlg.add_response ("yes", buttons.yes.label);
			dlg.set_response_appearance ("yes", buttons.yes.appearance);

			return QuestionAnswer.from_string (yield dlg.choose (win, null));
		}

		public string generate_analytics_object (bool pretty = false) {
			var generator = new Json.Generator () {
				pretty = pretty
			};
			var builder = new Json.Builder ();
			builder.begin_object ();

			builder.set_member_name ("accounts");
			builder.begin_array ();
			accounts.saved.foreach (e => {
				builder.add_string_value (e.uuid);
				return true;
			});
			builder.end_array ();

			builder.set_member_name ("analytics");
			builder.add_value (settings.to_debug_json ().get_root ());

			builder.end_object ();
			generator.set_root (builder.get_root ());
			return generator.to_data (null);
		}

		public async void update_contributors () {
			if (!settings.update_contributors) {
				// if updating contributors from the API is not enabled
				// but it has been enabled at some point in the past,
				// revert the contributors array to the default.
				// That will allow us to force clients that have
				// updated them manually in the past to use the bundled
				// array which might be more up-to-date.
				if (settings.last_contributors_update != null && settings.last_contributors_update != "") {
					settings.reset ("contributors");
					settings.last_contributors_update = "";
				}

				return;
			}

			bool can_update = false;
			GLib.DateTime now_utc = new GLib.DateTime.now_utc ();

			if (settings.last_contributors_update == null || settings.last_contributors_update == "") {
				can_update = true;
			} else {
				GLib.DateTime? then_utc = new GLib.DateTime.from_iso8601 (settings.last_contributors_update, null);
				if (then_utc == null) {
					can_update = true;
				} else {
					can_update = now_utc.difference (then_utc) > TimeSpan.DAY * 14;
				}
			}

			if (!can_update) return;
			var msg = new RequestV2 ("https://api.tuba.geopjr.dev/v1/supporters");

			try {
				var in_stream = yield msg.exec (null);
				Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);

				string[] new_contributors = {};
				Network.parse_array (parser, node => {
					if (node != null) {
						new_contributors += node.get_string ();
					}
				});

				settings.contributors = new_contributors;
				settings.last_contributors_update = now_utc.format_iso8601 ();
			} catch (Error e) {
				warning (@"Couldn't update contributors: $(e.code) $(e.message)");
			}
		}

		public async void update_analytics () {
			if (!settings.analytics) return;

			bool can_update = false;
			GLib.DateTime now_utc = new GLib.DateTime.now_utc ();

			if (settings.last_analytics_update == null || settings.last_analytics_update == "") {
				can_update = true;
			} else {
				GLib.DateTime? then_utc = new GLib.DateTime.from_iso8601 (settings.last_analytics_update, null);
				if (then_utc == null) {
					can_update = true;
				} else {
					can_update = now_utc.difference (then_utc) > TimeSpan.DAY * 14;
				}
			}

			if (!can_update) return;
			var msg = new RequestV2 ("https://api.tuba.geopjr.dev/v1/analytics", POST);
			msg.set_body ("application/json", new Bytes.take (generate_analytics_object ().data));

			try {
				yield msg.exec (null);
				settings.last_analytics_update = now_utc.format_iso8601 ();
			} catch (Error e) {
				warning (@"Couldn't update analytics: $(e.code) $(e.message)");
			}

		}
	}

	public static void toggle_css (Gtk.Widget wdg, bool state, string style) {
		if (state) {
			wdg.add_css_class (style);
		} else {
			wdg.remove_css_class (style);
		}
	}
}
