From eb11c6028973b945361095d3f4791ac6f61379a9 Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Fri, 13 Dec 2019 19:00:26 +0300 Subject: [PATCH 01/52] Disable rate limiter for socket/localhost (unless RemoteIp is enabled) --- CHANGELOG.md | 1 + .../plugs/rate_limiter/rate_limiter.ex | 37 +++++++++++++++---- test/plugs/rate_limiter_test.exs | 35 ++++++++++++++++++ .../controllers/account_controller_test.exs | 1 + 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 213742545..664c101a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Deprecated `User.Info` embedded schema (fields moved to `User`) - Store status data inside Flag activity - Deprecated (reorganized as `UserRelationship` entity) User fields with user AP IDs (`blocks`, `mutes`, `muted_reblogs`, `muted_notifications`, `subscribers`). +- Rate limiter is now disabled for localhost/socket (unless remoteip plug is enabled)
API Changes diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex index d720508c8..7fb92489c 100644 --- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex +++ b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex @@ -67,6 +67,8 @@ defmodule Pleroma.Plugs.RateLimiter do alias Pleroma.Plugs.RateLimiter.LimiterSupervisor alias Pleroma.User + require Logger + def init(opts) do limiter_name = Keyword.get(opts, :name) @@ -89,18 +91,39 @@ def init(opts) do def call(conn, nil), do: conn def call(conn, settings) do - settings - |> incorporate_conn_info(conn) - |> check_rate() - |> case do - {:ok, _count} -> + case disabled?() do + true -> + if Pleroma.Config.get(:env) == :prod, + do: Logger.warn("Rate limiter is disabled for localhost/socket") + conn - {:error, _count} -> - render_throttled_error(conn) + false -> + settings + |> incorporate_conn_info(conn) + |> check_rate() + |> case do + {:ok, _count} -> + conn + + {:error, _count} -> + render_throttled_error(conn) + end end end + def disabled? do + localhost_or_socket = + Pleroma.Config.get([Pleroma.Web.Endpoint, :http, :ip]) + |> Tuple.to_list() + |> Enum.join(".") + |> String.match?(~r/^local|^127.0.0.1/) + + remote_ip_disabled = not Pleroma.Config.get([Pleroma.Plugs.RemoteIp, :enabled]) + + localhost_or_socket and remote_ip_disabled + end + def inspect_bucket(conn, name_root, settings) do settings = settings diff --git a/test/plugs/rate_limiter_test.exs b/test/plugs/rate_limiter_test.exs index 49f63c424..f3343abca 100644 --- a/test/plugs/rate_limiter_test.exs +++ b/test/plugs/rate_limiter_test.exs @@ -16,6 +16,7 @@ defmodule Pleroma.Plugs.RateLimiterTest do test "config is required for plug to work" do limiter_name = :test_init Pleroma.Config.put([:rate_limit, limiter_name], {1, 1}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} == RateLimiter.init(name: limiter_name) @@ -23,11 +24,39 @@ test "config is required for plug to work" do assert nil == RateLimiter.init(name: :foo) end + test "it is disabled for localhost" do + limiter_name = :test_init + Pleroma.Config.put([:rate_limit, limiter_name], {1, 1}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1}) + Pleroma.Config.put([Pleroma.Plugs.RemoteIp, :enabled], false) + + assert RateLimiter.disabled?() == true + end + + test "it is disabled for socket" do + limiter_name = :test_init + Pleroma.Config.put([:rate_limit, limiter_name], {1, 1}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {:local, "/path/to/pleroma.sock"}) + Pleroma.Config.put([Pleroma.Plugs.RemoteIp, :enabled], false) + + assert RateLimiter.disabled?() == true + end + + test "it is enabled for socket when remote ip is enabled" do + limiter_name = :test_init + Pleroma.Config.put([:rate_limit, limiter_name], {1, 1}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {:local, "/path/to/pleroma.sock"}) + Pleroma.Config.put([Pleroma.Plugs.RemoteIp, :enabled], true) + + assert RateLimiter.disabled?() == false + end + test "it restricts based on config values" do limiter_name = :test_opts scale = 80 limit = 5 + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit}) opts = RateLimiter.init(name: limiter_name) @@ -61,6 +90,7 @@ test "`bucket_name` option overrides default bucket name" do limiter_name = :test_bucket_name Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) base_bucket_name = "#{limiter_name}:group1" opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name) @@ -75,6 +105,7 @@ test "`bucket_name` option overrides default bucket name" do test "`params` option allows different queries to be tracked independently" do limiter_name = :test_params Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) opts = RateLimiter.init(name: limiter_name, params: ["id"]) @@ -90,6 +121,7 @@ test "`params` option allows different queries to be tracked independently" do test "it supports combination of options modifying bucket name" do limiter_name = :test_options_combo Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) base_bucket_name = "#{limiter_name}:group1" opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"]) @@ -109,6 +141,7 @@ test "it supports combination of options modifying bucket name" do test "are restricted based on remote IP" do limiter_name = :test_unauthenticated Pleroma.Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}]) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) opts = RateLimiter.init(name: limiter_name) @@ -147,6 +180,7 @@ test "can have limits seperate from unauthenticated connections" do scale = 1000 limit = 5 + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}]) opts = RateLimiter.init(name: limiter_name) @@ -179,6 +213,7 @@ test "can have limits seperate from unauthenticated connections" do test "diffrerent users are counted independently" do limiter_name = :test_authenticated Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}]) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) opts = RateLimiter.init(name: limiter_name) diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index fa08ae4df..14d97e248 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -766,6 +766,7 @@ test "returns error when user already registred", %{conn: conn, valid_params: va end test "rate limit", %{conn: conn} do + Pleroma.Config.put([Pleroma.Plugs.RemoteIp, :enabled], true) app_token = insert(:oauth_token, user: nil) conn = From e93cc561cd42ff4ca7f3c95cdbf8dfa7fb9f4a74 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 25 Jan 2020 18:42:04 +0300 Subject: [PATCH 02/52] restarting pleroma from outside application --- lib/pleroma/config/loader.ex | 6 +- lib/pleroma/config/transfer_task.ex | 95 +++++++++++++++---- .../web/admin_api/admin_api_controller.ex | 33 +++++-- .../web/admin_api/views/config_view.ex | 10 +- lib/pleroma/web/router.ex | 1 + mix.exs | 10 +- mix.lock | 1 + test/config/transfer_task_test.exs | 67 +++++++++++++ .../admin_api/admin_api_controller_test.exs | 43 ++++++++- 9 files changed, 231 insertions(+), 35 deletions(-) diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index 68b247381..b8787cb49 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -3,8 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Config.Loader do - @paths ["config/config.exs", "config/#{Mix.env()}.exs"] - @reject_keys [ Pleroma.Repo, Pleroma.Web.Endpoint, @@ -35,8 +33,8 @@ defp do_merge(conf1, conf2), do: Mix.Config.merge(conf1, conf2) def load_and_merge do all_paths = if Pleroma.Config.get(:release), - do: @paths ++ ["config/releases.exs"], - else: @paths + do: ["config/config.exs", "config/releases.exs"], + else: ["config/config.exs"] all_paths |> Enum.map(&load(&1)) diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index d54f38ee4..6c5ba1f95 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -10,6 +10,30 @@ defmodule Pleroma.Config.TransferTask do require Logger + @type env() :: :test | :benchmark | :dev | :prod + + @reboot_time_keys [ + {:pleroma, :hackney_pools}, + {:pleroma, :chat}, + {:pleroma, Oban}, + {:pleroma, :rate_limit}, + {:pleroma, :markup}, + {:plerome, :streamer} + ] + + @reboot_time_subkeys [ + {:pleroma, Pleroma.Captcha, [:seconds_valid]}, + {:pleroma, Pleroma.Upload, [:proxy_remote]}, + {:pleroma, :instance, [:upload_limit]}, + {:pleroma, :email_notifications, [:digest]}, + {:pleroma, :oauth2, [:clean_expired_tokens]}, + {:pleroma, Pleroma.ActivityExpiration, [:enabled]}, + {:pleroma, Pleroma.ScheduledActivity, [:enabled]}, + {:pleroma, :gopher, [:enabled]} + ] + + @reject [nil, :prometheus] + def start_link(_) do load_and_update_env() if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo) @@ -17,21 +41,34 @@ def start_link(_) do end @spec load_and_update_env([ConfigDB.t()]) :: :ok | false - def load_and_update_env(deleted \\ []) do + def load_and_update_env(deleted \\ [], restart_pleroma? \\ true) do with true <- Pleroma.Config.get(:configurable_from_database), true <- Ecto.Adapters.SQL.table_exists?(Repo, "config"), started_applications <- Application.started_applications() do # We need to restart applications for loaded settings take effect + in_db = Repo.all(ConfigDB) with_deleted = in_db ++ deleted - with_deleted - |> Enum.map(&merge_and_update(&1)) - |> Enum.uniq() - # TODO: some problem with prometheus after restart! - |> Enum.reject(&(&1 in [:pleroma, nil, :prometheus])) - |> Enum.each(&restart(started_applications, &1)) + reject_for_restart = if restart_pleroma?, do: @reject, else: [:pleroma | @reject] + + applications = + with_deleted + |> Enum.map(&merge_and_update(&1)) + |> Enum.uniq() + # TODO: some problem with prometheus after restart! + |> Enum.reject(&(&1 in reject_for_restart)) + + # to be ensured that pleroma will be restarted last + applications = + if :pleroma in applications do + List.delete(applications, :pleroma) ++ [:pleroma] + else + applications + end + + Enum.each(applications, &restart(started_applications, &1, Pleroma.Config.get(:env))) :ok end @@ -43,12 +80,25 @@ defp merge_and_update(setting) do group = ConfigDB.from_string(setting.group) default = Pleroma.Config.Holder.config(group, key) - merged_value = merge_value(setting, default, group, key) + value = ConfigDB.from_binary(setting.value) + + merged_value = + if Ecto.get_meta(setting, :state) == :deleted do + default + else + if can_be_merged?(default, value) do + ConfigDB.merge_group(group, key, default, value) + else + value + end + end :ok = update_env(group, key, merged_value) if group != :logger do - group + if group != :pleroma or pleroma_need_restart?(group, key, value) do + group + end else # change logger configuration in runtime, without restart if Keyword.keyword?(merged_value) and @@ -76,22 +126,31 @@ defp merge_and_update(setting) do end end - defp merge_value(%{__meta__: %{state: :deleted}}, default, _group, _key), do: default + @spec pleroma_need_restart?(atom(), atom(), any()) :: boolean() + def pleroma_need_restart?(group, key, value) do + group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value) + end - defp merge_value(setting, default, group, key) do - value = ConfigDB.from_binary(setting.value) + defp group_and_key_need_reboot?(group, key) do + Enum.any?(@reboot_time_keys, fn {g, k} -> g == group and k == key end) + end - if can_be_merged?(default, value) do - ConfigDB.merge_group(group, key, default, value) - else - value - end + defp group_and_subkey_need_reboot?(group, key, value) do + Keyword.keyword?(value) and + Enum.any?(@reboot_time_subkeys, fn {g, k, subkeys} -> + g == group and k == key and + Enum.any?(Keyword.keys(value), &(&1 in subkeys)) + end) end defp update_env(group, key, nil), do: Application.delete_env(group, key) defp update_env(group, key, value), do: Application.put_env(group, key, value) - defp restart(started_applications, app) do + defp restart(_, :pleroma, :test), do: Logger.warn("pleroma restarted") + + defp restart(_, :pleroma, _), do: send(Restarter.Pleroma, :after_boot) + + defp restart(started_applications, app, _) do with {^app, _, _} <- List.keyfind(started_applications, app, 0), :ok <- Application.stop(app) do :ok = Application.start(app) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 2314d3274..6f0449418 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -890,17 +890,36 @@ def config_update(conn, %{"configs" => configs}) do Ecto.get_meta(config, :state) == :deleted end) - Pleroma.Config.TransferTask.load_and_update_env(deleted) + Pleroma.Config.TransferTask.load_and_update_env(deleted, false) - Mix.Tasks.Pleroma.Config.run([ - "migrate_from_db", - "--env", - to_string(Pleroma.Config.get(:env)) - ]) + need_reboot? = + Enum.any?(updated, fn config -> + group = ConfigDB.from_string(config.group) + key = ConfigDB.from_string(config.key) + value = ConfigDB.from_binary(config.value) + Pleroma.Config.TransferTask.pleroma_need_restart?(group, key, value) + end) + + response = %{configs: updated} + + response = + if need_reboot?, do: Map.put(response, :need_reboot, need_reboot?), else: response conn |> put_view(ConfigView) - |> render("index.json", %{configs: updated}) + |> render("index.json", response) + end + end + + def restart(conn, _params) do + with :ok <- configurable_from_database(conn) do + if Pleroma.Config.get(:env) == :test do + Logger.warn("pleroma restarted") + else + send(Restarter.Pleroma, {:restart, 50}) + end + + json(conn, %{}) end end diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex index 23d97e847..bbb53efcd 100644 --- a/lib/pleroma/web/admin_api/views/config_view.ex +++ b/lib/pleroma/web/admin_api/views/config_view.ex @@ -5,10 +5,16 @@ defmodule Pleroma.Web.AdminAPI.ConfigView do use Pleroma.Web, :view - def render("index.json", %{configs: configs}) do - %{ + def render("index.json", %{configs: configs} = params) do + map = %{ configs: render_many(configs, __MODULE__, "show.json", as: :config) } + + if params[:need_reboot] do + Map.put(map, :need_reboot, true) + else + map + end end def render("show.json", %{config: config}) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ef6e5a565..43fee8a0f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -197,6 +197,7 @@ defmodule Pleroma.Web.Router do post("/config", AdminAPIController, :config_update) get("/config/descriptions", AdminAPIController, :config_descriptions) get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) + get("/restart", AdminAPIController, :restart) get("/moderation_log", AdminAPIController, :list_log) diff --git a/mix.exs b/mix.exs index 0aa7c862f..1d0b59e56 100644 --- a/mix.exs +++ b/mix.exs @@ -8,7 +8,7 @@ def project do elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), - elixirc_options: [warnings_as_errors: true], + elixirc_options: [warnings_as_errors: warnings_as_errors(Mix.env())], xref: [exclude: [:eldap]], start_permanent: Mix.env() == :prod, aliases: aliases(), @@ -73,6 +73,11 @@ defp elixirc_paths(:benchmark), do: ["lib", "benchmarks"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] + defp warnings_as_errors(:prod), do: false + # Uncomment this if you need testing configurable_from_database logic + # defp warnings_as_errors(:dev), do: false + defp warnings_as_errors(_), do: true + # Specifies OAuth dependencies. defp oauth_deps do oauth_strategy_packages = @@ -166,7 +171,8 @@ defp deps do {:captcha, git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, - {:mox, "~> 0.5", only: :test} + {:mox, "~> 0.5", only: :test}, + {:restarter, git: "https://git.pleroma.social/alex.s/restarter"} ] ++ oauth_deps() end diff --git a/mix.lock b/mix.lock index c1fe223c0..538337cf3 100644 --- a/mix.lock +++ b/mix.lock @@ -94,6 +94,7 @@ "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]}, + "restarter": {:git, "https://git.pleroma.social/alex.s/restarter", "1932655b80a1409405d897911c06ebee4ac8c2d8", []}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index 53e8703fd..0d328f0c3 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -5,6 +5,8 @@ defmodule Pleroma.Config.TransferTaskTest do use Pleroma.DataCase + import ExUnit.CaptureLog + alias Pleroma.Config.TransferTask alias Pleroma.ConfigDB @@ -105,4 +107,69 @@ test "transfer config values with full subkey update" do Application.put_env(:pleroma, :assets, assets) end) end + + describe "pleroma restart" do + test "don't restart if no reboot time settings were changed" do + emoji = Application.get_env(:pleroma, :emoji) + on_exit(fn -> Application.put_env(:pleroma, :emoji, emoji) end) + + ConfigDB.create(%{ + group: ":pleroma", + key: ":emoji", + value: [groups: [a: 1, b: 2]] + }) + + assert capture_log(fn -> TransferTask.start_link([]) end) =~ "" + end + + test "restart pleroma on reboot time key" do + chat = Application.get_env(:pleroma, :chat) + on_exit(fn -> Application.put_env(:pleroma, :chat, chat) end) + + ConfigDB.create(%{ + group: ":pleroma", + key: ":chat", + value: [enabled: false] + }) + + assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" + end + + test "restart pleroma on reboot time subkey" do + captcha = Application.get_env(:pleroma, Pleroma.Captcha) + on_exit(fn -> Application.put_env(:pleroma, Pleroma.Captcha, captcha) end) + + ConfigDB.create(%{ + group: ":pleroma", + key: "Pleroma.Captcha", + value: [seconds_valid: 60] + }) + + assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" + end + + test "don't restart pleroma on reboot time key and subkey if there is false flag" do + chat = Application.get_env(:pleroma, :chat) + captcha = Application.get_env(:pleroma, Pleroma.Captcha) + + on_exit(fn -> + Application.put_env(:pleroma, :chat, chat) + Application.put_env(:pleroma, Pleroma.Captcha, captcha) + end) + + ConfigDB.create(%{ + group: ":pleroma", + key: ":chat", + value: [enabled: false] + }) + + ConfigDB.create(%{ + group: ":pleroma", + key: "Pleroma.Captcha", + value: [seconds_valid: 60] + }) + + assert capture_log(fn -> TransferTask.load_and_update_env([], false) end) =~ "" + end + end end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 5c767219a..81e346fb8 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -2043,7 +2043,6 @@ test "POST /api/pleroma/admin/config error", %{conn: conn} do Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) Application.put_env(:pleroma, :http, http) Application.put_env(:tesla, :adapter, Tesla.Mock) - :ok = File.rm("config/test.exported_from_db.secret.exs") end) end @@ -2170,7 +2169,7 @@ test "create new config setting in db", %{conn: conn} do assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} end - test "save config setting without key", %{conn: conn} do + test "save configs setting without explicit key", %{conn: conn} do level = Application.get_env(:quack, :level) meta = Application.get_env(:quack, :meta) webhook_url = Application.get_env(:quack, :webhook_url) @@ -2256,6 +2255,34 @@ test "saving config with partial update", %{conn: conn} do } end + test "saving config which need pleroma reboot", %{conn: conn} do + chat = Pleroma.Config.get(:chat) + on_exit(fn -> Pleroma.Config.put(:chat, chat) end) + + conn = + post( + conn, + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + end + test "saving config with nested merge", %{conn: conn} do config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2])) @@ -3001,6 +3028,18 @@ test "returns error if configuration from database is off", %{conn: conn} do end end + describe "GET /api/pleroma/admin/restart" do + clear_config(:configurable_from_database) do + Pleroma.Config.put(:configurable_from_database, true) + end + + test "pleroma restarts", %{conn: conn} do + ExUnit.CaptureLog.capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} + end) =~ "pleroma restarted" + end + end + describe "GET /api/pleroma/admin/users/:nickname/statuses" do setup do user = insert(:user) From ac97d01fb6c3eae653ee626e21a62f74362e07cc Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 25 Jan 2020 19:21:21 +0300 Subject: [PATCH 03/52] right test --- test/config/transfer_task_test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index 0d328f0c3..61ab1440d 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -119,7 +119,10 @@ test "don't restart if no reboot time settings were changed" do value: [groups: [a: 1, b: 2]] }) - assert capture_log(fn -> TransferTask.start_link([]) end) =~ "" + refute String.contains?( + capture_log(fn -> TransferTask.start_link([]) end), + "pleroma restarted" + ) end test "restart pleroma on reboot time key" do From 7c6e5c541de808957be8a1948fa7a20eba8734df Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 27 Jan 2020 19:48:20 +0300 Subject: [PATCH 04/52] docs update --- docs/API/admin_api.md | 13 +++++++++++++ docs/admin/config.md | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 07aa7ec3f..2c0c5f46b 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -665,6 +665,19 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - 404 Not Found `"Not found"` - On success: 200 OK `{}` +## `GET /api/pleroma/admin/restart` + +### Restarts pleroma application + +- Params: none +- Response: + - On failure: + - 400 Bad Request `"To use this endpoint you need to enable configuration from database."` + +```json +{} +``` + ## `GET /api/pleroma/admin/config/migrate_from_db` ### Run mix task pleroma.config migrate_from_db diff --git a/docs/admin/config.md b/docs/admin/config.md index 35e43b6a9..b39a73961 100644 --- a/docs/admin/config.md +++ b/docs/admin/config.md @@ -6,11 +6,7 @@ config :pleroma, configurable_from_database: true ``` ## How it works -Settings are stored in database and are applied in `runtime` after each change. Most of the settings take effect immediately, except some, which need instance reboot. These settings are needed in `compile time`, that's why settings are duplicated to the file. - -File with duplicated settings is located in `config/{env}.exported_from_db.exs` if pleroma is runned from source. For prod env it will be `config/prod.exported_from_db.exs`. - -For releases: `/etc/pleroma/prod.exported_from_db.secret.exs` or `PLEROMA_CONFIG_PATH/prod.exported_from_db.exs`. +Settings are stored in database and are applied in `runtime` after each change. Most of the settings take effect immediately, except some, which need instance reboot. ## How to set it up You need to migrate your existing settings to the database. This task will migrate only added by user settings. @@ -25,7 +21,7 @@ You can do this with mix task (all config files will remain untouched): mix pleroma.config migrate_to_db ``` -Now you can change settings in admin interface. After each save, settings from database are duplicated to the `config/{env}.exported_from_db.exs` file. +Now you can change settings in admin interface. If `reboot time` settings were changed, pleroma must be rebooted. **ATTENTION** @@ -35,10 +31,19 @@ Now you can change settings in admin interface. After each save, settings from d - all settings inside these keys: - `:hackney_pools` - `:chat` + - `Oban` + - `:rate_limit` + - `:markup` + - `:streamer` - partially settings inside these keys: - `:seconds_valid` in `Pleroma.Captcha` - `:proxy_remote` in `Pleroma.Upload` - `:upload_limit` in `:instance` + - `:digest` in `:email_notifications` + - `:clean_expired_tokens` in `:oauth2` + - `:enabled` in `Pleroma.ActivityExpiration` + - `:enabled` in `Pleroma.ScheduledActivity` + - `:enabled` in `:gopher` ## How to dump settings from database to file @@ -59,7 +64,7 @@ mix pleroma.config migrate_from_db [-d] ```sql TRUNCATE TABLE config; ``` -2. Delete `config/{env}.exported_from_db.exs`. +2. If migrate_from_db task was runned, backup and delete `config/{env}.exported_from_db.exs`. For `prod` env: ```bash From 91ea3ed82cc29f02ae8eec94e4e4ced325a7e008 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 28 Jan 2020 15:19:05 +0300 Subject: [PATCH 05/52] moving restarter application into pleroma repo --- mix.exs | 2 +- restarter/lib/pleroma.ex | 28 ++++++++++++++++++++++++++++ restarter/lib/restarter.ex | 8 ++++++++ restarter/mix .exs | 21 +++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 restarter/lib/pleroma.ex create mode 100644 restarter/lib/restarter.ex create mode 100644 restarter/mix .exs diff --git a/mix.exs b/mix.exs index 1d0b59e56..2608f986e 100644 --- a/mix.exs +++ b/mix.exs @@ -172,7 +172,7 @@ defp deps do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, - {:restarter, git: "https://git.pleroma.social/alex.s/restarter"} + {:restarter, path: "../restarter"} ] ++ oauth_deps() end diff --git a/restarter/lib/pleroma.ex b/restarter/lib/pleroma.ex new file mode 100644 index 000000000..da714654c --- /dev/null +++ b/restarter/lib/pleroma.ex @@ -0,0 +1,28 @@ +defmodule Restarter.Pleroma do + use GenServer + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + def init(_), do: {:ok, %{}} + + def handle_info(:after_boot, %{after_boot: true} = state), do: {:noreply, state} + + def handle_info(:after_boot, state) do + restart(:pleroma) + {:noreply, Map.put(state, :after_boot, true)} + end + + def handle_info({:restart, delay}, state) do + Process.sleep(delay) + restart(:pleroma) + {:noreply, state} + end + + defp restart(app) do + :ok = Application.ensure_started(app) + :ok = Application.stop(app) + :ok = Application.start(app) + end +end diff --git a/restarter/lib/restarter.ex b/restarter/lib/restarter.ex new file mode 100644 index 000000000..eadd86f89 --- /dev/null +++ b/restarter/lib/restarter.ex @@ -0,0 +1,8 @@ +defmodule Restarter do + use Application + + def start(_, _) do + opts = [strategy: :one_for_one, name: Restarter.Supervisor] + Supervisor.start_link([Restarter.Pleroma], opts) + end +end diff --git a/restarter/mix .exs b/restarter/mix .exs new file mode 100644 index 000000000..b0908aece --- /dev/null +++ b/restarter/mix .exs @@ -0,0 +1,21 @@ +defmodule Restarter.MixProject do + use Mix.Project + + def project do + [ + app: :restarter, + version: "0.1.0", + elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + mod: {Restarter, []} + ] + end + + defp deps, do: [] +end From aa3ba635131fb05b09ab487bdf68cbe5dda98257 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 28 Jan 2020 15:41:45 +0300 Subject: [PATCH 06/52] like this --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 2608f986e..ad23c7414 100644 --- a/mix.exs +++ b/mix.exs @@ -172,7 +172,7 @@ defp deps do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, - {:restarter, path: "../restarter"} + {:restarter, in_umbrella: true} ] ++ oauth_deps() end From 251bbd794a696655bee5df7a31727db7dfa60375 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 28 Jan 2020 13:29:44 +0000 Subject: [PATCH 07/52] Apply suggestion to mix.exs --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index ad23c7414..8cbea6f75 100644 --- a/mix.exs +++ b/mix.exs @@ -172,7 +172,7 @@ defp deps do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, - {:restarter, in_umbrella: true} + {:restarter, path: "./restarter"} ] ++ oauth_deps() end From da334ef9343b3ff96bf5802bc7e8864566e612dc Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 28 Jan 2020 13:29:48 +0000 Subject: [PATCH 08/52] Apply suggestion to mix.lock --- mix.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/mix.lock b/mix.lock index 538337cf3..c1fe223c0 100644 --- a/mix.lock +++ b/mix.lock @@ -94,7 +94,6 @@ "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]}, - "restarter": {:git, "https://git.pleroma.social/alex.s/restarter", "1932655b80a1409405d897911c06ebee4ac8c2d8", []}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, From 93b9f226e0163519b0bbb111191abef45893f23e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 28 Jan 2020 16:32:33 +0300 Subject: [PATCH 09/52] path fix --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 8cbea6f75..2608f986e 100644 --- a/mix.exs +++ b/mix.exs @@ -172,7 +172,7 @@ defp deps do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, - {:restarter, path: "./restarter"} + {:restarter, path: "../restarter"} ] ++ oauth_deps() end From 33bd8fbffea79b8ca510a098ad4654b8f01324d6 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 28 Jan 2020 18:02:11 +0300 Subject: [PATCH 10/52] filename and test fixes --- mix.exs | 2 +- restarter/{mix .exs => mix.exs} | 0 test/config/transfer_task_test.exs | 5 ++++- 3 files changed, 5 insertions(+), 2 deletions(-) rename restarter/{mix .exs => mix.exs} (100%) diff --git a/mix.exs b/mix.exs index 2608f986e..8cbea6f75 100644 --- a/mix.exs +++ b/mix.exs @@ -172,7 +172,7 @@ defp deps do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, - {:restarter, path: "../restarter"} + {:restarter, path: "./restarter"} ] ++ oauth_deps() end diff --git a/restarter/mix .exs b/restarter/mix.exs similarity index 100% rename from restarter/mix .exs rename to restarter/mix.exs diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index 61ab1440d..ebdc951cf 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -172,7 +172,10 @@ test "don't restart pleroma on reboot time key and subkey if there is false flag value: [seconds_valid: 60] }) - assert capture_log(fn -> TransferTask.load_and_update_env([], false) end) =~ "" + refute String.contains?( + capture_log(fn -> TransferTask.load_and_update_env([], false) end), + "pleroma restarted" + ) end end end From 6302b407916fb75db3d728176eb5da605dfc8349 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 28 Jan 2020 18:04:13 +0400 Subject: [PATCH 11/52] Warn if HTTPSecurityPlug is disabled --- lib/pleroma/application.ex | 1 + lib/pleroma/plugs/http_security_plug.ex | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index e17068876..2c8889ce5 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -33,6 +33,7 @@ def user_agent do def start(_type, _args) do Pleroma.HTML.compile_scrubbers() Pleroma.Config.DeprecationWarnings.warn() + Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.Repo.check_migrations_applied!() setup_instrumenters() load_custom_modules() diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index a7cc22831..8bc324f48 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do alias Pleroma.Config import Plug.Conn + require Logger + def init(opts), do: opts def call(conn, _options) do @@ -90,6 +92,15 @@ defp csp_string do |> Enum.join("; ") end + def warn_if_disabled do + unless Config.get([:http_security, :enabled]) do + Logger.warn("HTTP Security is disabled. Add this line to you config to enable it: + + config :pleroma, :http_security, enabled: true + ") + end + end + defp maybe_send_sts_header(conn, true) do max_age_sts = Config.get([:http_security, :sts_max_age]) max_age_ct = Config.get([:http_security, :ct_max_age]) From 77f24525ca6636f5fb0b3864c346be683566efd3 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 28 Jan 2020 16:40:44 +0100 Subject: [PATCH 12/52] Streamer: Correctly handle reblog mutes --- lib/pleroma/web/streamer/worker.ex | 3 ++- test/web/streamer/streamer_test.exs | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index a1b445f2f..5392c1ec3 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -138,7 +138,8 @@ defp should_send?(%User{} = user, %Activity{} = item) do with parent <- Object.normalize(item) || item, true <- - Enum.all?([blocked_ap_ids, muted_ap_ids, reblog_muted_ap_ids], &(item.actor not in &1)), + Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), + true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), true <- MapSet.disjoint?(recipients, recipient_blocks), %{host: item_host} <- URI.parse(item.actor), diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 7166d6f0b..848158a44 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -455,6 +455,34 @@ test "it doesn't send muted reblogs" do Task.await(task) end + test "it does send non-reblog notification for mtued" do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + CommonAPI.hide_reblogs(user1, user2) + + task = + Task.async(fn -> + assert_receive {:text, _}, 1_000 + end) + + fake_socket = %StreamerSocket{ + transport_pid: task.pid, + user: user1 + } + + {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) + {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, user2) + + topics = %{ + "public" => [fake_socket] + } + + Worker.push_to_socket(topics, "public", favorite_activity) + + Task.await(task) + end + test "it doesn't send posts from muted threads" do user = insert(:user) user2 = insert(:user) From e816edbb2f8966282d9e7c787272ed66d33ee088 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 28 Jan 2020 15:42:05 +0000 Subject: [PATCH 13/52] Update streamer_test.exs --- test/web/streamer/streamer_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 848158a44..f33b95142 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -455,7 +455,7 @@ test "it doesn't send muted reblogs" do Task.await(task) end - test "it does send non-reblog notification for mtued" do + test "it does send non-reblog notification for reblog-muted actors" do user1 = insert(:user) user2 = insert(:user) user3 = insert(:user) From a802e07241e441189f85568ee9ca58508ab6d0d3 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Jan 2020 11:39:06 +0100 Subject: [PATCH 14/52] Emoji Reactions: Add `reacted` field to emoji reactions --- .../web/mastodon_api/views/status_view.ex | 6 +++++- .../controllers/pleroma_api_controller.ex | 7 ++++--- .../mastodon_api/views/status_view_test.exs | 11 ++++++++-- .../pleroma_api_controller_test.exs | 21 +++++++++++++++++-- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index e60ef709b..5df29d93f 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -256,7 +256,11 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} emoji_reactions = with %{data: %{"reactions" => emoji_reactions}} <- object do Enum.map(emoji_reactions, fn [emoji, users] -> - %{emoji: emoji, count: length(users)} + %{ + emoji: emoji, + count: length(users), + reacted: !!(opts[:for] && opts[:for].ap_id in users) + } end) else _ -> [] diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index cd1c0764f..e6e2385b2 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -47,13 +47,14 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) Object.normalize(activity) do reactions = emoji_reactions - |> Enum.map(fn [emoji, users] -> - users = Enum.map(users, &User.get_cached_by_ap_id/1) + |> Enum.map(fn [emoji, user_ap_ids] -> + users = Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1) %{ emoji: emoji, count: length(users), - accounts: AccountView.render("index.json", %{users: users, for: user, as: :user}) + accounts: AccountView.render("index.json", %{users: users, for: user, as: :user}), + reacted: !!(user && user.ap_id in user_ap_ids) } end) diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 25777b011..fc110417c 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -37,8 +37,15 @@ test "has an emoji reaction list" do status = StatusView.render("show.json", activity: activity) assert status[:pleroma][:emoji_reactions] == [ - %{emoji: "☕", count: 2}, - %{emoji: "🍵", count: 1} + %{emoji: "☕", count: 2, reacted: false}, + %{emoji: "🍵", count: 1, reacted: false} + ] + + status = StatusView.render("show.json", activity: activity, for: user) + + assert status[:pleroma][:emoji_reactions] == [ + %{emoji: "☕", count: 2, reacted: true}, + %{emoji: "🍵", count: 1, reacted: false} ] end diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index 3978c2ec5..bc676c99a 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -25,9 +25,14 @@ test "POST /api/v1/pleroma/statuses/:id/react_with_emoji", %{conn: conn} do |> assign(:user, other_user) |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) |> post("/api/v1/pleroma/statuses/#{activity.id}/react_with_emoji", %{"emoji" => "☕"}) + |> json_response(200) - assert %{"id" => id} = json_response(result, 200) + assert %{"id" => id} = result assert to_string(activity.id) == id + + assert result["pleroma"]["emoji_reactions"] == [ + %{"emoji" => "☕", "count" => 1, "reacted" => true} + ] end test "POST /api/v1/pleroma/statuses/:id/unreact_with_emoji", %{conn: conn} do @@ -71,8 +76,20 @@ test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do |> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by") |> json_response(200) - [%{"emoji" => "🎅", "count" => 1, "accounts" => [represented_user]}] = result + [%{"emoji" => "🎅", "count" => 1, "accounts" => [represented_user], "reacted" => false}] = + result + assert represented_user["id"] == other_user.id + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"])) + |> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by") + |> json_response(200) + + assert [%{"emoji" => "🎅", "count" => 1, "accounts" => [_represented_user], "reacted" => true}] = + result end test "/api/v1/pleroma/conversations/:id" do From d378550d2d68f2a41993c59cacd646cacc0995dc Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Jan 2020 11:40:57 +0100 Subject: [PATCH 15/52] Emoji Reactions: Document changes --- CHANGELOG.md | 1 + docs/API/differences_in_mastoapi_responses.md | 2 +- docs/API/pleroma_api.md | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f4d848a..66c33b5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Change emoji reaction reply format once more - Configuration: `feed.logo` option for tag feed. - Tag feed: `/tags/:tag.rss` - list public statuses by hashtag. +- Mastodon API: Add `reacted` property to `emoji_reactions`
### Fixed diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 030660b34..82d967e4d 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -29,7 +29,7 @@ Has these additional fields under the `pleroma` object: - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire - `thread_muted`: true if the thread the post belongs to is muted -- `emoji_reactions`: A list with emoji / reaction maps. The format is {emoji: "☕", count: 1}. Contains no information about the reacting users, for that use the `emoji_reactions_by` endpoint. +- `emoji_reactions`: A list with emoji / reaction maps. The format is `{emoji: "☕", count: 1, reacted: true}`. Contains no information about the reacting users, for that use the `emoji_reactions_by` endpoint. ## Attachments diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 9f5cafe5a..c7125c1cd 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -455,7 +455,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to * Example Response: ```json [ - {"emoji": "😀", "count": 2, "accounts": [{"id" => "xyz.."...}, {"id" => "zyx..."}]}, - {"emoji": "☕", "count": 1, "accounts": [{"id" => "abc..."}]} + {"emoji": "😀", "count": 2, "reacted": true, "accounts": [{"id" => "xyz.."...}, {"id" => "zyx..."}]}, + {"emoji": "☕", "count": 1, "reacted": false, "accounts": [{"id" => "abc..."}]} ] ``` From b3a877d6c9f7645c854527fc5bf05d9161c2480b Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Jan 2020 11:43:36 +0100 Subject: [PATCH 16/52] Emoji Reactions: Correctly handle deleted users --- .../web/pleroma_api/controllers/pleroma_api_controller.ex | 4 +++- .../pleroma_api/controllers/pleroma_api_controller_test.exs | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index e6e2385b2..d76e39795 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -48,7 +48,9 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) reactions = emoji_reactions |> Enum.map(fn [emoji, user_ap_ids] -> - users = Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1) + users = + Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1) + |> Enum.filter(& &1) %{ emoji: emoji, diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index bc676c99a..be5007de5 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -59,6 +59,7 @@ test "POST /api/v1/pleroma/statuses/:id/unreact_with_emoji", %{conn: conn} do test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do user = insert(:user) other_user = insert(:user) + doomed_user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"}) @@ -70,6 +71,9 @@ test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do assert result == [] {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") + + User.perform(:delete, doomed_user) result = conn From e2e9299b8e0eadb2b43c7f7e034539c0f721d699 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Jan 2020 11:50:49 +0100 Subject: [PATCH 17/52] Streamer Tests: code readability improvements --- test/web/streamer/streamer_test.exs | 75 +++++++++++++++-------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 848158a44..16e3b8383 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -65,6 +65,9 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl blocked = insert(:user) {:ok, _user_relationship} = User.block(user, blocked) + {:ok, activity} = CommonAPI.post(user, %{"status" => ":("}) + {:ok, notif, _} = CommonAPI.favorite(activity.id, blocked) + task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end) Streamer.add_socket( @@ -72,9 +75,6 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl %{transport_pid: task.pid, assigns: %{user: user}} ) - {:ok, activity} = CommonAPI.post(user, %{"status" => ":("}) - {:ok, notif, _} = CommonAPI.favorite(activity.id, blocked) - Streamer.stream("user:notification", notif) Task.await(task) end @@ -83,6 +83,11 @@ test "it doesn't send notify to the 'user:notification' stream when a thread is user: user } do user2 = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) + {:ok, activity} = CommonAPI.add_mute(user, activity) + {:ok, notif, _} = CommonAPI.favorite(activity.id, user2) + task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end) Streamer.add_socket( @@ -90,9 +95,6 @@ test "it doesn't send notify to the 'user:notification' stream when a thread is %{transport_pid: task.pid, assigns: %{user: user}} ) - {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) - {:ok, activity} = CommonAPI.add_mute(user, activity) - {:ok, notif, _} = CommonAPI.favorite(activity.id, user2) Streamer.stream("user:notification", notif) Task.await(task) end @@ -101,6 +103,11 @@ test "it doesn't send notify to the 'user:notification' stream' when a domain is user: user } do user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) + + {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") + {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) + {:ok, notif, _} = CommonAPI.favorite(activity.id, user2) + task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end) Streamer.add_socket( @@ -108,10 +115,6 @@ test "it doesn't send notify to the 'user:notification' stream' when a domain is %{transport_pid: task.pid, assigns: %{user: user}} ) - {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") - {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) - {:ok, notif, _} = CommonAPI.favorite(activity.id, user2) - Streamer.stream("user:notification", notif) Task.await(task) end @@ -267,6 +270,8 @@ test "it doesn't send messages involving blocked users" do blocked_user = insert(:user) {:ok, _user_relationship} = User.block(user, blocked_user) + {:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"}) + task = Task.async(fn -> refute_receive {:text, _}, 1_000 @@ -277,8 +282,6 @@ test "it doesn't send messages involving blocked users" do user: user } - {:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"}) - topics = %{ "public" => [fake_socket] } @@ -335,6 +338,12 @@ test "it doesn't send unwanted DMs to list" do {:ok, list} = List.create("Test", user_a) {:ok, list} = List.follow(list, user_b) + {:ok, activity} = + CommonAPI.post(user_b, %{ + "status" => "@#{user_c.nickname} Test", + "visibility" => "direct" + }) + task = Task.async(fn -> refute_receive {:text, _}, 1_000 @@ -345,12 +354,6 @@ test "it doesn't send unwanted DMs to list" do user: user_a } - {:ok, activity} = - CommonAPI.post(user_b, %{ - "status" => "@#{user_c.nickname} Test", - "visibility" => "direct" - }) - topics = %{ "list:#{list.id}" => [fake_socket] } @@ -367,6 +370,12 @@ test "it doesn't send unwanted private posts to list" do {:ok, list} = List.create("Test", user_a) {:ok, list} = List.follow(list, user_b) + {:ok, activity} = + CommonAPI.post(user_b, %{ + "status" => "Test", + "visibility" => "private" + }) + task = Task.async(fn -> refute_receive {:text, _}, 1_000 @@ -377,12 +386,6 @@ test "it doesn't send unwanted private posts to list" do user: user_a } - {:ok, activity} = - CommonAPI.post(user_b, %{ - "status" => "Test", - "visibility" => "private" - }) - topics = %{ "list:#{list.id}" => [fake_socket] } @@ -401,6 +404,12 @@ test "it sends wanted private posts to list" do {:ok, list} = List.create("Test", user_a) {:ok, list} = List.follow(list, user_b) + {:ok, activity} = + CommonAPI.post(user_b, %{ + "status" => "Test", + "visibility" => "private" + }) + task = Task.async(fn -> assert_receive {:text, _}, 1_000 @@ -411,12 +420,6 @@ test "it sends wanted private posts to list" do user: user_a } - {:ok, activity} = - CommonAPI.post(user_b, %{ - "status" => "Test", - "visibility" => "private" - }) - Streamer.add_socket( "list:#{list.id}", fake_socket @@ -433,6 +436,9 @@ test "it doesn't send muted reblogs" do user3 = insert(:user) CommonAPI.hide_reblogs(user1, user2) + {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) + {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2) + task = Task.async(fn -> refute_receive {:text, _}, 1_000 @@ -443,9 +449,6 @@ test "it doesn't send muted reblogs" do user: user1 } - {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) - {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2) - topics = %{ "public" => [fake_socket] } @@ -461,6 +464,9 @@ test "it does send non-reblog notification for mtued" do user3 = insert(:user) CommonAPI.hide_reblogs(user1, user2) + {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) + {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, user2) + task = Task.async(fn -> assert_receive {:text, _}, 1_000 @@ -471,9 +477,6 @@ test "it does send non-reblog notification for mtued" do user: user1 } - {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) - {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, user2) - topics = %{ "public" => [fake_socket] } From e7fee0d6fa7b2ba046e57ca9364be1b62bfc9661 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 29 Jan 2020 13:51:17 +0300 Subject: [PATCH 18/52] emoji api error on not writable dir --- .../pleroma_api/controllers/emoji_api_controller.ex | 12 ++++++++++-- .../controllers/emoji_api_controller_test.exs | 1 - 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 0bbf84fd3..42f62d97a 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -573,11 +573,14 @@ def update_file(conn, %{"action" => action}) do assumed to be emojis and stored in the new `pack.json` file. """ def import_from_fs(conn, _params) do - with {:ok, results} <- File.ls(emoji_dir_path()) do + emoji_path = emoji_dir_path() + + with {:ok, %{access: :read_write}} <- File.stat(emoji_path), + {:ok, results} <- File.ls(emoji_path) do imported_pack_names = results |> Enum.filter(fn file -> - dir_path = Path.join(emoji_dir_path(), file) + dir_path = Path.join(emoji_path, file) # Find the directories that do NOT have pack.json File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json")) end) @@ -585,6 +588,11 @@ def import_from_fs(conn, _params) do json(conn, imported_pack_names) else + {:ok, %{access: _}} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Error emoji pack directory must be writable and readable"}) + {:error, _} -> conn |> put_status(:internal_server_error) diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs index 8e76f2f3d..6f1ea78ec 100644 --- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do use Pleroma.Web.ConnCase import Tesla.Mock - import Pleroma.Factory @emoji_dir_path Path.join( From 2bd4d6289bdc01dec69756c5e1ebca551fe3a6e7 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 29 Jan 2020 18:43:23 +0400 Subject: [PATCH 19/52] Make the warning more scarier --- lib/pleroma/plugs/http_security_plug.ex | 38 ++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 8bc324f48..0ba412699 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -94,7 +94,43 @@ defp csp_string do def warn_if_disabled do unless Config.get([:http_security, :enabled]) do - Logger.warn("HTTP Security is disabled. Add this line to you config to enable it: + Logger.warn(" + .i;;;;i. + iYcviii;vXY: + .YXi .i1c. + .YC. . in7. + .vc. ...... ;1c. + i7, .. .;1; + i7, .. ... .Y1i + ,7v .6MMM@; .YX, + .7;. ..IMMMMMM1 :t7. + .;Y. ;$MMMMMM9. :tc. + vY. .. .nMMM@MMU. ;1v. + i7i ... .#MM@M@C. .....:71i + it: .... $MMM@9;.,i;;;i,;tti + :t7. ..... 0MMMWv.,iii:::,,;St. + .nC. ..... IMMMQ..,::::::,.,czX. + .ct: ....... .ZMMMI..,:::::::,,:76Y. + c2: ......,i..Y$M@t..:::::::,,..inZY + vov ......:ii..c$MBc..,,,,,,,,,,..iI9i + i9Y ......iii:..7@MA,..,,,,,,,,,....;AA: + iIS. ......:ii::..;@MI....,............;Ez. + .I9. ......:i::::...8M1..................C0z. + .z9; ......:i::::,.. .i:...................zWX. + vbv ......,i::::,,. ................. :AQY + c6Y. .,...,::::,,..:t0@@QY. ................ :8bi + :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ, + :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2. + .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn + 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi + 7C...::::::::::::,,,,.. .................... vSi. + ;1;...,,::::::,......... .................. Yz: + v97,......... .voC. + izAotX7777777777777777777777777777777777777777Y7n92: + .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov. + + +HTTP Security is disabled. Add this line to your config to enable it: config :pleroma, :http_security, enabled: true ") From e07e7888d7b15d79fad98037e9830a618b93ae9b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 29 Jan 2020 18:53:43 +0400 Subject: [PATCH 20/52] Fix credo warning --- lib/pleroma/plugs/http_security_plug.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 0ba412699..e4939efe5 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -129,7 +129,6 @@ def warn_if_disabled do izAotX7777777777777777777777777777777777777777Y7n92: .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov. - HTTP Security is disabled. Add this line to your config to enable it: config :pleroma, :http_security, enabled: true From f5f7a0f9c157040bc3fc359e7dd07a1182aab066 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Wed, 29 Jan 2020 18:53:25 +0300 Subject: [PATCH 21/52] Update confusing changelog message for attachments cleanup --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f4d848a..68ebb03a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - **Breaking:** Pleroma won't start if it detects unapplied migrations -- **Breaking:** attachments are removed along with statuses when there are no other references to it +- **Breaking:** attachments are removed along with statuses. Does not affect duplicate files and attachments without status. - **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default - **Breaking:** OAuth: defaulted `[:auth, :enforce_oauth_admin_scope_usage]` setting to `true` which demands `admin` OAuth scope to perform admin actions (in addition to `is_admin` flag on User); make sure to use bundled or newer versions of AdminFE & PleromaFE to access admin / moderator features. From 7499805abf989fe0b5cb10c9da112c5f7371c882 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 29 Jan 2020 23:58:15 +0300 Subject: [PATCH 22/52] config.exs: Re-enable rate limiter and enable remote ip --- CHANGELOG.md | 1 + config/config.exs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ebb03a7..f8c5b8308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Pleroma won't start if it detects unapplied migrations - **Breaking:** attachments are removed along with statuses. Does not affect duplicate files and attachments without status. - **Breaking:** Elixir >=1.8 is now required (was >= 1.7) +- **Breaking:** `Pleroma.Plugs.RemoteIp` and `:rate_limiter` enabled by default. Please ensure your reverse proxy forwards the real IP! - **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default - **Breaking:** OAuth: defaulted `[:auth, :enforce_oauth_admin_scope_usage]` setting to `true` which demands `admin` OAuth scope to perform admin actions (in addition to `is_admin` flag on User); make sure to use bundled or newer versions of AdminFE & PleromaFE to access admin / moderator features. - **Breaking:** Dynamic configuration has been rearchitected. The `:pleroma, :instance, dynamic_configuration` setting has been replaced with `config :pleroma, configurable_from_database`. Please backup your configuration to a file and run the migration task to ensure consistency with the new schema. diff --git a/config/config.exs b/config/config.exs index f4e307e18..c57ef1bf7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -596,11 +596,21 @@ config :http_signatures, adapter: Pleroma.Signature -config :pleroma, :rate_limit, authentication: {60_000, 15} +config :pleroma, :rate_limit, + authentication: {60_000, 15}, + search: [{1000, 10}, {1000, 30}], + app_account_creation: {1_800_000, 25}, + relations_actions: {10_000, 10}, + relation_id_action: {60_000, 2}, + statuses_actions: {10_000, 15}, + status_id_action: {60_000, 3}, + password_reset: {1_800_000, 5}, + account_confirmation_resend: {8_640_000, 5}, + ap_routes: {60_000, 15} config :pleroma, Pleroma.ActivityExpiration, enabled: true -config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false +config :pleroma, Pleroma.Plugs.RemoteIp, enabled: true config :pleroma, :static_fe, enabled: false From f5cb2af85a2a3798d18d887b5947a65979cb65b9 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 29 Jan 2020 23:59:00 +0300 Subject: [PATCH 23/52] cheatsheet: improve rate limiter docs and add new limiters --- docs/configuration/cheatsheet.md | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 30d673eba..f910122df 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -326,14 +326,31 @@ A keyword list of rate limiters where a key is a limiter name and value is the l It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. +For example: + +```elixir +config :pleroma, :rate_limit, + authentication: {60_000, 15}, + search: [{1000, 10}, {1000, 30}] +``` + +Means that: + +1. In 60 seconds, 50 authentication attempts can be performed from the same IP address. +2. In 1 second, 10 search requests can be performed from the same IP adress by unauthenticated users, while authenticated users can perform 30 search requests per second. + Supported rate limiters: -* `:search` for the search requests (account & status search etc.) -* `:app_account_creation` for registering user accounts from the same IP address -* `:relations_actions` for actions on relations with all users (follow, unfollow) -* `:relation_id_action` for actions on relation with a specific user (follow, unfollow) -* `:statuses_actions` for create / delete / fav / unfav / reblog / unreblog actions on any statuses -* `:status_id_action` for fav / unfav or reblog / unreblog actions on the same status by the same user +* `:search` - Account/Status search. +* `:app_account_creation` - Account registration from the API. +* `:relations_actions` - Following/Unfollowing in general. +* `:relation_id_action` - Following/Unfollowing for a specific user. +* `:statuses_actions` - Status actions such as: (un)repeating, (un)favouriting, creating, deleting. +* `:status_id_action` - (un)Repeating/(un)Favouriting a particular status. +* `:authentication` - Authentication actions, i.e getting an OAuth token. +* `password_reset` - Requesting password reset emails. +* `:account_confirmation_resend` - Requesting resending account confirmation emails. +* `:ap_routes` - Requesting statuses via ActivityPub. ### :web_cache_ttl From 889965141a1411dd546757fbb964695bd8f712d7 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 29 Jan 2020 23:59:27 +0300 Subject: [PATCH 24/52] RemoteIp: only trust X-Forwarded-For Our nginx config will happily pass `Forwarded`/`X-Real-IP` from the client. Caddy, Apache and Varnish pass `X-Forwarded-For` as well anyway. --- docs/configuration/cheatsheet.md | 7 +++---- lib/pleroma/plugs/remote_ip.ex | 3 --- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index f910122df..54cc9b1c1 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -308,16 +308,15 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start Available options: * `enabled` - Enable/disable the plug. Defaults to `false`. -* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`. +* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `["x-forwarded-for"]`. * `proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`. * `reserved` - Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network). ### :rate_limit -This is an advanced feature and disabled by default. - -If your instance is behind a reverse proxy you must enable and configure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip). +!!! note + If your instance is behind a reverse proxy ensure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip) is enabled (it is enabled by default). A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index fdedc27ee..1cd5af48a 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -10,10 +10,7 @@ defmodule Pleroma.Plugs.RemoteIp do @behaviour Plug @headers ~w[ - forwarded x-forwarded-for - x-client-ip - x-real-ip ] # https://en.wikipedia.org/wiki/Localhost From ff4bef140af4cf17b2f8be2622a528cbea0ed330 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 30 Jan 2020 13:24:24 +0000 Subject: [PATCH 25/52] Apply suggestion to docs/configuration/cheatsheet.md --- docs/configuration/cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 54cc9b1c1..fcd7cdfb9 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -335,7 +335,7 @@ config :pleroma, :rate_limit, Means that: -1. In 60 seconds, 50 authentication attempts can be performed from the same IP address. +1. In 60 seconds, 15 authentication attempts can be performed from the same IP address. 2. In 1 second, 10 search requests can be performed from the same IP adress by unauthenticated users, while authenticated users can perform 30 search requests per second. Supported rate limiters: From 36becd55733fa3fdf046e24d7bd7fdd516fdd4fc Mon Sep 17 00:00:00 2001 From: feld Date: Thu, 30 Jan 2020 14:07:41 +0000 Subject: [PATCH 26/52] Update http_security_plug.ex --- lib/pleroma/plugs/http_security_plug.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index e4939efe5..b04273979 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -129,7 +129,8 @@ def warn_if_disabled do izAotX7777777777777777777777777777777777777777Y7n92: .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov. -HTTP Security is disabled. Add this line to your config to enable it: +HTTP Security is disabled. Please re-enable it to prevent users from attacking +your instance and your users via malicious posts: config :pleroma, :http_security, enabled: true ") From b3e9c8772445bfdfe4ec72378fb76c4cf76dc2da Mon Sep 17 00:00:00 2001 From: feld Date: Thu, 30 Jan 2020 14:09:41 +0000 Subject: [PATCH 27/52] Update emoji_api_controller.ex --- lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 42f62d97a..a2f6d2287 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -591,7 +591,7 @@ def import_from_fs(conn, _params) do {:ok, %{access: _}} -> conn |> put_status(:internal_server_error) - |> json(%{error: "Error emoji pack directory must be writable and readable"}) + |> json(%{error: "Error: emoji pack directory must be writable"}) {:error, _} -> conn From a0d9d42eaab397a1913038fea5c2d3630b812849 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 30 Jan 2020 16:07:37 +0100 Subject: [PATCH 28/52] Emoji Reactions: Actually use the validation. --- lib/pleroma/web/activity_pub/activity_pub.ex | 6 +++++- test/web/activity_pub/transmogrifier_test.exs | 19 +++++++++++++++++++ test/web/common_api/common_api_test.exs | 4 +++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1ac67b618..5c436941a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -325,12 +325,14 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do def react_with_emoji(user, object, emoji, options \\ []) do with local <- Keyword.get(options, :local, true), activity_id <- Keyword.get(options, :activity_id, nil), - Pleroma.Emoji.is_unicode_emoji?(emoji), + true <- Pleroma.Emoji.is_unicode_emoji?(emoji), reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id), {:ok, activity} <- insert(reaction_data, local), {:ok, object} <- add_emoji_reaction_to_object(activity, object), :ok <- maybe_federate(activity) do {:ok, activity, object} + else + e -> {:error, e} end end @@ -345,6 +347,8 @@ def unreact_with_emoji(user, reaction_id, options \\ []) do {:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object), :ok <- maybe_federate(activity) do {:ok, activity, object} + else + e -> {:error, e} end end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 5da358c43..0829a6ec2 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -395,6 +395,25 @@ test "it works for incoming emoji reactions" do assert data["content"] == "👌" end + test "it reject invalid emoji reactions" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) + + data = + File.read!("test/fixtures/emoji-reaction-too-long.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + assert :error = Transmogrifier.handle_incoming(data) + + data = + File.read!("test/fixtures/emoji-reaction-no-emoji.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + assert :error = Transmogrifier.handle_incoming(data) + end + test "it works for incoming emoji reaction undos" do user = insert(:user) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index f8963e42e..8fa0c6faa 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -238,7 +238,9 @@ test "reacting to a status with an emoji" do assert reaction.data["actor"] == user.ap_id assert reaction.data["content"] == "👍" - # TODO: test error case. + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) + + {:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".") end test "unreacting to a status with an emoji" do From 01fb4dbeae2b08e5ffa3d5dd8eedf6c741c7b63f Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 30 Jan 2020 16:26:45 +0100 Subject: [PATCH 29/52] Emoji Reactions: Add fixtures --- test/fixtures/emoji-reaction-no-emoji.json | 30 ++++++++++++++++++++++ test/fixtures/emoji-reaction-too-long.json | 30 ++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 test/fixtures/emoji-reaction-no-emoji.json create mode 100644 test/fixtures/emoji-reaction-too-long.json diff --git a/test/fixtures/emoji-reaction-no-emoji.json b/test/fixtures/emoji-reaction-no-emoji.json new file mode 100644 index 000000000..fff77b29b --- /dev/null +++ b/test/fixtures/emoji-reaction-no-emoji.json @@ -0,0 +1,30 @@ +{ + "type": "EmojiReaction", + "signature": { + "type": "RsaSignature2017", + "signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==", + "creator": "http://mastodon.example.org/users/admin#main-key", + "created": "2018-02-17T18:57:49Z" + }, + "object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454", + "content": "~", + "nickname": "lain", + "id": "http://mastodon.example.org/users/admin#reactions/2", + "actor": "http://mastodon.example.org/users/admin", + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "toot": "http://joinmastodon.org/ns#", + "sensitive": "as:sensitive", + "ostatus": "http://ostatus.org#", + "movedTo": "as:movedTo", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "atomUri": "ostatus:atomUri", + "Hashtag": "as:Hashtag", + "Emoji": "toot:Emoji" + } + ] +} diff --git a/test/fixtures/emoji-reaction-too-long.json b/test/fixtures/emoji-reaction-too-long.json new file mode 100644 index 000000000..31830d90c --- /dev/null +++ b/test/fixtures/emoji-reaction-too-long.json @@ -0,0 +1,30 @@ +{ + "type": "EmojiReaction", + "signature": { + "type": "RsaSignature2017", + "signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==", + "creator": "http://mastodon.example.org/users/admin#main-key", + "created": "2018-02-17T18:57:49Z" + }, + "object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454", + "content": "👌👌", + "nickname": "lain", + "id": "http://mastodon.example.org/users/admin#reactions/2", + "actor": "http://mastodon.example.org/users/admin", + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "toot": "http://joinmastodon.org/ns#", + "sensitive": "as:sensitive", + "ostatus": "http://ostatus.org#", + "movedTo": "as:movedTo", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "atomUri": "ostatus:atomUri", + "Hashtag": "as:Hashtag", + "Emoji": "toot:Emoji" + } + ] +} From 01537cc1d6a290f6a9002b331b18986036300634 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 30 Jan 2020 18:28:28 +0000 Subject: [PATCH 30/52] Apply suggestion to docs/configuration/cheatsheet.md --- docs/configuration/cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index fcd7cdfb9..a81bfa29d 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -347,7 +347,7 @@ Supported rate limiters: * `:statuses_actions` - Status actions such as: (un)repeating, (un)favouriting, creating, deleting. * `:status_id_action` - (un)Repeating/(un)Favouriting a particular status. * `:authentication` - Authentication actions, i.e getting an OAuth token. -* `password_reset` - Requesting password reset emails. +* `:password_reset` - Requesting password reset emails. * `:account_confirmation_resend` - Requesting resending account confirmation emails. * `:ap_routes` - Requesting statuses via ActivityPub. From 8057157ee3172c370200f328373f0a7e32092b91 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Fri, 31 Jan 2020 01:20:37 +0300 Subject: [PATCH 31/52] Make attachments cleanup optional --- CHANGELOG.md | 2 +- config/config.exs | 3 ++- config/description.exs | 9 +++++++++ lib/pleroma/object.ex | 13 ++++++++----- test/object_test.exs | 44 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0b3cecd..713ae4361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - **Breaking:** Pleroma won't start if it detects unapplied migrations -- **Breaking:** attachments are removed along with statuses. Does not affect duplicate files and attachments without status. - **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** `Pleroma.Plugs.RemoteIp` and `:rate_limiter` enabled by default. Please ensure your reverse proxy forwards the real IP! - **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default @@ -54,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - `:chat_limit` option to limit chat characters. +- `cleanup_attachments` option to remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances. - Refreshing poll results for remote polls - Authentication: Added rate limit for password-authorized actions / login existence checks - Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app. diff --git a/config/config.exs b/config/config.exs index c57ef1bf7..60c982557 100644 --- a/config/config.exs +++ b/config/config.exs @@ -271,7 +271,8 @@ account_field_name_length: 512, account_field_value_length: 2048, external_user_synchronization: true, - extended_nickname_format: true + extended_nickname_format: true, + cleanup_attachments: false config :pleroma, :feed, post_title: %{ diff --git a/config/description.exs b/config/description.exs index 5f3c58b08..1ffb66287 100644 --- a/config/description.exs +++ b/config/description.exs @@ -764,6 +764,15 @@ "Set to `true` to use extended local nicknames format (allows underscores/dashes)." <> " This will break federation with older software for theses nicknames." }, + %{ + key: :cleanup_attachments, + type: :boolean, + description: """ + "Set to `true` to remove associated attachments when status is removed. + This will not affect duplicates and attachments without status. + Enabling this will increase load to database when deleting statuses on larger instances. + """ + }, %{ key: :max_pinned_statuses, type: :integer, diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 38e372f6d..52556bf31 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -184,11 +184,14 @@ def delete(%Object{data: %{"id" => id}} = object) do with {:ok, _obj} = swap_object_with_tombstone(object), deleted_activity = Activity.delete_all_by_object_ap_id(id), {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), - {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path), - {:ok, _} <- - Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{ - "object" => object - }) do + {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do + with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do + {:ok, _} = + Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{ + "object" => object + }) + end + {:ok, object, deleted_activity} end end diff --git a/test/object_test.exs b/test/object_test.exs index c6b2bc399..5690bedec 100644 --- a/test/object_test.exs +++ b/test/object_test.exs @@ -76,8 +76,9 @@ test "ensures cache is cleared for the object" do describe "delete attachments" do clear_config([Pleroma.Upload]) - test "in subdirectories" do + test "Disabled via config" do Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], false) file = %Plug.Upload{ content_type: "image/jpg", @@ -103,6 +104,41 @@ test "in subdirectories" do ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + assert Object.get_by_id(note.id).data["deleted"] + refute Object.get_by_id(attachment.id) == nil + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + end + + test "in subdirectories" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") @@ -111,6 +147,7 @@ test "in subdirectories" do test "with dedupe enabled" do Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) Pleroma.Config.put([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe]) + Pleroma.Config.put([:instance, :cleanup_attachments], true) uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) @@ -139,6 +176,7 @@ test "with dedupe enabled" do ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil assert {:ok, files} = File.ls(uploads_dir) refute filename in files @@ -146,6 +184,7 @@ test "with dedupe enabled" do test "with objects that have legacy data.url attribute" do Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], true) file = %Plug.Upload{ content_type: "image/jpg", @@ -173,6 +212,7 @@ test "with objects that have legacy data.url attribute" do ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") @@ -181,6 +221,7 @@ test "with objects that have legacy data.url attribute" do test "With custom base_url" do Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) Pleroma.Config.put([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/") + Pleroma.Config.put([:instance, :cleanup_attachments], true) file = %Plug.Upload{ content_type: "image/jpg", @@ -206,6 +247,7 @@ test "With custom base_url" do ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") From 983a87175e6f83da1828630cbaad4b33b04c6d81 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 30 Jan 2020 19:47:57 +0300 Subject: [PATCH 32/52] mastodon API: do not sanitize html in non-html fields --- CHANGELOG.md | 1 + .../web/mastodon_api/views/account_view.ex | 4 ++-- .../web/mastodon_api/views/poll_view.ex | 3 +-- .../web/mastodon_api/views/status_view.ex | 19 ++----------------- .../update_credentials_test.exs | 4 ++-- .../mastodon_api/views/account_view_test.exs | 4 ++-- 6 files changed, 10 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ebb03a7..917e5d4ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. - **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes` +- Mastodon API: stopped sanitizing display names, field names and subject fields since they are supposed to be treated as plaintext - Admin API: Return `total` when querying for reports - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Admin API: Return link alongside with token on password reset diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a5420f480..c6d37ead7 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -67,7 +67,7 @@ def render("relationships.json", %{user: user, targets: targets}) do end defp do_render("show.json", %{user: user} = opts) do - display_name = HTML.strip_tags(user.name || user.nickname) + display_name = user.name || user.nickname image = User.avatar_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url() @@ -105,7 +105,7 @@ defp do_render("show.json", %{user: user} = opts) do |> User.fields() |> Enum.map(fn %{"name" => name, "value" => value} -> %{ - "name" => Pleroma.HTML.strip_tags(name), + "name" => name, "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly) } end) diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 753039da3..6bb3652fb 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.MastodonAPI.PollView do use Pleroma.Web, :view - alias Pleroma.HTML alias Pleroma.Web.CommonAPI.Utils def render("show.json", %{object: object, multiple: multiple, options: options} = params) do @@ -57,7 +56,7 @@ defp options_and_votes_count(options) do current_count = option["replies"]["totalItems"] || 0 {%{ - title: HTML.strip_tags(name), + title: name, votes_count: current_count }, current_count + count} end) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index e60ef709b..721e9f566 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -216,21 +216,6 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} summary = object.data["summary"] || "" - summary_html = - summary - |> HTML.get_cached_scrubbed_html_for_activity( - User.html_filter_policy(opts[:for]), - activity, - "mastoapi:summary" - ) - - summary_plaintext = - summary - |> HTML.get_cached_stripped_html_for_activity( - activity, - "mastoapi:summary" - ) - card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)) url = @@ -282,7 +267,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} muted: thread_muted? || User.mutes?(opts[:for], user), pinned: pinned?(activity, user), sensitive: sensitive, - spoiler_text: summary_html, + spoiler_text: summary, visibility: get_visibility(object), media_attachments: attachments, poll: render(PollView, "show.json", object: object, for: opts[:for]), @@ -299,7 +284,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} conversation_id: get_context_id(activity), in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, content: %{"text/plain" => content_plaintext}, - spoiler_text: %{"text/plain" => summary_plaintext}, + spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, direct_conversation_id: direct_conversation_id, thread_muted: thread_muted?, diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index 09bdc46e0..82d9e7d2f 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -269,7 +269,7 @@ test "update fields", %{conn: conn} do |> json_response(200) assert account_data["fields"] == [ - %{"name" => "foo", "value" => "bar"}, + %{"name" => "foo", "value" => "bar"}, %{"name" => "link", "value" => ~S(cofe.io)} ] @@ -297,7 +297,7 @@ test "update fields", %{conn: conn} do |> json_response(200) assert account["fields"] == [ - %{"name" => "foo", "value" => "bar"}, + %{"name" => "foo", "value" => "bar"}, %{"name" => "link", "value" => ~S(cofe.io)} ] diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 2107bb85c..00c294845 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -368,10 +368,10 @@ test "returns the settings store if the requesting user is the represented user assert result.pleroma[:settings_store] == nil end - test "sanitizes display names" do + test "doesn't sanitize display names" do user = insert(:user, name: " username ") result = AccountView.render("show.json", %{user: user}) - refute result.display_name == " username " + assert result.display_name == " username " end test "never display nil user follow counts" do From 50f5a920219d6637582a1998fd33ec4552e02e9c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 2 Feb 2020 14:55:06 +0300 Subject: [PATCH 33/52] fix not being able to pin polls --- lib/pleroma/web/common_api/common_api.ex | 3 ++- test/web/common_api/common_api_test.exs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index c05a6c544..2a348dcf6 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -315,8 +315,9 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do with %Activity{ actor: ^user_ap_id, data: %{"type" => "Create"}, - object: %Object{data: %{"type" => "Note"}} + object: %Object{data: %{"type" => object_type}} } = activity <- get_by_id_or_ap_id(id_or_ap_id), + true <- object_type in ["Note", "Article", "Question"], true <- Visibility.is_public?(activity), {:ok, _user} <- User.add_pinnned_activity(user, activity) do {:ok, activity} diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 8fa0c6faa..214cbdd7c 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -324,6 +324,21 @@ test "pin status", %{user: user, activity: activity} do assert %User{pinned_activities: [^id]} = user end + test "pin poll", %{user: user} do + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "How is fediverse today?", + "poll" => %{"options" => ["Absolutely outstanding", "Not good"], "expires_in" => 20} + }) + + assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) + + id = activity.id + user = refresh_record(user) + + assert %User{pinned_activities: [^id]} = user + end + test "unlisted statuses can be pinned", %{user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!", "visibility" => "unlisted"}) assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) From 90b862e7ab2ad64aed63502bc52ae92e5c0a1791 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 4 Feb 2020 16:22:13 +0100 Subject: [PATCH 34/52] Add cheatsheet entry --- docs/configuration/cheatsheet.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index a81bfa29d..ed9049a8d 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -69,6 +69,7 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `account_field_name_length`: An account field name maximum length (default: `512`). * `account_field_value_length`: An account field value maximum length (default: `2048`). * `external_user_synchronization`: Enabling following/followers counters synchronization for external users. +* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances. ## Federation ### MRF policies From 8c71f7e11a377d92234c141ea50170485e773fdc Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 4 Feb 2020 20:35:32 +0400 Subject: [PATCH 35/52] Add support for cancellation of a follow request --- lib/pleroma/user.ex | 25 +++++++++++-------- lib/pleroma/web/activity_pub/utils.ex | 9 +++++++ test/web/activity_pub/activity_pub_test.exs | 17 +++++++++++++ test/web/common_api/common_api_test.exs | 24 ++++++++++++++++++ .../controllers/account_controller_test.exs | 10 ++++++++ 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 3c86cdb38..398c91cf3 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -647,20 +647,25 @@ def follow(%User{} = follower, %User{} = followed, state \\ "accept") do end end + def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do + {:error, "Not subscribed!"} + end + def unfollow(%User{} = follower, %User{} = followed) do - if following?(follower, followed) and follower.ap_id != followed.ap_id do - FollowingRelationship.unfollow(follower, followed) + case FollowingRelationship.get(follower, followed) do + %{state: state} when state in ["accept", "pending"] -> + FollowingRelationship.unfollow(follower, followed) + {:ok, followed} = update_follower_count(followed) - {:ok, followed} = update_follower_count(followed) + {:ok, follower} = + follower + |> update_following_count() + |> set_cache() - {:ok, follower} = - follower - |> update_following_count() - |> set_cache() + {:ok, follower, Utils.fetch_latest_follow(follower, followed)} - {:ok, follower, Utils.fetch_latest_follow(follower, followed)} - else - {:error, "Not subscribed!"} + nil -> + {:error, "Not subscribed!"} end end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 4f7fdaf38..5bca3868b 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -490,6 +490,15 @@ def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do |> Repo.one() end + def fetch_latest_undo(%User{ap_id: ap_id}) do + "Undo" + |> Activity.Queries.by_type() + |> where(actor: ^ap_id) + |> order_by([activity], fragment("? desc nulls last", activity.id)) + |> limit(1) + |> Repo.one() + end + def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index ff4604a52..c8f630266 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1174,6 +1174,23 @@ test "creates an undo activity for the last follow" do assert embedded_object["object"] == followed.ap_id assert embedded_object["id"] == follow_activity.data["id"] end + + test "creates an undo activity for a pending follow request" do + follower = insert(:user) + followed = insert(:user, %{locked: true}) + + {:ok, follow_activity} = ActivityPub.follow(follower, followed) + {:ok, activity} = ActivityPub.unfollow(follower, followed) + + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == follower.ap_id + + embedded_object = activity.data["object"] + assert is_map(embedded_object) + assert embedded_object["type"] == "Follow" + assert embedded_object["object"] == followed.ap_id + assert embedded_object["id"] == follow_activity.data["id"] + end end describe "blocking / unblocking" do diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 8fa0c6faa..2bbe6c923 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -536,6 +536,30 @@ test "also unsubscribes a user" do refute User.subscribed_to?(follower, followed) end + + test "cancels a pending follow" do + follower = insert(:user) + followed = insert(:user, locked: true) + + assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = + CommonAPI.follow(follower, followed) + + assert %{state: "pending"} = Pleroma.FollowingRelationship.get(follower, followed) + + assert {:ok, follower} = CommonAPI.unfollow(follower, followed) + + assert Pleroma.FollowingRelationship.get(follower, followed) == nil + + assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = + Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) + + assert %{ + data: %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "state" => "cancelled"} + } + } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) + end end describe "accept_follow_request/2" do diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index ec1e18002..e2abcd7c5 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -457,6 +457,16 @@ test "following / unfollowing a user", %{conn: conn} do assert id == to_string(other_user.id) end + test "cancelling follow request", %{conn: conn} do + %{id: other_user_id} = insert(:user, %{locked: true}) + + assert %{"id" => ^other_user_id, "following" => false, "requested" => true} = + conn |> post("/api/v1/accounts/#{other_user_id}/follow") |> json_response(:ok) + + assert %{"id" => ^other_user_id, "following" => false, "requested" => false} = + conn |> post("/api/v1/accounts/#{other_user_id}/unfollow") |> json_response(:ok) + end + test "following without reblogs" do %{conn: conn} = oauth_access(["follow", "read:statuses"]) followed = insert(:user) From 3909b5b7b34bbf879fd7809eef6ec0ae286c2ef2 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 5 Feb 2020 21:13:56 +0400 Subject: [PATCH 36/52] Remove AutoLinker `scheme` option from the config --- config/config.exs | 1 - config/description.exs | 5 ----- lib/pleroma/formatter.ex | 3 ++- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/config/config.exs b/config/config.exs index c57ef1bf7..d01dd8d74 100644 --- a/config/config.exs +++ b/config/config.exs @@ -508,7 +508,6 @@ config :auto_linker, opts: [ - scheme: true, extra: true, # TODO: Set to :no_scheme when it works properly validate_tld: true, diff --git a/config/description.exs b/config/description.exs index 5f3c58b08..a0cb03e48 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2171,11 +2171,6 @@ type: :boolean, description: "Set to `false` to remove target='_blank' attribute" }, - %{ - key: :scheme, - type: :boolean, - description: "Set to `true` to link urls with schema http://google.com" - }, %{ key: :truncate, type: [:integer, false], diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 19b9af46c..90895374d 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -13,7 +13,8 @@ defmodule Pleroma.Formatter do @auto_linker_config hashtag: true, hashtag_handler: &Pleroma.Formatter.hashtag_handler/4, mention: true, - mention_handler: &Pleroma.Formatter.mention_handler/4 + mention_handler: &Pleroma.Formatter.mention_handler/4, + scheme: true def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do case User.get_cached_by_nickname(nickname) do From 5db6ac8ee405d89943a3669da4ea154ce004860f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 5 Feb 2020 20:36:21 +0300 Subject: [PATCH 37/52] removing migrate_from_db endpoint from admin api --- CHANGELOG.md | 1 + docs/API/admin_api.md | 15 ------- .../web/admin_api/admin_api_controller.ex | 15 +------ lib/pleroma/web/router.ex | 1 - .../admin_api/admin_api_controller_test.exs | 44 ------------------- 5 files changed, 2 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 713ae4361..b146ace46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking**: OStatus protocol support - **Breaking**: MDII uploader - **Breaking**: Using third party engines for user recommendation +- **Breaking**: AdminAPI: migrate_from_db endpoint. ### Changed - **Breaking:** Pleroma won't start if it detects unapplied migrations diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 2c0c5f46b..e445583cb 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -678,21 +678,6 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret {} ``` -## `GET /api/pleroma/admin/config/migrate_from_db` - -### Run mix task pleroma.config migrate_from_db - -Copies all settings from database to `config/{env}.exported_from_db.secret.exs` with deletion from the table. Where `{env}` is the environment in which `pleroma` is running. - -- Params: none -- Response: - - On failure: - - 400 Bad Request `"To use this endpoint you need to enable configuration from database."` - -```json -{} -``` - ## `GET /api/pleroma/admin/config` ### Get list of merged default settings with saved in database. diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 6f0449418..293f1befc 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -97,7 +97,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["read"], admin: true} - when action in [:config_show, :migrate_from_db, :list_log] + when action in [:config_show, :list_log] ) plug( @@ -793,19 +793,6 @@ def config_descriptions(conn, _params) do |> Plug.Conn.send_resp(200, @descriptions_json) end - def migrate_from_db(conn, _params) do - with :ok <- configurable_from_database(conn) do - Mix.Tasks.Pleroma.Config.run([ - "migrate_from_db", - "--env", - to_string(Pleroma.Config.get(:env)), - "-d" - ]) - - json(conn, %{}) - end - end - def config_show(conn, %{"only_db" => true}) do with :ok <- configurable_from_database(conn) do configs = Pleroma.Repo.all(ConfigDB) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 078bf138c..e86bc3cc3 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -196,7 +196,6 @@ defmodule Pleroma.Web.Router do get("/config", AdminAPIController, :config_show) post("/config", AdminAPIController, :config_update) get("/config/descriptions", AdminAPIController, :config_descriptions) - get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) get("/restart", AdminAPIController, :restart) get("/moderation_log", AdminAPIController, :list_log) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 81e346fb8..87f1366a4 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -2984,50 +2984,6 @@ test "proxy tuple ip", %{conn: conn} do end end - describe "config mix tasks run" do - setup do - Mix.shell(Mix.Shell.Quiet) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - clear_config(:configurable_from_database) do - Pleroma.Config.put(:configurable_from_database, true) - end - - clear_config([:feed, :post_title]) do - Pleroma.Config.put([:feed, :post_title], %{max_length: 100, omission: "…"}) - end - - test "transfer settings to DB and to file", %{conn: conn} do - assert Repo.all(Pleroma.ConfigDB) == [] - Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") - assert Repo.aggregate(Pleroma.ConfigDB, :count, :id) > 0 - - conn = get(conn, "/api/pleroma/admin/config/migrate_from_db") - - assert json_response(conn, 200) == %{} - assert Repo.all(Pleroma.ConfigDB) == [] - end - - test "returns error if configuration from database is off", %{conn: conn} do - initial = Pleroma.Config.get(:configurable_from_database) - on_exit(fn -> Pleroma.Config.put(:configurable_from_database, initial) end) - Pleroma.Config.put(:configurable_from_database, false) - - conn = get(conn, "/api/pleroma/admin/config/migrate_from_db") - - assert json_response(conn, 400) == - "To use this endpoint you need to enable configuration from database." - - assert Repo.all(Pleroma.ConfigDB) == [] - end - end - describe "GET /api/pleroma/admin/restart" do clear_config(:configurable_from_database) do Pleroma.Config.put(:configurable_from_database, true) From 2a3e06eb709966204c1723258b82644a7ac8d11e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 5 Feb 2020 18:28:05 +0000 Subject: [PATCH 38/52] Apply suggestion to CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b146ace46..1b6ba53d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking**: OStatus protocol support - **Breaking**: MDII uploader - **Breaking**: Using third party engines for user recommendation -- **Breaking**: AdminAPI: migrate_from_db endpoint. +
+ API Changes +- **Breaking**: AdminAPI: migrate_from_db endpoint +
### Changed - **Breaking:** Pleroma won't start if it detects unapplied migrations From c85aa6e87f8887ad532580ec3f84811abace05f2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 5 Feb 2020 17:06:01 +0300 Subject: [PATCH 39/52] removing confusing error --- docs/API/admin_api.md | 1 - .../web/admin_api/admin_api_controller.ex | 72 ++++++++----------- .../admin_api/admin_api_controller_test.exs | 7 -- 3 files changed, 29 insertions(+), 51 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index e445583cb..fb6dfcb08 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -689,7 +689,6 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Response: - On failure: - 400 Bad Request `"To use this endpoint you need to enable configuration from database."` - - 400 Bad Request `"To use configuration from database migrate your settings to database."` ```json { diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 293f1befc..c95cd182d 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -797,16 +797,9 @@ def config_show(conn, %{"only_db" => true}) do with :ok <- configurable_from_database(conn) do configs = Pleroma.Repo.all(ConfigDB) - if configs == [] do - errors( - conn, - {:error, "To use configuration from database migrate your settings to database."} - ) - else - conn - |> put_view(ConfigView) - |> render("index.json", %{configs: configs}) - end + conn + |> put_view(ConfigView) + |> render("index.json", %{configs: configs}) end end @@ -814,45 +807,38 @@ def config_show(conn, _params) do with :ok <- configurable_from_database(conn) do configs = ConfigDB.get_all_as_keyword() - if configs == [] do - errors( - conn, - {:error, "To use configuration from database migrate your settings to database."} - ) - else - merged = - Pleroma.Config.Holder.config() - |> ConfigDB.merge(configs) - |> Enum.map(fn {group, values} -> - Enum.map(values, fn {key, value} -> - db = - if configs[group][key] do - ConfigDB.get_db_keys(configs[group][key], key) - end + merged = + Pleroma.Config.Holder.config() + |> ConfigDB.merge(configs) + |> Enum.map(fn {group, values} -> + Enum.map(values, fn {key, value} -> + db = + if configs[group][key] do + ConfigDB.get_db_keys(configs[group][key], key) + end - db_value = configs[group][key] + db_value = configs[group][key] - merged_value = - if !is_nil(db_value) and Keyword.keyword?(db_value) and - ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do - ConfigDB.merge_group(group, key, value, db_value) - else - value - end + merged_value = + if !is_nil(db_value) and Keyword.keyword?(db_value) and + ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do + ConfigDB.merge_group(group, key, value, db_value) + else + value + end - setting = %{ - group: ConfigDB.convert(group), - key: ConfigDB.convert(key), - value: ConfigDB.convert(merged_value) - } + setting = %{ + group: ConfigDB.convert(group), + key: ConfigDB.convert(key), + value: ConfigDB.convert(merged_value) + } - if db, do: Map.put(setting, :db, db), else: setting - end) + if db, do: Map.put(setting, :db, db), else: setting end) - |> List.flatten() + end) + |> List.flatten() - json(conn, %{configs: merged}) - end + json(conn, %{configs: merged}) end end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 87f1366a4..5fbdf96f6 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1899,13 +1899,6 @@ test "when configuration from database is off", %{conn: conn} do "To use this endpoint you need to enable configuration from database." end - test "without any settings in db", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/config") - - assert json_response(conn, 400) == - "To use configuration from database migrate your settings to database." - end - test "with settings only in db", %{conn: conn} do config1 = insert(:config) config2 = insert(:config) From bdf0a87be95f311b094273d67244914173ee9dac Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 5 Feb 2020 19:05:20 +0300 Subject: [PATCH 40/52] dropdown type --- config/description.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/description.exs b/config/description.exs index a4d1a74a7..0ac1f4d8d 100644 --- a/config/description.exs +++ b/config/description.exs @@ -871,7 +871,7 @@ }, %{ key: :limit_to_local_content, - type: [:atom, false], + type: {:dropdown, :atom}, description: "Limit unauthenticated users to search for local statutes and users only. Default: `:unauthenticated`.", suggestions: [ @@ -942,7 +942,7 @@ children: [ %{ key: :level, - type: :atom, + type: {:dropdown, :atom}, description: "Log level", suggestions: [:debug, :info, :warn, :error] }, @@ -974,7 +974,7 @@ children: [ %{ key: :level, - type: :atom, + type: {:dropdown, :atom}, description: "Log level", suggestions: [:debug, :info, :warn, :error] }, @@ -998,7 +998,7 @@ children: [ %{ key: :level, - type: :atom, + type: {:dropdown, :atom}, description: "Log level", suggestions: [:debug, :info, :warn, :error] }, @@ -1969,7 +1969,7 @@ }, %{ key: :verbose, - type: [:atom, false], + type: {:dropdown, :atom}, description: "Logs verbose mode", suggestions: [false, :error, :warn, :info, :debug] }, From 7a2e153a8dec4337f16aeacab534f26f720cb5eb Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 6 Feb 2020 13:15:23 +0300 Subject: [PATCH 41/52] description change --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index 0ac1f4d8d..6b912a07e 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2178,7 +2178,7 @@ %{ key: :new_window, type: :boolean, - description: "Set to `false` to remove target='_blank' attribute" + description: "Link urls will open in new window/tab" }, %{ key: :truncate, From 6722dade42d5f404c00386b0336d821028d58d7c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 6 Feb 2020 15:00:33 +0300 Subject: [PATCH 42/52] Simplify in-database config docs Large part of it was no longer true (i.e none of the changes need recompilation anymore and you can't brick an instance by changing them, it's not necessary to manually truncate the db manually anymore) --- docs/admin/config.md | 79 ------------------------- docs/administration/CLI_tasks/config.md | 18 +++++- docs/configuration/cheatsheet.md | 3 +- 3 files changed, 17 insertions(+), 83 deletions(-) delete mode 100644 docs/admin/config.md diff --git a/docs/admin/config.md b/docs/admin/config.md deleted file mode 100644 index 35e43b6a9..000000000 --- a/docs/admin/config.md +++ /dev/null @@ -1,79 +0,0 @@ -# Configuring instance -You can configure your instance from admin interface. You need account with admin rights and little change in config file, which will allow settings configuration from database. - -```elixir -config :pleroma, configurable_from_database: true -``` - -## How it works -Settings are stored in database and are applied in `runtime` after each change. Most of the settings take effect immediately, except some, which need instance reboot. These settings are needed in `compile time`, that's why settings are duplicated to the file. - -File with duplicated settings is located in `config/{env}.exported_from_db.exs` if pleroma is runned from source. For prod env it will be `config/prod.exported_from_db.exs`. - -For releases: `/etc/pleroma/prod.exported_from_db.secret.exs` or `PLEROMA_CONFIG_PATH/prod.exported_from_db.exs`. - -## How to set it up -You need to migrate your existing settings to the database. This task will migrate only added by user settings. -For example you add settings to `prod.secret.exs` file, only these settings will be migrated to database. For release it will be `/etc/pleroma/config.exs` or `PLEROMA_CONFIG_PATH`. -You can do this with mix task (all config files will remain untouched): - -```sh tab="OTP" - ./bin/pleroma_ctl config migrate_to_db -``` - -```sh tab="From Source" -mix pleroma.config migrate_to_db -``` - -Now you can change settings in admin interface. After each save, settings from database are duplicated to the `config/{env}.exported_from_db.exs` file. - -**ATTENTION** - -**Be careful while changing the settings. Every inaccurate configuration change can break the federation or the instance load.** - -*Compile time settings, which require instance reboot and can break instance loading:* -- all settings inside these keys: - - `:hackney_pools` - - `:chat` -- partially settings inside these keys: - - `:seconds_valid` in `Pleroma.Captcha` - - `:proxy_remote` in `Pleroma.Upload` - - `:upload_limit` in `:instance` - -## How to dump settings from database to file - -*Adding `-d` flag will delete migrated settings from database table.* - -```sh tab="OTP" - ./bin/pleroma_ctl config migrate_from_db [-d] -``` - -```sh tab="From Source" -mix pleroma.config migrate_from_db [-d] -``` - - -## How to completely remove it - -1. Truncate or delete all values from `config` table -```sql -TRUNCATE TABLE config; -``` -2. Delete `config/{env}.exported_from_db.exs`. - -For `prod` env: -```bash -cd /opt/pleroma -cp config/prod.exported_from_db.exs config/exported_from_db.back -rm -rf config/prod.exported_from_db.exs -``` -*If you don't want to backup settings, you can skip step with `cp` command.* - -3. Set configurable_from_database to `false`. -```elixir -config :pleroma, configurable_from_database: false -``` -4. Restart pleroma instance -```bash -sudo service pleroma restart -``` diff --git a/docs/administration/CLI_tasks/config.md b/docs/administration/CLI_tasks/config.md index 2af51c247..873775962 100644 --- a/docs/administration/CLI_tasks/config.md +++ b/docs/administration/CLI_tasks/config.md @@ -1,12 +1,16 @@ # Transfering the config to/from the database -!!! danger - This is a Work In Progress, not usable just yet. - {! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Transfer config from file to DB. +!!! note + You need to add the following to your config before executing this command: + + ```elixir + config :pleroma, configurable_from_database: true + ``` + ```sh tab="OTP" ./bin/pleroma_ctl config migrate_to_db ``` @@ -18,7 +22,15 @@ mix pleroma.config migrate_to_db ## Transfer config from DB to `config/env.exported_from_db.secret.exs` +!!! note + In-Database configuration will still be applied after executing this command unless you set the following in your config: + + ```elixir + config :pleroma, configurable_from_database: false + ``` + To delete transfered settings from database optional flag `-d` can be used. is `prod` by default. + ```sh tab="OTP" ./bin/pleroma_ctl config migrate_from_db [--env=] [-d] ``` diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index ed9049a8d..f30aedc01 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -856,4 +856,5 @@ config :auto_linker, ## :configurable_from_database -Enable/disable configuration from database. + +Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information. From 8b9742ecf546c37695229d54f0a0b3ed4edd66e1 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 6 Feb 2020 16:47:15 +0400 Subject: [PATCH 43/52] Cancellation of a follow request for a remote user --- lib/pleroma/following_relationship.ex | 23 ++++++++++++++++++---- test/web/common_api/common_api_test.exs | 26 ++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 0b0219b82..cc381af53 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -30,9 +30,24 @@ def changeset(%__MODULE__{} = following_relationship, attrs) do end def get(%User{} = follower, %User{} = following) do - __MODULE__ - |> where(follower_id: ^follower.id, following_id: ^following.id) - |> Repo.one() + following_relationship = + __MODULE__ + |> where(follower_id: ^follower.id, following_id: ^following.id) + |> Repo.one() + + case {following_relationship, following.local} do + {nil, false} -> + case Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, following) do + %{data: %{"state" => state}} when state in ["pending", "accept"] -> + %{state: state} + + _ -> + nil + end + + {following_relationship, _} -> + following_relationship + end end def update(follower, following, "reject"), do: unfollow(follower, following) @@ -58,8 +73,8 @@ def follow(%User{} = follower, %User{} = following, state \\ "accept") do def unfollow(%User{} = follower, %User{} = following) do case get(follower, following) do - nil -> {:ok, nil} %__MODULE__{} = following_relationship -> Repo.delete(following_relationship) + _ -> {:ok, nil} end end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 2bbe6c923..7eff24ce4 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -537,7 +537,7 @@ test "also unsubscribes a user" do refute User.subscribed_to?(follower, followed) end - test "cancels a pending follow" do + test "cancels a pending follow for a local user" do follower = insert(:user) followed = insert(:user, locked: true) @@ -560,6 +560,30 @@ test "cancels a pending follow" do } } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) end + + test "cancels a pending follow for a remote user" do + follower = insert(:user) + followed = insert(:user, locked: true, local: false, ap_enabled: true) + + assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = + CommonAPI.follow(follower, followed) + + assert %{state: "pending"} = Pleroma.FollowingRelationship.get(follower, followed) + + assert {:ok, follower} = CommonAPI.unfollow(follower, followed) + + assert Pleroma.FollowingRelationship.get(follower, followed) == nil + + assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = + Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) + + assert %{ + data: %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "state" => "cancelled"} + } + } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) + end end describe "accept_follow_request/2" do From 2cc4fbab96376f0ca815f9be0cfad8e5ba1df7ce Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 6 Feb 2020 18:03:53 +0400 Subject: [PATCH 44/52] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0b3cecd..ecf84257b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - OTP releases: Not being able to configure OAuth expired token cleanup interval - OTP releases: Not being able to configure HTML sanitization policy - Favorites timeline now ordered by favorite date instead of post date +- Support for cancellation of a follow request
API Changes From fed9b0d1e09712b34e4a0374e47a50249aa7626f Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 6 Feb 2020 10:59:41 -0600 Subject: [PATCH 45/52] Update FE bundle --- priv/static/font/fontello.1575660578688.eot | Bin 0 -> 24628 bytes priv/static/font/fontello.1575660578688.svg | 126 +++++++++++++++ priv/static/font/fontello.1575660578688.ttf | Bin 0 -> 24460 bytes priv/static/font/fontello.1575660578688.woff | Bin 0 -> 14832 bytes priv/static/font/fontello.1575660578688.woff2 | Bin 0 -> 12664 bytes priv/static/font/fontello.1575662648966.eot | Bin 0 -> 24628 bytes priv/static/font/fontello.1575662648966.svg | 126 +++++++++++++++ priv/static/font/fontello.1575662648966.ttf | Bin 0 -> 24460 bytes priv/static/font/fontello.1575662648966.woff | Bin 0 -> 14832 bytes priv/static/font/fontello.1575662648966.woff2 | Bin 0 -> 12628 bytes ...2989700.css => fontello.1575660578688.css} | 44 ++++-- priv/static/fontello.1575662648966.css | 146 ++++++++++++++++++ priv/static/index.html | 2 +- .../static/font/fontello.1580232989700.woff2 | Bin 11280 -> 0 bytes ...2989700.eot => fontello.1581007281335.eot} | Bin 21916 -> 21916 bytes ...2989700.svg => fontello.1581007281335.svg} | 0 ...2989700.ttf => fontello.1581007281335.ttf} | Bin 21748 -> 21748 bytes ...89700.woff => fontello.1581007281335.woff} | Bin 13324 -> 13324 bytes .../static/font/fontello.1581007281335.woff2 | Bin 0 -> 11272 bytes ...2213354.css => fontello.1581007281335.css} | 12 +- ...ca107175d.js => 2.9be9f9ec29f7536c73c3.js} | 4 +- ...d.js.map => 2.9be9f9ec29f7536c73c3.js.map} | 2 +- .../static/js/app.0aac253187b2af873849.js | 2 + .../static/js/app.0aac253187b2af873849.js.map | 1 + .../static/js/app.9cfed8f3d06c299128ea.js | 2 - .../static/js/app.9cfed8f3d06c299128ea.js.map | 1 - .../vendors~app.9ab182239f3a2abee89f.js.map | 1 - ...js => vendors~app.c26cf2fc57e9c1975e8d.js} | 23 ++- .../vendors~app.c26cf2fc57e9c1975e8d.js.map | 1 + priv/static/sw-pleroma.js | 2 +- 30 files changed, 455 insertions(+), 40 deletions(-) create mode 100644 priv/static/font/fontello.1575660578688.eot create mode 100644 priv/static/font/fontello.1575660578688.svg create mode 100644 priv/static/font/fontello.1575660578688.ttf create mode 100644 priv/static/font/fontello.1575660578688.woff create mode 100644 priv/static/font/fontello.1575660578688.woff2 create mode 100644 priv/static/font/fontello.1575662648966.eot create mode 100644 priv/static/font/fontello.1575662648966.svg create mode 100644 priv/static/font/fontello.1575662648966.ttf create mode 100644 priv/static/font/fontello.1575662648966.woff create mode 100644 priv/static/font/fontello.1575662648966.woff2 rename priv/static/{static/fontello.1580232989700.css => fontello.1575660578688.css} (75%) create mode 100644 priv/static/fontello.1575662648966.css delete mode 100644 priv/static/static/font/fontello.1580232989700.woff2 rename priv/static/static/font/{fontello.1580232989700.eot => fontello.1581007281335.eot} (99%) rename priv/static/static/font/{fontello.1580232989700.svg => fontello.1581007281335.svg} (100%) rename priv/static/static/font/{fontello.1580232989700.ttf => fontello.1581007281335.ttf} (99%) rename priv/static/static/font/{fontello.1580232989700.woff => fontello.1581007281335.woff} (98%) create mode 100644 priv/static/static/font/fontello.1581007281335.woff2 rename priv/static/static/{fontello.1579102213354.css => fontello.1581007281335.css} (89%) rename priv/static/static/js/{2.59b096781ddca107175d.js => 2.9be9f9ec29f7536c73c3.js} (82%) rename priv/static/static/js/{2.59b096781ddca107175d.js.map => 2.9be9f9ec29f7536c73c3.js.map} (99%) create mode 100644 priv/static/static/js/app.0aac253187b2af873849.js create mode 100644 priv/static/static/js/app.0aac253187b2af873849.js.map delete mode 100644 priv/static/static/js/app.9cfed8f3d06c299128ea.js delete mode 100644 priv/static/static/js/app.9cfed8f3d06c299128ea.js.map delete mode 100644 priv/static/static/js/vendors~app.9ab182239f3a2abee89f.js.map rename priv/static/static/js/{vendors~app.9ab182239f3a2abee89f.js => vendors~app.c26cf2fc57e9c1975e8d.js} (97%) create mode 100644 priv/static/static/js/vendors~app.c26cf2fc57e9c1975e8d.js.map diff --git a/priv/static/font/fontello.1575660578688.eot b/priv/static/font/fontello.1575660578688.eot new file mode 100644 index 0000000000000000000000000000000000000000..31a66127f4af543f01bdfddb026350b4da88f620 GIT binary patch literal 24628 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 literal 0 HcmV?d00001 diff --git a/priv/static/font/fontello.1575660578688.svg b/priv/static/font/fontello.1575660578688.svg new file mode 100644 index 000000000..19fa56ba4 --- /dev/null +++ b/priv/static/font/fontello.1575660578688.svg @@ -0,0 +1,126 @@ + + + +Copyright (C) 2019 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/priv/static/font/fontello.1575660578688.ttf b/priv/static/font/fontello.1575660578688.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7e990495e49d0417a3d64fa9e02321b0d8e29a4c GIT binary patch literal 24460 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 O`rCQwqYxBuwf`U8^R8$B literal 0 HcmV?d00001 diff --git a/priv/static/font/fontello.1575660578688.woff b/priv/static/font/fontello.1575660578688.woff new file mode 100644 index 0000000000000000000000000000000000000000..239190cba188279cf239b56d11dd8efc50b065ed GIT binary patch literal 14832 zcmY+LV~}P&)UMmMZQHhOPTS_RJ#E{zZQC}cpSG=O>-775r%u&L)m=O5%38_Zsr*Ro zWVtI!NB{u?{S@w7AjJQk2GRd(|5yM2i=?W$2oMmk)(_?Vks47HxtF3UBg+p<{;@@W zq#?klx@Tf%#nQ~k6bOh7@Z<9{J`e|H z^~aax4+H{YNBXgeenbX20fu2|=i>3hn0{=RA6>2vf3Mp*nEdBg{A2&L0eL*zL#Egn zdHjsaf%jt*{U_ig_)>c#JF_3g_wya@KtLcf(>a91jt;+n`r&N;Vf{ZWq7@UMZEyvX(H$&T6*NoT+p=b4bs_lk`o!Zl+FgMMTvx-8`i7(;H{F=`e zW$)f*??ISAPK^1xg@s`=tJ@~_*f-08u4GZlr|npso9(CBGB!THj;4cQZj^3e*-7^x zh~`<-ILaE9V-7Ch5Tgy-IjjqCAb{vGPTCV8^0{E6UX7 zCapyRlZw*6iBI^luCA%c;B?gJS}Fqd#l?ow83T!nLF7kJvNC9?$@Fy8W?CxS^~Hxq z(%8d^i{a!)D6%qCsmbbe)Gb;nehtNBCej(BiHkL(5Jz~p;N-cDxXHeg`bw}=kix$C zc;M_o&UnknlWgVm$w~R!N(!Q%l=u2dX}5k#Rky-QeYZwROSdigvwIi9S3{J-{KK4q zI1vtzuBbcE*3^Dj%bGqo%ev-8`3S6=h|%9b>y$^iKMVC|O$mXbAdP2#2|@=0qauJl z|F9YjEB7S{G<;9#>|i=_SbA%t;bE(K2@2bR0BGSx6C%Crnw0z~G~Tn`yNI1#g9p>U zzfU>+e%;_QlEpWlIA$glxu49eOmCW@{7PJhy+}yTVjNjsrHHxYgFXwDpHlXr;`Jsr zi=5a2S4OHy{o95gCMHIr)KXh7hV)LLooVWGsw^?pTC6EQNzZ@ApmQMHqrT&Z!!y~y zTTpDy>SD>?NUn~hUMN+{ud-NbR{lmSP21f!IITIOG#IWxV%3d3Ixn@2YtcMu$=-TE z6=~ZDpi&{r+!IFgOkP1bIgEscn}nQ+zn?5aJ{tN6uU;?8)~J_lUHa%{^el7d7HlwZ zP;NaGA{>vs+`V)mHwf0zF#J-@X8jlBGs$O`wAE`Y)5SA-P!;O~qqvI@giwuVI}*zpK9sX|t9GO76ydI*9l> z+{zPomMUc#ayfscXNsud3zJF|lLIJK6F7qSMietq>oTkg9mc3TYmjyq4FxIzsr7m}_X zt|4SxW22oqu~LmuF~00BnyJxSy#yBNjE6ba8}0OIz{;AMj?%jmvD8G$!2kP77HSF4 zC(D&TbFE^;8Q96(Q=v>d_2z>f1dSUY#^3`$m~tcXr<+3L@cBoDrcr!Z`$%g#W2 z6w@IHLOfG&38<)+ejqU&I-F4ll&BVl5!4hp*5JJf)}S+XXnl9J*~+r2^ugZ@EeKl? zL*rP2j%o2DYnl! z9A3@-hI0qP*eC8gmDQ*aLGbUDRWXo(4MgVV+GZ!kIG&ASJo5l+XY15)#g7#!?oDYE`c(jb!x;1DGdiMq;Gh)Uz*&LXY zjxvYT-}PCr2zb1Q0MCaUFbbpdO87&Cy-@fUa3lKxPfFcFuOhmzYw^nms^SlwvVF0q za?9xcrVU)9x*L&62PZf(w$~CrI5qTw3M_W?c{UaMatMRbjZQm}QG|b_G;TwU)%!zZ z7|$t*cG$r_#o;SOMR}|Gg$k7bKXbISo19+o0U~y0fA1X^r})XPUlS~ncA%;96Dp!3frr_S&UbI+NmdG`PI zY%^*LF)zwaIhkx~I0P`-O`^1tpvF(%aK4lP2Ud0`FXT-ZETj=}1{S*NsyQo_& z(Z{MlMX%A^0{th`M)Rf!LG;H=>*ljK{RqFYE?L3`2!3Pv3}m|oFlG!9{j`R2!{stj z0(ng0K^QQH4XjNY-NOIA2KAZ3gJAxRMK*0zUw5|3G)=ERub1B{tey|5s;C7R+E-FF z8~=*M4E^j8p8gsjD*Y-ydy0QUWrTaQ2ugp85|;Z^U&5HxbW>n@1XGPiVYXAPOo_}Q%&ow%IzmuL{bVtEF!IK#X=Y2MprNxNWanJA9)8ozo@oxU_I@+Ce5A7#DCq6sR-vn!L zSY028ABwM(+Ljzs45|R89kF z@bTzkxjqzqQNIMflN9%j1{E#7Q@rzTCMbQ~d0p-{=H9lo7uFQf)BBnEDRUAg&j6!K7F>>`KWn8YW~GlJK#apWUWiw$Bt@ zEhzqtAads^!bP1OaS-2+cigV0sZgDWJb|aYt0v$#dsle10* zu`56IZ2aju^)JY4KNyEC7$O1FS3~Q%LXzZ>z~^9}%SAUvmws!*+?&6%l%^4Ebp>>? zm{eh&al(yJO%v!Ess#s{T_R1UckJ}Vx9@XksO@8Jj{bet{hVty{O%b*&Cn!iqgS#}itHq1P!GtbDf;Ao9uq56p-fd5DQO~%O?uET<%@eb@KP#rS(vM9Diy+X> zujFSS{s?PzIWhIjZuDKP9O~!0a+ezMC2ln*{gjevNAVSCNC$El_aKQ#8_=;#O1JL0 z)F@xGtkOK4!D`V|><#S8JE^TEm4NN6iYL()HFY&e8Ey88V7oFU;U^jriw&a?(g+o4 z_s4f>KQ+mGt#Gvltct^O1*i#ZXA%yEL`)T}Uo*#*PRX=NXZTpbhmv&}Awzz@4}kj) z2yvM{8R5UL9LsFWO}sV_gTR*nzwdT1Ve}e_J!hJsy*#`g>Z01@DLEf^t_|Ft3GKAp zeJi^Kjka4!T|H*r3S21({$x3B60RG*eIECxe~W_HSKRcJIeCN6<%; z_VKx3-AYEN}#gpDB$fs+2 z6saIY+r$R0x5^^Me}4@R|6PnRxph!xAQ01sbc5TNC+N6yc$Q`3^H@Wf$LD!ry?*P1 zMbLZ3=6_kDNS#;yZB0k$=QlLIC+T-^xIoP8b=qL}jPD@;ILtUH?omNHaZI->k0Hp; z(4tJo;^3-RQ4BNK4adm>b%#cUB`d^m9Ig*gO>`W)6@9PW?D_pry6wY)3{%+{6f{fG z6W~o01cHuf;7Sr3W0O^cy-QP6EFKX*b*#j)yRe0#MH;8xRAaXo!uf(2!ghrY zU<;9IVqGy~7U;gn)$^Il@y=IOb>Ns*iX(Za;IHhgsnOF6`SzQ+cNcS`@-r(UHLV%D zwF*;%hkG@j!JS|`x85wg>ZAa_mQ;9fm2evua805Q-Y&Q8AK0s?=qfF}E^zke11KwX z%Xpw1t`OK2E;*EQo~>GISB#j|8Ke8;;0x5Nwb|=<8+TY&JRK%h9@o>=-Pmf_Jr_~A zt_3h#<@OCbmp?1nZ&1-F_0#AcNBRU$prj?hh76Mp#3F^!^q-tcWQuH)#U`%@Cs|J& zvU00m3w0d507(a1SL0E2EW=6yBswzecIguJis=|TGeIGD?~41H}%hzfk| z2FtrKmV)eLZ_vr&a}nqagzrea)rM=!ZX>p6JObHCYbIxbO|wiOWWt$Q^GnE~zm!45 zN$!GG@`RGJnj- z8xw$JelKUN2#F#N4kYxDzD#Kv_#9m7hYNq~I~`KXCd3j)Ve!glM2#(>Q<)W;WT3<` z5bhOuON=OeUbUUf%?Mq>(aiOzDIX|WN!%{#yTjg>oes+-_FZS=vg%K{)#EC0`3C10 z=Na`4>6f?gs~mJb%}bu3h+sU!70&122_a93sS0Uob6GmcBij5Xp8q5=)td(A!+T9i z;>daUV@mXiWSB{FE#aNH(K;ppEpT6FW3|cDvJSeA%Gz>?De%1PAnPhiv*}r8&a7!}tS$0zs6rOME*X={A z11dB)Uw@bWz0sJ?&Xqu5neS5xW*wR|@EGs4h%ukvbEWl;j>^6Mn7pQ4>A#e-JVAJmCELOA>OJItl`8LssOEWQE|dcwmkq2UTgTwr zYrcSo+6iP0u_w4Ih0t|a{$+ir*0>L>+(e22+?~cks>#HwcCo#2WiMgDekrxR=ksDu9cgzj?I5@~JHLP2q6&d&d$_T(D zthZ0Vix7fLMXH(76dZRKFS?tZ(s1M05PVKvG%8H5;jFE)H$D&E%r8}yMsrI_NgRiW z5f*yCdEekaTf5oI@IKe@(YjrrUUJ@kTIp}aMS$yGNc&p(M?^4Mluo}6##5Yg;w6_@ zJM+vLZfd&3p~B~E-Sxuw6=2E1n>jVMs^@+wo1(MGy;Q}mH$|H@>K3aGk(MSi6&EXS zkJX2kBK{1cofPI33>aki;F>f{(i|DUTMCrz!4d$l0dG7%W4VPe{f6f0Ve=V80Nhv>WE}7=Jcg`$>^j= zWhrzWYBwKr#a`O4b9-`Y_k!^u;DZlQ5Iz@>+vEhWg6Q@4eYis0;~;7DjiCeSe)AWE zNQpVf2HpX;H5vOB6q%lGr&XpcIxCXV&P2x<@p&<{sdBl!&v6^WWxl%g78 zI?=BUE7NLQRZ^{TjY&p0A!~dxpg<#y-oYPI-nc%8?6q%#wg4jo+*myRq@3s>oJdl& zXh2*QD-_8y!)?k#{Ee2QQoF06M52am5IaqU$290PH<7x>9_BCCsEpZNli0zhOa}AOc<%wbYp6SSccdlvgFaLTly(<&szA zM7&G2$CnNB6b$NC=@h$Mp@yFfLmI$g){)2O%fJq!MH*2g%(_@$vQl9xNu*m-Y#A-K zD}oJ8QqU4XQyKU~`S}K9fg$1bcu@IDq%(oCMJt0f!hr))iypAZZA^>6=X=6=-F|?J z`E^?Ee4|M`%+B@^f(8|awCyaP;5ZS?WGWsI3Zk3{_C=LXWSJZi$S&L@bd820xd~%1 zs*YckFEbPhf!QdTXzL(lX?X#h=88b9hpejsCym>ahepi;Qt z9K>L}$78O?Qou2f!e|ZUTtsbSJ;=5~cacob6-|7Vt*^d;@KX0)1_a)Y-q7&_IO706 zjSl)shy^UQu9)S9NbVS6egba02%dCIiX+kBKj1a^ z_(1pG+crkP8NsiKq#t_3ihO%BKh$fx3aA#p;5&+U;hMk0VciO2&U0M}h zeLT{3VYREM^=LlN_#{E4Fm34RSIZvCF+S4#S@g1K7o{5htv0154_JZNCl4#`G*Mt8R4V zy6Y5I=MIvJgwt^fmghYJU}rNKOMaMIV&WbElj*%xW~ZDE=o-)oT|99-7|ia!dg2;h zg7e&4b~X!V5)?wBiVos}h!7-c5!I;kdA~xxLk0SLZ`UZ;zVo*h0ot6zMXCf+I_(P) zu4zyfBh=u^eoPD}r?4cKy~>)yL@EmBU?HT6%EMTZWf(^Pz1|!XlP<%zZ7UDH1-)-j z?=_-mGV?y)Z`S#w_Ypmt(z3m=hz+q%#7b2TjQs2N50hac|G4y(u}t+VP9e2R(H^r2DDEVgK*bUhLdhxri2AV({@S34oC3)-4)v@k~JV(4m*- zagW+%xGO3T?R)}T%Scf6zV@OI?mkR2tdS*D6JkR;~2*)9A0wvAq`@j}lpfYF5P^{BESu4YQY~#LA~>-;V0|SWk4~#kO1; zW*id*iAN1m!BXB~PK2^ut*1&WsTL~?lLsqD{0>irN4YbVWk!Ibay?vMwta3~X&(F}Bnn(VU7SWzO{_7-b8L915p#HV{fQbvmRsNk<%F582pO2}2x& zmAJ65>^(=UYxmM!O+I|WO{a&mB~D6?*0VQX#F*|SgS~7q^&KoRc zlPiWo-v?CNr;Jh#e`{tNeOrtM-Q!xxPEzUt){Sw*ughA*t=9ow`#hb}1r>%^UpE7e znTd{s-$6z8NMT>&VpiGI0`gd$P82$!wYU}|F?-Fk|0JWgxZ;#@WGpax z7Nimg5MPmlD;l1p%0UxW?l~yanY+Id@o&vR-HHv*tx3?%#&-!CLHAk%##n;KFxBv_ z%f8{R46VB2Rr>7LEZpqAClx#vOiTk6Hl2g>uL`8R%PJ>R=iCnScSvzdDZ}wY8=No< z;^$q8uo^zO9r_vG%k>T^|AuVW>6PDyppWkabB}Rwfin$#)b`7VYeEH;s|Lq>;y)`2d16;1CCf}TW~p;MDoN07NZj=T&F4eFZ7%N4{np# zp&$45oh|OTK~5C2Qix2VkYZO78sv{!|0k?FWaPDa=ruxaU=Zc2?DzdG*Y_&*bL|&R zt>MQatj}YKNmg+W=1xG{dDq_ALt55dK zoZgcBRX8DWd+nX4*h%7Si6{Da3h@x~)GmElPXCT4BMzKaz#AV>r@fuAh=-$pRl(@pfN@HVcUUV!-2g-a;nl1;nV-+vo0p3%O7M}QX;^29%XjHS zjNR17FK4E#TkpL^PO2}@H8j4DepbSxXa75WEy2Y;*Ol!f6^bSPRZ@;<&M8D06JK9O zo~vEx_qMg?nw?%>Ut33)`efI_oGZXx8!liX6+)8twL(I!<;$HF@zy)9N0R$4XWI`e zY1QsIjDLZOEh@|T)a@)Vs-(-%dnPh0{;!@eOYZc}DF4#Bct7ORQ_*D#ge0tgLXW5$~2oX~{mDrt9x4_eFiWZUd7LI#t&jW6I_m z2>(W86CNht>zo*M^^Fy;szUn=;+GJQZhD+sQe@vhqjA*k|3X2x_Rs_VBZ!E_{sISyoWYE`*__kjPvxN?4}W~HdvQ=&6BuG zcD;PDJiSpK4dY$wy*HXJ{hJJ`J|}VgM?;=(A9Yvtzs~JYlKQ@0DFvEsj74ceo4B4m z;IaKJxibU5uZsZ5%Xi;*CxEU!6T?{S{If=d2le(z^hc-%mICh)JW<;s((z)chUix0 zk>RrWcu5x+ntkXcm!N}VHdgZA`wsf=$DDref<7OZaR4vF@9hkg0#3q4J6^PZ2?N3p zt_eXG@ujp81S17X%5}4b9&2Es_bDIXk%fH@Un1*n-el}+P~coH#d6e_@J_NtvX>y< zL!%x+dW8!?ZwS2Zy!m%vsjV}|fL1{hRgbEPdPUTL3B*U?hmS~#TJ>tBG^EvZgW3>$ zarE3Q^dZ#9XqY$_YNQ9RWgAoZq~~(;qTB+i7A3S9r%EWT-#M2F?va*44^(SZFnE7! zuyTCh4jrePdyN~$LqxK@XvmPZ&RT7;YZv&(MgiiisJ z5oyqbG>=M0@q|(ss6`6lkkJ<&kj?YIn3K6P0M_t`fswXFh4b@DV?|08y+skFHf=pR zyn5~jx^HYt>iEd*{xj#!3#D*<)j2X5#Wla&yzMKt1mI+^p2ijBEpRxNO;Qsz=(&zd zq3~Fn+4Og3N;L{q{T{n0czin-!tcRwVq%f)1BatM)ut-{H*B0(I=#}bTn!o zcPop)KpK zg4+5FO``KBO;KFso7LkTvT>G6?<1v%W5)C5nBvnn$%JoXTV;Y!#SvkV?)aTKIMsBn^R#wIAn34#YJfzuQxSG13PMYk zXxy$->TRf*vd%0kS-4VOB7tNwERgKW6a$55k|2a&BHb!ChX)dh@uiJNE>TI!)C_lJ z@P}@wVspS=z%d+S-yC-MnUmAPXuZO7b;*>g>dORK6viS=x|a+j4&n zi}MYjnKL;G&R#%sP|A!L(X@lwyc>>P;P0Bd(p}vP3piAL`r4a*j5B3@0CdT( zl_V{+=E$ChcJ&CirjNM|Y)#p0H2f-u6oSs5(7!qfbO)`KTzburVAVR_m$AoHk!rUG zYAk*x_G&;Y)+JOG(tfdWO*`zHS&lAnlUDhruCMLhZd3niJcU9wzN{L3Lpi@;5Sn*6KI?HC6M%OV?p!YL%PL0=m*4 z6^<~}%fSw%@p`2Y2$PUZ;3o|77unXjFAX-d?CLzu8TDlUgdM9FG!Lg4W6&xVlo_+L1#cxmd;p_i!uBTB*D|Y1N%dLWs3?Tl(q@HL)UaL z8SF2{$eU?;={8c%L9h}Yv#45FUVt1p6{iGE2=42eeWl-nK+8YB(IWdsIj~x{S(Fp4vz8Ewfldce}XUOrQGbm5NdG*BWwOC zDJyMX7+>SwS!HWZ+&Yuv3Q)E|Qm(8!CbVa70jx1)GNKquXy;cS7sW_;R|j|{y*uq+ zoJoFZKT(!6rbK(UqazOUOCP131Xirf@6f8(zY(r+vOOJY@??iqEj%e$-b6=l-SF*j zV)N!qo(ZdkNhn<;BgA@2s{=ebf^Y5hAtxa$9?D54`g1HOjGdDVL3$xeZ^1^{So_o0IU0;yX3o`vO7{CB zw|y9JMT+ev7WCH|!yjbE<&k1lvpWI<`e)SDPdB4wIF@JvM|h$zgI&7x!g&W5-uUAJ zQl6tt<972->)<{dQw|+&k|DZiGdDPUMvB^oazD-qwmH`gzSWv$1=;5Mnuh8bzs5P- za1zrbcpSJMc7eb!C>+Rfl5j0orUDKc>2NC3cnUCPKOH+hJ{R0bxH-s!e@ zr;3L%?V?a-f=vzPs7_Vfxv;BK8%kj1G8s0IPePL=T}D~NTe42q`IAXnCW6is>M4OS zgvj1gOii}7fE~nCcVxjYTGmneh9F>9DGp(0dTd()_UKBhx5=Lf|B%u?0o51!^>(aZ z8Ub$F5_GGx*B0u_gf1?1+ED<5UT#L9+3vh~heji{CuXA(c?Bvq0jhDb#$_U~1@|g>`Mbz_x~-4h0To z3_0WOCnh<2@wH57OcagMEzIzV9m%EHf#%!J9ThwU>uN$Pjk;9Cnj0gq_B1E(wb?x` zlPWLprdql@MK~Y#a;#g)ac6JQwRtpVyGE7QA|aX=?p?=F1(5X&J?c)Ni77JkBOhD_ z%~yff?lR>jNP$IePN@8jnw@VU#nf3C_5xlV(WD~uXSLY-=nCeNq((d7EE}OCN87yj z@9F_%@019LC05LfL#ogxS_beEsd)whAcB5zAYW>qxdlV0pD`)9I0#CuF`I>}&$;9? zK5EPCDjAO|A8Gr<^O1o-A%Wb@BE*+UG$_ANiHSs!5w~B=;m}1omKnPY?U_wd7C}*w zmopCxp|s(#+W9XJcgJ)2S4`YQ>iWzxkyv?)KCrR@xP6YF7Gm>^aIiJ#)SK+>Z5&95 zG-chf&`vT|F}STa%LaC4_r|td!#vud>Np1RBbTEL%gV*4wc>KbGpV|9Ui{WZ|C`QMG z2s>`~2s$3Tn<4JTZc&OZt?w6tme%}IY_6M>VdjK5L$vG zl$;)$u2ValTA1zMl;)aIiv{m(^zeF|#xFmoC}(KA%kj<7 zY6_NEjilLuE-W^OvjxyH@oLUVm@GH?;)6WF>k$UzY;-onh7q-~<@opB3G%A(D$-f( z>kpO1^IYd`p)K-ABq+z5fco#nX{F6Doi%yvPanQUHuir2`M7&Y`vN<8k|$l*I*7d* z3Vr$2PSblAq)lnS5%1vku*b`R$db}hU4o6%Mq%E;`8&lD_X%zWWDHt}*1@vJ^3nEg zK}uzP^Kq9h>*}s7R&SK@iTZa!{#B zksSP6iRz{{HX~7tVqBK3BQS`pePAR;gRw6Xv1=>sY*?hK0Qaacy>0WJ=g2;fDi9|4 zw?s6n_98Z0%3&jULv`E%%BWzAc{u|fM_(KxQt;AdO43e#53oolQ~KuO5YxNi@*dRt zR<^ahn{OisSNu2d)l{>RmM`%lB!3OlE@ef9Hb`PImS>^Z1bnk7BA*g)vOaiE(;}{; zD9Ot`0I?2f1My2%g}2{WtSDyP#Hga!v|b4JpAxGzGKKKJ*}=R;B;=Uwt(M46 z+H^94j`tD@5edG+4UvkIR?>~yF=70IWXa|na{g}PrPA2_{?VQW8-e$J-MO5C)=e*o zsRpgQoZJ^VcOHVaCZA`&-4hObE*v;>;zwA**BDa{yE4{>#lP@vcX_f3CHtW?SG|;j z?6u+M)w>+jr+$LihEC0Gtqs^u2aFEeT1n~Be}rVP^h4YrSRMWmL9n|7T}b-m%Z4({ z=tzNr7Q`NxjzN3ovXu>FI0eDb`(pu52w(Q=zO*&h)qHAasYDg6Z!@hYZ_cz#8;)ue zdzDtvqGk8GtoH6W69ZycDAQ^*I!{TXtVfhZYeLI)iHf~D_ACd#pF7RK z?>c*#6N86^SSB1~+Kx~|z-(o!<$&mH6hVAUJ(fzMaMuol3nOyx^^OSk(Cf%G$?nn= z+h_1&IOp2PNF5}SCQh>winYH@z28U|z!Z522T;jK38mYHAPr&&@$va_cA~S3?IR@G z@-wTyHHzTXoy~N%9{pzvdbUPjpt=qkW@FQVnFj*1Jd$HV3gD@P& zc}ljjo#N1fC))EarHRAyXWnF_vXgx>3(6mF1kJ$IHk!}BO<{Gs2ERUhF~9F=p;r9p zuAamgzETa+O1yJh+M#}7I;_0O5wtM){pr(SlaBw9>;xS{6M*#Ut-r3;9A(+1C}Zb%lybOBM_yT74=cWom0?9sR} zy!#YFEMDuuLs>9AE@!f~QgZDEMT039|D0o{w*pkvx3t4^SBnl2GRx_0=EipOwE4@Y z8qu%!_xsz+uB92%#%v7P7gmZ-H2rzmO$Rr{fo*B)7;LJQa%fPz!-HqL6%rrI0!GqD zVph_*en2{fCzNquLySpF!pacUg`@jXg5}-p69*;QZANElJ2W})UxSro7&JNZoGZ{y zN9OHc!+&aEp9S}^Ml)+*-Thzl+dckpoHhQNU?Wt3nrk{Mb4{Xbzu7n%^p@kyzd3Z? z^AuwByjk*aSC8?tY6yMSs*?CPl(ytO=097C_3XWTJU4JeB>2pU`$DwEl@+w-2N+*q6lxjj&5OT$;QT zr(0{4mG%pYy6T*AV>v^U2z`z_533Paty?6YPIE-GCx1?dZ*?N$ec$^gAqVctepV&c zbk@s%4$wZ*S&v2Aw$ph%U&H(Tva!O15%&dR2_nJxlT!o)Bm*=Hi~?*6d=F9#3IRF} zCIc1#b`EX|(F$=1Nd&0@MFC|G=L}a5&j#-U-;Pj=$b?vlgo3n_}(1p7a400Kk@#0T{Ir(Og5lkfE3T)Dhum+Ce4z72^4yppjI+*sPO?}n6d?QAdx_%(74f)XvzaGf z!OBjx8Ri9b7!nQW(v(KBj#xgsj$jCov}*Su2&;)APognin&}+L8R+wNMF9V~{$fb< zPL*v(uS=c!mwN<5LDHf-(({5-zM>&1Y-wtzly+raMOvm)QC>J4Q_{szuvl?9)gcp2 zI5v5=>^x^8s5Mr*9ro_If5D~_Vaq+!gYz0ur*|;twz4EY&tcAR@{9gx-h4bo19pd1 z!E(BC)AEBK5tORBK15=;3dV>?xv1v1eNhAkPcbzs8S6te6twOz8^bC9*_@X8KwvCe zS>%G1l!Ev6MUUW>nR@ptUH#v}xe*`n)oeHm<3`%hHA0g_G1-(0EARUi|43+Aixs^4 zRp+v{CdyC{PU3Tej?TQtKLt11(AFr39sjQz$V-o4^`CjSQ?hW!B}Q7-(lnchSn|Gs zrfyZu5gZygk4@)g8i~BW7|PM_H6wJ;)-q4e+1R$ljN14N^veSV?EnLMfFWPLeo?+b6TSgezMKK+tD zgUGQ?fpaf`buXbeFMm`&ve5$(16`17ICRYg9NVx^i?&1U&Zum|MWl6%&3hR5oG3dx znCuGVZH&uHHsGeUa9{{PF!iibpca(<3dk}-RP8WJ{c5awswOSVKJ$rdGE_ygFjzA< z_URm2tf(Ax|LNsmSwT(0Wvnb<>V8EEn3Abeq3BZaw^>thFs$gg3M$piNUEz8#9SyHW z32M}T6&q-iqoQP+P(L-^@krIh?K@tzrg0Yy=<$g-r*A2@HV-8+ubUY+TK= z+W}h_@jW~XX_R24L{yGeo&Eoxlan!o)q$y1c1V!mm|HN0n=^u@iC)*F(F?O!u7$<9 z`>KM68>@ItSD{)w*|2pqJCoPa={YhF`uT+BAqt1Zs)OvDwhHt5{s=p-ve@*(FeRg@ z@$Z3D`!0fohGbi}M3Pj*Ra8oKbili`ik<3vecDnbyRu4YDj)VUAr;quc46q(j?maA zxdWtIH7$jfWD+2hgmg*`385q+DTII^Nf;sqy3j)p5Gk=Cij9b#%SHsTo}las*pTh$ zmWsQg&wJP#x_jzj{O@h`dd{WyUib9CenQrK7#XKlBW}^Etf|=+Q0z#h(3F}Zrqm7{ z0%r1e|)GB~mTX@}?G$(xj<2CRhcF5zBo2RGX2L%lw2tNOD@Ywj+fI zJ~VL`OjUth)@A-}-^?x#2ojb!2!ZG#Wfm`_bNaxkL6y zW-@)TPzjwB=#9b5sdmR)Iypm6Q2z>0QJAwdem^bXU4gYhk0XT&6PxI_FU;JWUq=93 zPJc{(;3IscZ%&{v+P+CL4a2}Pz$j=J=)#Q$CRrlIQHBphf2CTPsd}*FAKo}a6<3jb zyth&)Wm1%nA;`f1BmwYv2Y_eDvjF)9$9==)+vUEcoOd|z49MHBC-*$xy67UwXU?S` zNMp*}l6^zo)7n5zeP%`z_ zkqDj;ak$-o8ss@pSNriyI)4Lz0605oDQNaYS|&hRztmI<_(0qipo{%Exc)+U5kS1z zFb2XriGsJ~;q2r9UE~V@w*Tya|A-6(uXzc)exzHeO8|NQdY2j>lkBn_gUvOdz$sv( z(BZ70L*X3U2Zp?e%OwoBk)BTBFtJ4Mvl>hzZqnqhyvV)mpvL zv~0)q{2+{4al6y)^#{XbG@eXn^96(kl#O-t+XUah<^=%}A!0;=ND&z#M-+$>Q6XwX zgJ=;QqDKrMMi3K-8N>o&1+jtHK^!1XAkH8zAg&;8AnqU@Af6yzU=Z&d;*%qQZb}c@ zVnbJcLSJrTD0eZIdzi`s=JF9sI$=!`wxnWDFC4iPXDV=IBJM1~ldX7VJKi~mPu|0~ z5`|x7*C&73s^KYPN3V~U4Wf;|Ji68chyRdo4;lZDj%Tk7i!ecYbQF#Mvm2?0GEiC* zw#2?P1qg#RL|`*c3^`x~zB;3YkL93O_S<25@yTh?D!l-2&i0qgD{z+IS>cgu>^S1V z`#c(nQ&L~_k>FJT6Qr->4vHE)(JU6qR+_-X2;No|k!f29J|DNShAE&0d5t4bV2gEC zEQ5zDtvwkHd5D(H7-g?43NtO!FOtHhM$L7|CQ?7zuhW+MeUE%Nci~LM3-81y9Cqm{ zp+=;1iuqLqP~=7djxCVpB31hyTn~wF=z58GLyuG@Bhs zDB={@+I7unkmoX9R~m4TNvKNGaJeY$DKX-b1r<)>1J6aq8!Wjd^@jdSlfgB)_XWbd zIb`W*!b?3#9XhQ<`*qD*a>!igkgpch4D+ZBKv+D$C2L)*XLE$^P!4A_3IJ7hwBR4_ z>a?ePa}ER2+dLly2~%-+<+3%6U(7$ImG8Xhtv`2%mv^Sp^#n=B$WWMG^TE_cmX-ta zqp6G9+`C6j3waPQrJcx(Gidip`WS6Gxt1r{y{@W2$iNnt>&8U`r`*BpzHz08dJT zFNqLH5`>bxE&^Vw6118EhY@xAqUJYYI7TJPh|zGXdA{*>##@6PtX?Z|OWf4;ue6o+ zZL!1uEAzm1PPBhKGOf#z`7f{Q0WXHA_S3H+Kgt!-TAa6NE$F}C^%tjkD_gnewROV* z@EZLPZDL>j5E%|p-FMM zZa4-(C$El`hqG{FoQ@0dI?ZBJCM8rib&wZ}fLHOhCbzP4_fuZ8O{ME$w(PBSYe+?S8Fs85iCr`iHC3uBVmehq{zvPii==fI-=WAp!)Gay11o;5<7~{tb^AWkn3W{ zIIk(+9YN#Jv4%?vlhL#XvWATPx=uITYnaNQV^J2u^lf>)MNQ0Vl*ER42ec(BC^0W$ zy;r!oBRseYuSF|z7v2aI*lmVhZn5uIGqW_W(kem`BD-So!Z@uwiV$`?jxRfp_7YM; zIc9ek&I|e~$DVFbOIAyrby*|MU;U9c;Mgbzcfg6Irbi{^HKmK2&B(K2oL6MZ1tBMY zT(UU6`7K)2-OVgDFK;V{{}8?6(Y{lc^1-iVOKDM;wI80?V?=i9(tcSB#Q7b@iw<;6 zGsC;U1lL?>4Dy-7_h>T%I^7)db)3FNGbewbw}J{?+EemqpKi+H5jpqI*=7kbKp{0l ztU^FLAWj36x*=X6pdXN+0ZR=-qC&tpAV~w{rXg7&U>=a70V>Oost~XaNYj7>+mNmh zun+hQnq4-6u zo2RL={+=Qn(VZIJB6bissnELUzu}u*z21~sxH45AoV3SG=05 z&B>4<^-aLji{ax`Mk@PvGp150VO36+H6abdZGj00)fEhWOR8hsHJ-{aAuNg!Rq6s& zLRT)EqErPmjWfym8KHJV6#W9$Y~o zE}F|3mBt0Z!cr}hTsF8kIPgqV0+idwP*~aTgiemokFA7glL$iv+c`uC#<&Pk5!jM} z3kP@4;8`u(Vim1n#yhcR6FUrTB^WnV#f!Sm1zS>e`Wd7^AqD{EnnI=B=IVf}0xV>V z{Oj*ePRm?r9Ye0^r)H+}*Xh+XKqZ03Nv(*~8X*k;W>NuF(Pj)lwR5;UyTB#qP4 zZgx7h2@;eG)nrw}k^2BVG;mEV&PY$>S#^@+;;v*E79N6i8Y^XSztx56&%5lsMii&4 z?Oh;&$wgAT7|@0dChY2k-tZ*TcItGpRZT&PQDHfw9anVpjw7`6{b7li+iI%+*g$e5 z?Zdr^2=)EiP@P>BGG$NAotir9cj5`2XOke8J4rE7pZ5=J`bt)YzCml}MCo z@p$f9Yir?>zWDbhwXzGo{~#L8RKV|z#j=$!KLcCf2h?cg=){TqAn=o57ON1rnoO22 zYT)0Unv|D)|8Zkuz7GCCJf5pFejdWaVWY8r?6;=vSbe`#B$_wFqWubHN$EUa&~bNe zJQL|zldJfuzmP7>0Q?14XW8!Y@c8+G$0QapVVGPh4f?R}>`_1VO_YuabKbo&4;Ap+ z@skCpI1oa{FDiQDb&cB9R|MU$00Ebe=C+ab-Sx1^lQq4C{l#W#i+MN=+ui;={*VsGssO$#~k)dcSMCeF>nY(Gb9PSr#sF8KVnj)+6`bz^{m&+Lz*&!5uM#o zJ$JvkLJwe_jx^3e8e7#*cT-DFJU!K*76*71*ZkpRhu_j~j2wC@Oh=!m5Me;(olPZy z4N?X0vRSdfy7d38q9#EEA&Bd>tqGfKvqMwU#Pq6X#9Y%93a<_}i}iQ!Vp+}N<-Bf- zl4=Ha&`p4cWFQ^2J$q%2Gz*z{ThZ9-TfSnfhT{rdr$(vZ0DbG@nFl8GyHlk!!oUu& z)i%m|Pij+9IUR{2+8e5q zeXDxaPbzTS)X=Fr*jwan+s3XGbl9CzVv4w?+d<-5@TPlPnbJW=PTji$ZNKQq-dK*z zl?tkP;mL#pz8^LnKG%7<@E%xV^}=QbO-MX|3~k(C|0CjgCibnxZylKyI#Va=5@yvP z=*E?ew4f~&=1GeyqzH(Csj{ALY>FASo8kSz_uE}Qpufo_01qytowyf;4yur-!r$AW zH5^y93&{HAlK`ty#C`72^aZ4_hQ6d)8fMZAI#yjLCY;s5$d+?12eu19A=f-F% z2}9^5Cj0v3K0cT?hCFJ7pHLTtu<{Ur=XoejG-TJ$n=cYld8^6jD`z@(0WB@w_1JLh|LJcF?C`lU$>+9k`eDA0N2i*M5bL zQ7zMhP=l2S*)u2LyxSgHjU__hD<(#rR^@HbW`J2QhO_hS$I~@RuyYL6UUm~5r7IwU zV~2H)TPuZ-a$95##2dCo+=OL8}s?Y zb;TE})oeDS;RBf{(?Vq!(`;wUH8N@cT&8fpCK$iHI=y$&4FLbw*jrXK2*Fr;QZ zrkfqf)qUWFJQ{TUa$mX_{f?;&N$)Vf28$f5A4-Rk>GF5K`OZppeKckIC`L@{T5VDp zHlq1>LbJ;#);d5~iDLVZtrdM5a8WAyPv&!#iPm=>gWoqwF&si}Zju%(lScBabmz`$^(2st%)pgZXKUJ&Yl{Z-NL|TOji_6>XoLb zl3Q-AW3ykdPmIsD`Wi^4D@v!Nl~qx0_IfWwPvwJjINd}tC?$2l>91odT~*#|<(Zv% zw#qUW$B`$Bi`6a;-Qglsse~?$hwmbwwBpebX~kHwGePCwl1n3OLm55kBE-|8`nbVS!^>_xAn)_U`$)x9<;J?>1&Bb5a`h)mO!d?YiFh^2!7octEzpRI?-Q5WGWSDwXX4 z(%KV?zxpaMQ(7X^W2^NT*lfU>44AK_gV0`BP98WM++ZN7?`pN*5K7Wy+xv*ArUXg{ zagsnTJ7!9kWl{|4guEz{y1ilWNn0tYupQq)=$Dt1SI^a@t#D1cqt&_*n$lz)k49~e zKsyMNcv9K9s&r{0xkRIsMUf=iK8tHDB^UOqOQIkYVGsclAuz4dGuUO&s#|Y2K zaPO(6v2o){27@_dCD;i^2~#>NVRGq7wCm8L00fDULBL`)8U|=tUzT>3l1qRyv?1H38mgzSf!V!GNet<4L*_e|fO?yC^ z5?eVV6%^%O92jAEvv@|&kDkt{NzN_2xQgi0aUDU~X>z8W`7S`9kPFh~0)Zmn-4zo? zaCYP#KOG%gAudaDjwgCPUlw|VsbQ$}nnZ)eHCDX92&(~98bHfPWG4Oi07*nR&p8t( zs$&A}fse%5@#D;D8HP23nAY`=A*JV{j!wUUC_NoIw7P8f?qB!p21~1{^!Z?)zo<TgkaX_J&+vSf#!0?|ae+f}85XsfJuP{f_ z=`ccSh>I%$8mM^6qo20{dR2hRU%v&=$7f}=Wn~S#aWv%k_-H+5j*oZbbEU>AqMcApuoDpqAH{y) ziVWcQO9^fGW)+e65qGw*oOqf*_L-)NlmihTZ}uA}+WN+LO5nLHi3hmSlL zb)p4%^pHM}H_=bt!-t+X=pf&~;V~@eL{uTRO|RVs9#S7}vmS_OR?N^hlV^U{He z-rkvBTRF&9(`;STO$YO=CK>wavQU_87suVY82av%!cg(+zlyoT+~|~FHmz_^9sKKU zR3!O7nNHGb^jKHX8&2Vn+S?OKAS7>rzMt41{o74iNj}MRo1WL7OW|9%hQp(J-Qdvh zgR?yS{G?9LE-CS|`IRu2^ep*lL$W1XL;ZPS)HYf&+iB}8TUKI(;#8?&A6zb~kv$4; zz(0*oN#AftrC+Xkn5bP2D48)XihmXidpP)*YZ`AYsP?dJdDp;gHZ1((rf`YtkzrU% z{@dG4Fb@4g6~RP#V1t?it5!eLr&Eaw+eh8-ZfKTQbdZ~FzqrjE=nV7 z|DiO~Rc;|v;9BJdw^c_mmL8vMK#4Wop>8AP#y5B?Yk*ro~OQ0^t-R06au^$I8KlkVjc`U|aA!jw)QI!QRxP5}Hd zL_1NlpS^mGS8Ddc+n2;0VDFYHJjeDVczN8AS5}X09{W6RATyU{wtp);93Iu>p7Xwu zzJ7O%o%IRT@ye>i8e| z;~By4c!Ar{XtJ~0crBdO^bW5=9i3#=v8)W_%WKljqbZRbY}|T%@h!b4)TecD$&_WKvTkj!cqa#i#unVHikEvc&pRhAUZo)72}KrPT%njra|B4>N|L zhfcefjBiZoiSnZTL3CAKfnw*0Bs)!-C*_xD#DtfC0U)1{B)3e|Mc@<+a68s1l?;5V z#w=a`+mt{okLDZTsjc^Z>Q0UR$Eflhf*1<#_WSv&GwpBbUxiAv+b{KslFp&mzJXll zoG$&~o3X2;K*98v>EV#MkZ3*@HaqKQmrnXxF6C5v__Z!I%C71^Jbn>-Iqcn@sfd`% zP)_|wNNN^6CG6*Q`wjfE=UUS0d$!?P`MTwA)3?KYHs71xzSEuLzB9dTSDE%~=jN&9 zKRT@OetSw5HZ)#bZtet9muef513%pTZXlDFC~JD1$t}8{!9Gjpx69Wui@FZoE9ef` zT~oMA@NY3pyygX>F*NR1QNKybWgk{O%8D%R1uz6osqwsftNyo==)`dV6?O`;*E@ZG zjA>`SwuEK(rG-quT2W z2>l;jjM1L)ax1?8iB?IRs7^-Mw&BE)ZfRI$YHDSel%J5t^JN$&L+Oh;Yv`ium9Y75 z;BCM0CZx{27`AlX!@Q(HxHmyniS`q5G)TaTr5Rak=MAU<7h|=NBWa8?>~<__=fa3D;6%@>61-EdA2{&ik%hENA)7`tRZGBPns6 zc?DfDJ)2{43S^yNZ%k)FUZ<=VUN0^xm9!_PMGl`*nRc8$kB^GwiSqVez&$pwf+Qi2 zD=(kCCkABA$T4!MR7w(*JSBEERYLb09 zuab-crr|Jm^IGeH!ay^J=3OTURH~+@Pc0EAs8j0(*KygHXnrm_1I);<6nTPl2#=Vl zsZQ6>0e!3FpVy$%6!lAV&ZBR#f{x-}dc6{&UN7lrnX}nP>>a$;%YiQ7)-GT68xRky z3G$&inb+JJ)zl~vrMl)f|Ku2~F*+bc)1s-uqOZiVNnLkLKmMX)Czwu?E?yw?*P9~t%9;J&*t&A!9>yj(Wb$<7B#u4w<|VB7wwRT1p)&Orit@lIYNUJv{~5SdljfU}Q@FBM9XRV6I#z2~ zTi>yfMGqerrbvuM!>|-KuPI2B)H1-P%rGkm!{imyEw5zhT1WhSzSPeww%?b<;ET>4FQR*cAV>pyx!8gTh?}P2dE)Tn+2yymcMdg(` zK(&PLYU_aLP}xz@j>yW7*Y0mmPvVx{Bv|zIG25X?$gvQY@C8?Koil|R76=JSOAR`u ziWw*-hmPsByg>9VG1GGdIZjEi1FY-@7FVMR1bV(x%u<4wBshEsPVtnO7JFBt9Cbjj z-}H(Jwyxt$Y*wkd)D|KsE0j9`K*ijd3%)2fpDb!O_8rqu8^s18Sn6)i%^w+5@<;e> zHt+V~15V{LH$Vwv`Ck*Kux=?UxvZ1g4V57>FV-$O@qUP?OFL03f{M0FLau!QYuD!1 zo>tg~XwzDXro{WtwHI4P!T+=4M-L75fAX)M?VC2P>1Z+6n#xM@b5>-gr%J5qFI#z>(aX0mF!r74I*hub*vy!w(1hi+R6SF?9{AaF~Z>nMe-=6;OJ}g2*Z(XB_yyiN?uk;!TK}_RVo<4^PUv znPLg>=@%VWg+Dlo*>gXNhDmpXQTn&$hI&nWZcy4E)bcu0jgN z966B{ui%EEY*dQ#EN6snG~JeHJ?z|kN6*%jVBr2j{K4G0JEL|{TG zZiO(2jX^Z2q=gikB;=H=k^5(A<{Y`&5UQBY>CjnWqWXk|gV=(ny12eZl8am|3nx$b z6hFfK8^V`4{9)+ynrRB)_4!WtA& zMuLDcCX~(?D?zu7?&-2yikwOzAY(o~Yap&Jx}b0mOq+f)_8j+~KM2eQ7w?ElR|!TP zr)9HxQIp8z4YMHdowPS;_+V=7nr{|`7BswSYGpy~(~0LYwF+TVUQUV#$OTOh7z0bA zY>k985`3MB*4SpLri2N077g2diOgXj(!te`SprUAP{R2*6o&2N8;P`d@_|y zDN!gHJP^KRAS6?}x=y@O5;~tNDWs>~!=u1_q}PBh?;RNi?Alo})I!MvK%(`xC_9MA z+La2V90De^jSG#U0ol<;JK_5FTfSM&=9w(?!O97@9$Efanja*lh~-vh6_n^r+69%;yU(n# zK=<-JqD7u|Tczih2Z0@Lc3nE==DNGJa(DeYNq0@ncw&?W8YsuBzxl???bE~E&H35s z@&4Y(->H!@HN@ovM&hKL|WQSQhLf zBGPipl$r!l=B4TpycZ2g_zJVSL+;Bo#I&MH=nKzKU<~d$!}v^qLX;MhCtmnmhj_9c zM*RmG*tD^z!l2hjxyU55kMU3c&Ou23jh~$7zx=))hX3)z&tGO-96B|b_}O0u3om!L zkgsA+(KP;l4IMmiVCfY!n%^piC|Wyp-P&+ z+8YPm9mG#!aP#Ld==!N^grJwphwcJt9J;tDL>Rvv^un9yhT?3613VyI9jh2G9N$E| z+wslF%5ODSvzYgl3OZR89}roN*tL@q*ufn zC`9HN$xV~P)UAaN7OJ%CIUzUCje9LgGDK_nFDqB6C?`7AeERz%-(4iUuMfV{K@#WX z%1nDj4LYu`r4$j|I2uXk4Hco7=L7?ss}WMmqR*=0bbd%#%~Y%=UoQ^#)~oq!l=ORX zEAR;=IO`i@mOU#8vA-4-MZ4GlbFR7PdeB5YSH_-&Yh6CDh;ZeyYu74Rg$>-0p4Dn* zb4~=H0P5QafT2NM6hpT`ILMcd=-nq`vq1Awxpjs3;GF0sFSCrNwMd&$whd-7rD%ys za+9*d#*VYDhb>~`YOEc|%$V_(h%Qbss`W}B5?dCb4G_B&7L&VUC(#|(k$h{+?#!Yv zmA@z8g;6tMg0`U{g{8J-xW{xBVN4L?U5TwG!pURtussOeX3J{Tsv4041NLivxh;{> zVU>iXf)ENKtemiRckE9GZQQx=HF3UvgBWSEBm^(#YPq$-u-3pk}{ zRlJ^Rl}n%3A?8*3JRwlY>K7||1!R4m&!9%#k-lEo4jx5TS8-icVM+s@8V=Q?xMg0W zLGX*`yW5lF^{O%J7c)-!y>=W1u5FfZozzRJ>hKE6|4F@&C<*uaT*$=FS7`VkFgcn& zCH|@Duzt6VB;r!ugu*Z46rL=Bq?JmA@`*SeG!;N>ei}zY{sKI)ych#m0qm!$F4wfkwSz7HQu`Fbpxc zkXvJYCw|fPxGL>zOR;p%@{ z>n8xf-{;(4_6z?1_VFtFFnb-4A%Jd%^>SeECtx8v%`LgQj#yaqc7^i-?EO^Nd0NGtR1J>`@qin2{$Z5uSu!?lbD;* z(@HzL&OdEV8#mNh9^cM+QbM!2rCik1REEdRC#hy%pHP94&T(r*K&4K-R*fo^tI&ikFwh9X zCgP^~aoAR|Rcv)S@Zi@fQjJcL=3=cvRL!8u2d7M3%4t%n)at_tSB%|jBsGIM<(k=> zEz+pc`MR<~sr_1|^y#xj3MbToG+M1Ltw<=A_}ejSkz9mCX=Ogt8h!NU*pm7cwt88u z^3>u`C0vCGzn)Y?f4#z1x;Zg=5m5R2bpnXwrN~1HBn93@uYU&!A zEH;PB;|qi$EwQ$auAaVup~T48#MI2(!qO`E_mJt#RTPJ}MWq`J^v8g7;V_5%suHdt z>L_C9V%pxXV=mU#<6sO$0iAi6hcA8JFzvUs{z|XB7~wLUhg7pT04Rl`H0jl4DRyHV zUg(#ZcoG^CsvyoD)j_YU23)wNQa2~`W`<#^5#!XN9;PZT;bjp)O{r>{(xy-QGp{m_ z_n0mP7k1~m9%JJy18?R9{i;1cn;Ouc880*)y|k+Rs=??Dn2cv3oYLJH02P%30Fe5D zKQG-F0(&|l=*rst7&vyra`6DP8i$jZFf@-#_X*4U2iFC#g&Jb5E0V_I=C`MeL5qCmD^{ ze7{rO07;3m9e>~Z-X9NSRo$w))V=4P{haE|7-Ns$$rxt>qd$SkqFum~CpeV`hl=-C z&-P>7KJiF<^y<@2n&WXEwo&KB{+WP9;sksV`)?ba4s z$C_9(YHrjYkLjfz_8=44!oZ$edaREo?!%LJaSY$QXKhRO+C4u+Bbw{piP`amu~(jJ z$Nhg~Ozxhzd&yF+6y9cR&l2jb>4k%{r+(*&QQSYpm}BAK_^|~%_u_sASMA`;z0=SC zF!lw;lx4>Hw;Y-ppA5g?N&^-&!R#SCP`@RaxIcvZ+C#HTCoa5v<#pUYj{AR{nV%T{ zmpS#{Gq&Rd?oZ5)pIG3(>v|sd7jbXRjn7W~*FUJPVQeSfDg4F4{IR7M!+$%(*w7Df z|JcIf)WT0UH++S$J0-M##9q4r;B+&gZ_D5?bI*(~&9QPuwe&}@xwgz_4j&w6TB(Mk zj%qocaHgz06`$izFd6T=Pr#el76J#>6f$26)kXg}aM&d;w3XRT@{relGDo+&41|?Yzr1%Pq^9i)Su=>*5bC z{`BI{FBUH8m)w{9mm-(CFKxcmcWLs{y_fF4^w*bu`c~$xC$C&#Y?;|TPTM_x^6T`# z+kX8&j+rgm|CWHvOYH7n@sD62U~LkhsxTGM*0D|pb21kQzytj9F+U5iAPWKg!z{we zu!j|_l2x%Ns|FNmSd7J4ElaQ@OR+k(hSjqMz^f6Yu@Bu5TP_{M;+9JXVd!${AS_)j9Yhh!rGsc?xpZJZ*>dS1 z%33ZR*kiU_ItZ(mO9%FyEtd{p09!5{zy-EkI)D{yxpV+O*mCJ0PPAM)fHQ2lbO3wU za_ImbvE|YMjAF~B1GvSOO9!xwEtd}98(S_Nz&y5GI)H<0xpV*<*>dRsUb5xV0Ssl! zr31LimP-e)mMxbK;4fP)9l&I^TsnZ$Y`Js*yFszV19;AsGsOcK&n})W9>9He@l5dm z6|jrnDjuK*cJT+r1C+ro{-k(-M%cxl77tJhyZH0s0lHxq3&n#t(IxB=9iSz42{5Mv zRK+dna=#X8SEFPdpcIn>Y0or7j?k^so zQg-RDiwEeHUBW!+0Ohi`(1#AtFgR)PAZjLjz4Fh(e-}Oms6=xWu`;hJF@X<=;L5^0 zNA+~DG8mNPNJAp%;Yw^RSLu|@Y~m?8Wm~FwJDp5ahfU$Xdj`E}?{m-LH0Y%(E-UvQ z&vVat?lHZC9?z#g?RkGgPZKpD`nd9v_^j9l%=ZD}IkqL&+lKe*Ou(!Ln6Bva8dp?h zo~fcbj}C$~EOQal6+|}1BuN;-BVll3SG+YIYe}q+_zk(LA(2VCg(%OquUtbxC0-Lt zrrI*uaBGxr;4Rtq))rG#8aS%dI91_PT%{?z8jJ>oNO|OALCY_gm60u$`R=#FQEr)U zIloC$i)c!K;)IN*E7h#K$sYu%>cxBODMbEWXudDF5?oOpFzU3HZ2 zHs3)H)BR2wU5s}?g2lc04&X2VY_H0d`#pvv%CtG)ETz4v3Wh~F+`s|*Kq%!C|=bGnZ-~Ddv`G1(7=jV!3uKa)r z19mj$D^j!Y5I90F>&b13aY-Sx1{$ecnbo->sfu(|1D>l~P=Pd1ABKTCY(yp%X0Rt` z)+8b|W~eS;<4#aX0ae!WCf@3c$C`KvDF{tMHH7~w^pVVDH}JL^n@F-6k7B>I^Vo!Qy6Ha3H|z4K#HX}#PRns#!;`{S>4~8U5y|dw)NT4z zCl^GCJNeCKg>k{@ykJxY`2}T0{*lYL;Bs9sD$EP&QQ2jnnJj2x;T-re$(Spzip#=F zkRIjG>iXEN?AF|Hrb1xS9t9+|yI0`CP|unah)Uto0Fxzo9v}oc@mZ4iTsg`V5&w@e zQJlL5RDc;99NZpgDyt1vsd9NkqP>Z?XWA9juB!t9+&vx!oI^TdP*_W`q~paeD(f^K4D~a zMF}cgE?nOEU`)JV+cEaBLE~rKkXCf$u)_Y4udj{7IS1{rSC%uC`v(T_D8Vg0$D zsnNmBQ06xn0%w`J3a^A26_Ep2!lZd(QS%IooX3(xV2m(*`)zx6-L$o#K4t~{s%&D? z$(Wl12NGbY*nHItn?dYtidKMK2B(cB+fvCCqzcY9Mzq^hMobwr1_UUfNi}Zp4@{bH z1xV8pE)AyIBqnUwePXw;cWE!L)YLVz)s~R`tLz+9BD7MvhM9Otd^gRGd^Hhd!mObj$pd{(V#s;6-Fg$$s@bC$$_eMh% zEs9$S4RN{4r43ek4fUYz?2?sSRCX)Q7Efh`hdWifZCSb1s5;f)&^1jR8)Uh+vedNP z3s!3H0nC}B!mH3ga>TcSU=`r_iXaP$JPUJKkU@o#$V7>OHkqJsVT^9XVbCTUA!BIJ zjF%{U{ZWDXK4MhFe>r0uO>K#*!O3+HK?+h(Ung z40DrDfj4kXJJ-8^fA2ZX;JkR7$nb3K=M@3`Lva+oahs4|W~GC#b+ie7*{uuW z{$BoQ?|wsb=!y%l&Lj&b&`}aJo_0G5uNgtlv!?q3IN`Gpj5;0w1XvWi@}~Hr&-^^gHjm1Y^FqR16-j39ueLmtYqEf3`p5(k4mZ&O1uUKaLN`>F#0zh-M4 zZooeq%Gp z6cnDP3v&?vwbxu+@g{aNyA#am!|V+E6#Gx?s~I&x-{l<37}7br0JN4iw58a1=FDd8-pV(7<2`}!Mp!$J%d3$ zygC984vyxMpZm=7fAsVxKmOF|#~*q4Ll4}2Z0_jvMX}5K)+S65oRkkg}^R$Ot#AK}VQnz1gd<>Rm5w{W!RiXHmss1=245(Q8^#@|;cJM` zQ3Gm7JfQflPprI)nCCU`3Wx1?31qVsxhOc6ZNmwt3*X~wilY)2?rbTsLt2fC*h#Ys}m&MSC@uYL9l>V-wt++abGX3?P*%c z(~ia44TG=2+rX;nMFz2RS`^uZ$HlLUqp-X)Y$MyrzL@i6GlImb+yaEkz#xRvmhJg+ zs9S4fXec0VJ`0te>`(G(Q~b+|OJDh!T}=E9t9+uGOLxxRUAMVT4$ds#iN*U2^_#W+klZirM} zqP}31w{-J1P*oeG2oYK`1;sfjW9(3C2I3{5M!DLWO(jW$gP>bXP#dIrCd=P4EqS1$ zwX?H+M}5`*N^ah{Ir+b;>UT7BuFtd&NY#ZeY=~A3n2~kD`sS7l^AHxEztCnzgbfch*U;F-U*J=^@A+6;h`@mx8jwpe-57^^aSc|EKu+HlM%wB?6 z_&9qMPB#&=J_B4KXX^LaUqP!HWxH4p45e0P!C_?(2Jol(C-}$tHqyXsB8dh=6DxpTPps)1H|a4y%V`Mm_Di~sIyE}M3rnzO;og_Oyi=)kAh>( zfk#5A9ma);BeDeVjyPBZW3E%;kjk<+1@e>y^(e-Wi(|;eG32Xb$Z~NEc`w#3Z>#tX z-#j|HxeTCV{B?eT|7ZS1ek%?W4p{AWo4~>1au+-8@c%rc5%OKtHW!Ce>m$P{Pz0FGyinjG9W)J-7~ZS-eUy zRaoREiFX)c?U`^&QCn!JaC1h6zzMg{oS)Rw4JEt)MD6|--T*^Ut?)7dtRf)lMinGLsOv0N-GsD$EK zaxvn$8dKe35>uf26wM0?O9tyOvls~zHrt~Dpp$Jk!4}7 zN#}Mmo9yPH_H3NyNC0kW13*Mrn~=U)*q`*@!?Cgu;MIg(^YCOgNpR09A+T*20=5+Z z?i>y(CVye+8;gtISpMdma^F{Y0InAhMTjI1)7Y@-P*R1{R_2`(@GT#OnoTS%s*`azGXxlFNY?DUv4X(ykVX z%uJC-;6YMliROt{!sIhl)i0^Kgck~K+zW1)4}wP%F|-I-2$78uWTz^Kny9O$qR5)& zm4X-tgNtr>tqfW783O*eEZ|NMog(Z&0u@+*1Md;Epa=t#L^&uMPhpWr5mASb<)TY) z6FA@j6%@{H=?a1EoOu_!yx3`4*c!FmB# zE+|e}0bC*Cu(`OPEMS`0aI74At%{mzsFJM6PC^YV&8Y(@GL|6v1ktV0b5X~(iVAm2 z28NWeEJIRNmFu#mY5)?(hrKPqy(PM-T9#ml7@9{AiH*7e07=0=Orr5-U>deh@fer` z>^HX_bO>B27a(ON1=g%6dI4ielQoGuA}-m9RZC9IExEbj2*OsE0dnkxUo_zVAfKgS z16c5Ce!^eO!+`!mTM2l3We@RU!GT5NR*JZFx2%)P6`%&tK_;@`0gm7v|6x7C2LM32 z1;dc>Kyv6<4;CyKShECz00 zuGD}~8R5UM_FIKN zYab4x0#fVBFQ9|`96GCq)j*+`%sDHt--3?$Ba~6pJ>deB2`Ohu7a;=^69QOoXR_>+ zp>oC;fD8AUh^el&z&4YgP#U48Fe3GEM+(w{^r-~YsTI4B?6g!MlcEREB*R5ZPK2`+ zk_fgMSSQ#eeA62Ybw)Mp6G&B6?^8fNPW7PX)c9wEAwA}Q_j!Lz4+Z%PdMp{+cSJJ` z4JYpW9+Y570SCDPk*f&aci)W1eSX+a@wn*s1%nWWBsBpHKXkD;Ta!z|fd%&=z`lU6 z4Js&l1LqZB0`FD4HG!}VErg;lsdyKP%^i2H5bZtZ}5UpR3>F$stE&|K`F3gpf2W`5#y3X6)qZ5ppnHwA%9UC z({Q86T)7j-A;`nP6`HRskbdMK3#il{ew|<4A@Uo&D7Ws~xxwBd1peid8--bfm_%Ri z#;$cuYm(6lGXO{^L7gDmo`Sj`BEhSW{(^{>?M@)SCHzO&PgDTK;i0homhe!(Rf&eC z80K*V2vem%B)^!uCD+D7y8g26$5Fe#x6n-fOa59ss*7rcX1JV%X4`+nUrS`p(MT=7cSjO)V{p}6m%_1uv@bE_ct8PA=q4FMZ#W>PBw@VQ~~JL5}-5j7~GYi zU~3D^|C9|wI0Q;i{C3Mjty}q>PFX5^M{+@{SSv=C3(d>oj^G{3cLX<>4+UEvYV8_; z=#vV6DdEhUq`Q_2Yv16%R~5SBjoU+^hfJII-euc_4#dAFUPP=7R9|wrRWMakz=l;e z2KHYP4r9>xT_U(%Ed>=8u5Uu5NlihU5G5ea(VV+69cc9hW33@*TX2$*Q>*9#Yst#Q zK1AF5of3(AAQ0t)I%m5hQRf4DcN$&;|M@+p`=6?~p>U7T@I$zFM%t_Dg{BHVRaIYK z)jq=Sd50Jto&FRKtHN6lksj^|div*hw7#>Rr#sTT@S{0mbJ+Kj;xnLe4g222T64`w z$iTXk9|D|qR2%>SW6z){P?!x|9jp-SkWW{}8zXkGXC*|PSn&rThL|V@PT+EF2k#@_ zO(;z2ZHsxK6qx4Ig@;0&=7vy+KVS~?|KO_l$jyhIeDaXBrA*iVXhx{tUgI%VgVFz7 zcqka$h@IpQv=9FeGqHU?fAX~_grHaPtNTxO31y8z+aFKz^AE%?iH(@!-OS2W-Fjo+S~Ya&;+1R}Di^Pc9m65Y zC#!%CBJAR|o7!Nd#Vf|J%f)LpDTD4|q=-)kyimf_s`7B40-9B0ndwg$Zf_>&b0>Yl zaEn7%tw6XU0UQeYLS;Ue(~w*qKZ1_vr~exL!1J&63n+!#c7JO3&M)ucuegka-z;l{ z3R)frmshH)?DPap+3$79hQsB@L_$E>BqnBQy3raAdQ)y+u+2mL!td!9_*MM^+0X5I zcGt~c+6~(h{6)G4cqT$RxsWRnV$-<^CL8j+hZ~|X4=q&_=iRU=w0Q>*kI(B+SiuM8 zT@cxlvKtpg+RLCcZu9%yZVlQS#9hcA3I^Ohx6kX*T$;=2Fmy>pxLn4IM4uNNg!|$? z`v}$eTJ2JTo=lzLQ>TSj|Mz19!naYx|H6;3lhMmB3tg8_ZWZ^v`zC+8u!p~J;qpm@ z=JBVL{Pv@meGA)w*z{g@C-=R0vzrvh5}z1g4qek7+N>8UxULCDp`D9}E*(`lxiVxv z>qLmM(0%SbL+apA0~p+)&ohVS zn5U^}06QsZA2_YjIHd4kI4C@rBorRV#OVGz_Kofx-aWK^+f4(#J)62a*O!HYW$p1m zjn_|H4QU2&tY;9b6R&g*G=@&4g)((;bHIyu(qJm#JdI4=7P32 zF$J$-HxIYKv@SK-zOQq8V~+Pr^*uGogeW|;ySEVO8{(4FYb85tl-j1PLtDz$c(qtZ z((=0bzmssWojcyN>sE zVhkQKee8X!Ra8~k%}Og()NRndMsv`|%OXB6)K8DE_~$^JIOMAd#i4zz9K5ED93>=6 z;hzqx;_|Hh_wHZwfBkZzo)6!*o2P5Paz=OI^+<$oUuLa3{@&xA5BA5au!{3f z=Ro+KjW7lO{eyx-R0sa&-w5vhk8nqy!Qn2^X;&3^JRJg{#x)6!5M2nLa3;UR;A z)=McXvh9$jl1Pu>Cd(qqMgE_a42ByfFd*j&K(oaiMbU{QVPO^1T&i({e`L5ooE7sI zLKJ{0rY{sj0r2dr@anInunhE;VyX;=z-Z_UXFrC7(F1vh!Qq2LbEpD7JB(23n@rh# zf6cD^?GnMhKnhHKXWCX|aD82qZdIItYIp_uymBsNDyrle{m3Vag*eS zj_T`?S{zDOg-gSPoGp^H-6P9d8i9BhN7jhE+EVtSrJ;3|+btdIsHLPtL#~PdJbH!y zwo}(U?oDn%sH;eD?#S>u_{283J(|8#*S#*PiRu|3VO^M~sLJKlg;vxRd!?w#nXzl! z9vz*u)lQJqP{vE0koYUE<`%_~TiVZ_&6QV2;4=yMVcLlw3?M1_Jq3s81Wn_d|(xPL=pDcEwJb_}TlccwwG|GNu0u%?FpOAcFC$m^ZE+SD#2>Ll3L}a8* z$6EufNjnjX5#7PEK)4EC70MCCEtT5r%)oX?pq_KVXy|Y>^!so=d4rgzuJjX(AK=S6%7Y85#E;tLP5?lfAgWcW_$Y)qm+gNa$D=3TPJQlo$1sC=f60DhSVwe_04_z zZs)Cb<=%;*t^I8qG{Nz&rCKA`x9^VWkK8raV~-(@=DLs0{gwvzx&OePU2E5McdXX~ zVvDGo-XCjV3R{wO1*uqNMc-dXy+5L9fD7p&z?B2y-(#<;*=BZAjvTfyhSqW3S5z_s zKpsGl@+bKm(oEoc1f)GEtsoWId!sqXtQspAVAZ@@S^?rZK!9@F!r-|IJO(6F5Tt+; z62P;8Owk|u2S}|$8w&7HG7=>q@waZ>HQcv%=E%V#H}}-kC<%ADx78;aLYya(Pwc;~ zAV)k<{duiWo7{Ta{rB8^a-13;MPo}&Xo}k}j#fq2_lJT}>*k)ld(Q7#Q{m-4(WC79 zFQa!nkxUfc^h%0m-)+6EHWn${bxqq)jT@K@_|VDL)lzH|JDCgA!AaE{(`4^1Xja+iUXQMsr4IMWQX6Xr&Z5 z;zpz@m;6X3(qriUS|^e%l>40{n3=BXU?t&WfFs}NK9wQHF;ht5`5xA0OTtLp- zfx=U=SIR-g%^Yx;Zsdss`5rrGj=u(%k=<_9UxYL|gMXirNCYQ3LTDn%h&oo9l&?#A zZZU2h!M6u3xwU=-Gsx0j!6?{ha3qX@jbE25+SVQ{Yqc2^@_15MAaYRckRjYVMZ&@g z6>|EnKmr-`4{!O43DCP&g}2K{04cvgmCYEgtuYP@@BxX`-E>py`;>u_FSe8sZ<)xT|n+A9KSLYHW8E|Vvj)= zrF1)TBMefiQ?k`|VPnr|rnbMA_J9~}=^m*`_PuTXK&06Bqe1Vx|3q0iZqaAw=zQSa z(QDFvxOX+XhX=Wv5`K_R1j~t*K{Wo7_y%$@%kT|FW3HaQKM=7G@Jkit!h7crwpH8N zDpB5R3DWI8qElo9lHp}@Hmr->9s-HRqKm|)>=LRiBX8+dB*B$czdC3o`znOWp1SHg zzi36WYyL9R=8Q#Mf-~xix?;-jj``zVys1IVqWNDG`inUEH2rS^MKEH|C7@%J-Tz}qc0H-6>V0)IE;*?1371g1W3{W8NMRX!$`FR7>lXl6dnV4 z6g{9wVIdFb)oOBoLTr=*6QemFXB#?eV^tMMEei9nVha!ekYq6A1WW{?`);Ah4p%_n zViU=akZMCP8hT;;d)N33KF_ri%k;}1Uavqw|) zbH7_rD{4?|p?5eXS5OUlRnN#If8=M}`?E)chjuvaPkLwksscJ0JeU!#fK< z8h>s~8c%AH6Cp1+%{{W))G8|j^({{hqiXEA6&SrN?t_fXu%~m5azq}1Re~KLBNICX zEJIvWoTX?H;T<|DOhCej0<8xVLJ;g42T$z>HAk|mfUsIgq$G+Wh4R;FT+lSO3xtgb z=5T3H0g*Kblj@j19;bx%iUt`@bNFFmloHv3Kq0=pfiHzZQ*GT6(hbN)>*i#NAwM7( z75QborkVRY>vaC-^;Pe{*m|d`zOt#g)-ODKShmuZeB>kCj5RlpYRxsezNVA^pH=nn zh-|2@inI*xe{{$2lU~CC4iXDF4DXY}J0BU_4dT5jBjSiEZq9X+?OP2mvoHYs!jhN` zEDAp(GX{K01%IX>9ev`WjKCOB2HV==t!BJ7uE~|Q*IbRmbq= z5k|a`^f+J4zdUDi-gCv9^KSF>b>d?)G1>EWlk;|lCr1<0eLl!Hy;p!BcNuZ3wV)T0 zANBAFB6qpWSLC}`6vW1UhA=NS5bujlx`~Y>H4UypUEWnW7OPM6@FB>$4*=3&>pz+^4nQE zAeqhjZJi(M#djlqR>OKp#z3uybg>gXfR(m_cLYR+(0e`(o47)jCFMH-P?XLXz7I?BZ0Nb zQ5g|V^3(gG%XfTA@_Qx232Q}6R(I^nHAnr*8Mo=+QA%lz;&X&EKitmb)!1-=Rk8%W ztL$a?5(63Zw)EG*fppuFrVvy~i%~iBF;&xS%UBy>NZ1(MoC}BXMN^=x%>Mcil8rQY zFbBX4B-IV62L=*L7G1OmOHoy(Nuc<$*e$k08?|%B;PMsj4K18r3`*|Ga>+fof15|D zEcX_ER+4($D#^Bwb?$%U^rN?T2_0h(KlzaZ*_*D3M|0kbOv!$Cx$g9)J37)Hw^9Dn zlGv&&T|aZv!($!P_u>6rQfB&r9oL4gX%E489s-|^v+1JpZb3M~pk%Xf*OBk7n6(EJ zls;b)?c{~Tjhl#QLmE5Lt0=>Z5a7(Ju80H!UN^`z&f_Xcvm$xgn&3)ODDuck);6>O zB^(lV0G`)ph6X*rX|P((KN}5Sn;Lrl;8VgAhY8<_#hxb%1An5Qzg^N-ksX3GmLl2V zN_0oMHxVBGY}J&)(nMv&R0L32>3)^!O7|tIG%pl04~i)YRmDs3QMG-86Bb~nDqP$0 z24w>O)tpv*%a_c>R}D(IJR!6ww^rczY^5xsD#&|v+D7HI35eNDDv?w|h=`f63$Dvs z8}j=LulZ{Ix?j64^C@H08|NIMVBzi}|G>ksHXW+nlxz$5bvTdO;TW6dSkYOT^c!xZ94X$Ar>s5_M0{FsA~_G5%O@Lh z^v$L{9#0oOX^+OI?5`J^<7NK(SarNQ)SYVJezzwAL&UDj#n<_bSTJHnVy4qy9uIa1N(XBPKUGs3jRup#o&#Jszomc9?@#jhzx(hX#*($c z+C)`mQ~!eSTe0DI)KJAeC&EbAQee*7v4REU2hsPFK78rl!FC`G`xjrlX-iII4hjS! zbvw77e&@h3;erlo=!T%rdblncx_A_UPRW3(gY80koFpFw8Ztv*#+``8mHc6(NOoWV z$NX>7Key@E?MiQ)!}|?`fdOF}x59~PYzNd6)ztz2$gR6~Z5`OyRozkD(FUU^-B6dR zjg>|G(Lglf^^)y~1Xvh|CK6#O3K8Wp2|AJ##8N0po}?hYy|NvTxUS_kl_c<)aSa3| zQo6DTHnhN^GF9SMc1H*oq>Uos&eSCD&vmEMk8)v4IL58!SWGxrTUKwiSbT@I&faB)BHL6|EBtY>4>huRTfz2!j=|ophTp@e zj#gDx1Pqn+7r&v!<`?rMTpS)D{JcUbYhv{CA(K96K~fuK8UmZ~!3oYyd?QcTpX`YN zj&B6T*8|dN(W4oPTKN9+hEJ6p&ceS2rJ(ps@U!>7`-3m@9~FKf?s0|R{bR2#izi-0 z{-EFa?u?dp2odMIpA&vuXc^>_g}&l9)x}t(7dajYcskazzsZGL*9fWx@0L&%ayj8@ z6$d0PQzjQ7U!&qgx-C})B*7puDXW5f6dn;(X0WFLju8a7H0M?*dI;vPcyQAn+=&_@ zLw>a<0-3p%U(p<|KznG)P*wJ#uSP+^oaMt04z#zmtZTH9u{v5&<_r2@spGdnJnb$! z;14cP@@b@e0rc;M(d#y zW6LDzgctz-NaNeTdvZ(Z1hST`YBy4GTLJ>ob_Wm$ftHCpT%<%W!(}knEOWR7mqVC^ ze}uUlhHDI(HyElWhzN{Gk~WO+h%^F2Lz5s>cWmFbwZCukrfloFwQK5XW0e(QeAQzh zi5uT6Xdc`AhJqJG4mH>)(X6enLR5itS5*dp2w9otrh6pr77|uNLed;@E(eD+L?VJiN6SR7C-4tZQmd z*9sdf$@OCG3%WzF1ka0&lKUi*xznc`CD(^EhtTi>?yhUmB(An_pIm7XJ$;&&ophh} zXeV7?bi4J>*(aC&{zrflNRf_c`y)@>>FN3f0?o?=zVCY{zqWEkCh!4Z=t10*JGA(R zKKAcgG3jp9|6Dp@k5sC(FB@ni5%eiObM=tcqD;@9xpD=%L$QwTKXauZt;H{2;D8Oy z=&;+NPWTAQqgO85<1AnKg*_(SQ$LJh*V8+2FWrp;W7uVS7v3ZJ=pOCgC_cB}OYdGT z-P1UwvFyGyH~ap|WqWK|!^-=qOvmz-!pi&TYX5Cx{}=c38UB{=oT!N3l(tC^N`Eg; zDlaPks9Ne?Evd~p7oB-m#`S>fCHLn%&7ME;`n`YW%lY>Pnu32BdL;A>^Y_E)@T-x! z$mvL-EMMMJ{Ry|ntdbB+H_3A!ri}m|8lQD1Xk8G%K;Qvbc1^xu^ zkS_dK9Ahsy7VIa;iei7ax;OaMol==T?Lq>G-3GrT0v}(oOrL`FmC9faY_L?8@dmi? z?Dh(t-&rafu!|3t$`1V6(vzf0OY{puN7$!JWzM|ZQz{D#F|$%x#Ph~d8NY1CHzvHb??;TvBUFo*1D$Vl}7_pb5o1s zOH-3H`mwtYwk$18Thoj4v(}cCKUcOMr2}YTp=GhvVf-@SIDQ4t!udFQ9I|VU;Xa4sy}ulY z1Xa|}zVG?XIKY`XwHdL1Iv=a)++V`zvWqrV^75-z`nE3wF26Am6BNqwg z5Skj-xxwkH7Z*%34@W!@6dFLtBm}K8%p<&vm-7l;A!3nAIMtX%-11Zp_ONN8_(h=6*~BO-ie=I>SD^o_}s+QjBv zXwa!~YIb4iUUhM5>7J>nC3WiFsmA%~Y310|_~OJNabo_UGBba0{`itUIe*VwL3-J~_EKb?jKdbzIadnQL7JVKJFd-+AD*E*`QZ5RgHtY02_7#M zVWrutL8X%>+j#7*y} ztvtJBq<_@1w!g&OySC?*J^M!TnTmYf=-9M%>XwnbkQo2Z8rbT@MEpQSO--JS=2>sN z?;PIIJJ!>f=jpsPHr<#P($=Jv|N2l~O71&X$Bo|piT>TUjnu?zDo%}9`JthantX1w z!pe70sbh51I$LZsKAEq>qtcy~Z>IWYYWek{5et(zHEvNR;uxM-RA*2rOQq~s#TfGM zD)PKxbTpo4LnBk8qm6knZS`AuDKU--$-P4(c{$#bSK>XG{%D?$HRh#s9P_d!&&mgS zEUGEKoX)84pU8{#HF(}@ow810?6b{s0?XPpGB#8(zI${eK8o7ho)J`4(4tCjZp_Q+ zyxQAv4y?cije^^FPaGhQ_l)O-1Jijvf${TlePdotTQo^W@5D!`m7-I=O@Se8uRWn7GPO< zXYV#z7Ygy7(Y%Z9cH_>4yT-f+!+33wSb)F;rk!{9j#;P1th^gQYs`Do+lNQaN|Sw~ zwRzW6{6u5km)^c>Wc!}t!-^U_57^KB>9fq!d&|gKj|clV-jnw<5Rw9Qd(JxP zP3(l27#cZCmDArRK;6BxkVkHBZ|G*eF@w%=0fDJs$SRf7% z_vTsmIfNQ)YzU;9jh*!?4f(}} ziWtBY#M%N4jrmaeET^jpIMX$pJ}c5Sl0GZZwJd#BrfYfntU}j{^jVdzmFcq@U8~Y( zb-G5=EbnUg4JJ~Ji9|7gg^AF$1{0xc3=^Si9222yEha+O1SUe)Bql=F6edE~I!uJF zYtmMijpy}gtlv9k^%5*-)5dYluOXyrNayPt^7SB>G%$4lD0M@6iH~>0iC6v77QmOr zeBloI+O*YX&$AhAc>nv!6x& zR)mBq+jJ$*~(aOq?5i#d> zjhq)O(W*EvB*pU49%67BSS8+vH^#Ssw6KfU(+P;0HxxO!&^tC6&x^g|li)Ez?|22u zW1}F<_x2gbl)!c4TgE#o;&{UrtP2;=?frYor$r23WT>wpo{W!0IdM#^9e1Kj?yS3g187-g$Q8ql%WOYIC z(TqzEG1yg*D+%1)gsN51THL7jBiC5Gv@@7vW4^OAgWi>`86(;I-X*SV<%V?JY9gp@ z0oUysZ90pf?>8kL}hgU9W4?ozAy4tkhgL=%#c&+i(gvM96Xq4o53u=pjR<Xv(hxE%txF^DW@DJQ&Eg zpv^!!pTT7-L9HKfwYETJt-$G~G@)aDE5N!feU7osC~QZ8Q(-WDj@wUmpkP1QNo{&i zbu+c0!VtBg!Y*n9~dIV(=~olVl+m$V1r0 + + +Copyright (C) 2019 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/priv/static/font/fontello.1575662648966.ttf b/priv/static/font/fontello.1575662648966.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ec67a3d00aafa0a7181035459a528ccc5a30fe99 GIT binary patch literal 24460 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 N-MsX12nx8`{~yr1tZD!N literal 0 HcmV?d00001 diff --git a/priv/static/font/fontello.1575662648966.woff b/priv/static/font/fontello.1575662648966.woff new file mode 100644 index 0000000000000000000000000000000000000000..feee99308142ce9baac531d51fa91f6e182f0c65 GIT binary patch literal 14832 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 C)+MI^ literal 0 HcmV?d00001 diff --git a/priv/static/static/fontello.1580232989700.css b/priv/static/fontello.1575660578688.css similarity index 75% rename from priv/static/static/fontello.1580232989700.css rename to priv/static/fontello.1575660578688.css index a9cbcb04d..f232f5600 100644 --- a/priv/static/static/fontello.1580232989700.css +++ b/priv/static/fontello.1575660578688.css @@ -1,11 +1,11 @@ @font-face { font-family: "Icons"; - src: url("./font/fontello.1580232989700.eot"); - src: url("./font/fontello.1580232989700.eot") format("embedded-opentype"), - url("./font/fontello.1580232989700.woff2") format("woff2"), - url("./font/fontello.1580232989700.woff") format("woff"), - url("./font/fontello.1580232989700.ttf") format("truetype"), - url("./font/fontello.1580232989700.svg") format("svg"); + src: url("./font/fontello.1575660578688.eot"); + src: url("./font/fontello.1575660578688.eot") format("embedded-opentype"), + url("./font/fontello.1575660578688.woff2") format("woff2"), + url("./font/fontello.1575660578688.woff") format("woff"), + url("./font/fontello.1575660578688.ttf") format("truetype"), + url("./font/fontello.1575660578688.svg") format("svg"); font-weight: normal; font-style: normal; } @@ -29,8 +29,6 @@ [class*=" icon-"]::before { -moz-osx-font-smoothing: grayscale; } -.icon-spin4::before { content: "\e834"; } - .icon-cancel::before { content: "\e800"; } .icon-upload::before { content: "\e801"; } @@ -105,7 +103,9 @@ .icon-play-circled::before { content: "\f144"; } .icon-pencil::before { content: "\e818"; } -.icon-chart-bar::before { content: "\e81b"; } +.icon-spin4::before { content: "\e834"; } + +.icon-verified::before { content: "\e81b"; } .icon-smile::before { content: "\f118"; } @@ -119,18 +119,28 @@ .icon-ellipsis::before { content: "\f141"; } .icon-bell-ringing-o::before { content: "\e810"; } -.icon-zoom-in::before { content: "\e81c"; } - -.icon-gauge::before { content: "\f0e4"; } - .icon-users::before { content: "\e81d"; } -.icon-info-circled::before { content: "\e81f"; } +.icon-address-book::before { content: "\e81e"; } + +.icon-cog-alt::before { content: "\e81f"; } + +.icon-apple::before { content: "\f179"; } + +.icon-android::before { content: "\f17b"; } .icon-home-2::before { content: "\e821"; } -.icon-chat::before { content: "\e81e"; } +.icon-hashtag::before { content: "\f292"; } -.icon-login::before { content: "\e820"; } +.icon-quote-right::before { content: "\f10e"; } -.icon-arrow-curved::before { content: "\e822"; } +.icon-laptop::before { content: "\f109"; } + +.icon-chart-bar::before { content: "\e81c"; } + +.icon-zoom-in::before { content: "\e820"; } + +.icon-gauge::before { content: "\f0e4"; } + +.icon-paper-plane-empty::before { content: "\f1d9"; } diff --git a/priv/static/fontello.1575662648966.css b/priv/static/fontello.1575662648966.css new file mode 100644 index 000000000..a47f73e3a --- /dev/null +++ b/priv/static/fontello.1575662648966.css @@ -0,0 +1,146 @@ +@font-face { + font-family: "Icons"; + src: url("./font/fontello.1575662648966.eot"); + src: url("./font/fontello.1575662648966.eot") format("embedded-opentype"), + url("./font/fontello.1575662648966.woff2") format("woff2"), + url("./font/fontello.1575662648966.woff") format("woff"), + url("./font/fontello.1575662648966.ttf") format("truetype"), + url("./font/fontello.1575662648966.svg") format("svg"); + font-weight: normal; + font-style: normal; +} + +[class^="icon-"]::before, +[class*=" icon-"]::before { + font-family: "Icons"; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + font-variant: normal; + text-transform: none; + line-height: 1em; + margin-left: .2em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-cancel::before { content: "\e800"; } + +.icon-upload::before { content: "\e801"; } + +.icon-spin3::before { content: "\e832"; } + +.icon-reply::before { content: "\f112"; } + +.icon-star::before { content: "\e802"; } + +.icon-star-empty::before { content: "\e803"; } + +.icon-retweet::before { content: "\e804"; } + +.icon-eye-off::before { content: "\e805"; } + +.icon-binoculars::before { content: "\f1e5"; } + +.icon-cog::before { content: "\e807"; } + +.icon-user-plus::before { content: "\f234"; } + +.icon-menu::before { content: "\f0c9"; } + +.icon-logout::before { content: "\e808"; } + +.icon-down-open::before { content: "\e809"; } + +.icon-attach::before { content: "\e80a"; } + +.icon-link-ext::before { content: "\f08e"; } + +.icon-link-ext-alt::before { content: "\f08f"; } + +.icon-picture::before { content: "\e80b"; } + +.icon-video::before { content: "\e80c"; } + +.icon-right-open::before { content: "\e80d"; } + +.icon-left-open::before { content: "\e80e"; } + +.icon-up-open::before { content: "\e80f"; } + +.icon-comment-empty::before { content: "\f0e5"; } + +.icon-mail-alt::before { content: "\f0e0"; } + +.icon-lock::before { content: "\e811"; } + +.icon-lock-open-alt::before { content: "\f13e"; } + +.icon-globe::before { content: "\e812"; } + +.icon-brush::before { content: "\e813"; } + +.icon-search::before { content: "\e806"; } + +.icon-adjust::before { content: "\e816"; } + +.icon-thumbs-up-alt::before { content: "\f164"; } + +.icon-attention::before { content: "\e814"; } + +.icon-plus-squared::before { content: "\f0fe"; } + +.icon-plus::before { content: "\e815"; } + +.icon-edit::before { content: "\e817"; } + +.icon-play-circled::before { content: "\f144"; } + +.icon-pencil::before { content: "\e818"; } + +.icon-spin4::before { content: "\e834"; } + +.icon-verified::before { content: "\e81b"; } + +.icon-smile::before { content: "\f118"; } + +.icon-bell-alt::before { content: "\f0f3"; } + +.icon-wrench::before { content: "\e81a"; } + +.icon-pin::before { content: "\e819"; } + +.icon-ellipsis::before { content: "\f141"; } + +.icon-bell-ringing-o::before { content: "\e810"; } + +.icon-users::before { content: "\e81d"; } + +.icon-address-book::before { content: "\e81e"; } + +.icon-cog-alt::before { content: "\e81f"; } + +.icon-apple::before { content: "\f179"; } + +.icon-android::before { content: "\f17b"; } + +.icon-home-2::before { content: "\e821"; } + +.icon-hashtag::before { content: "\f292"; } + +.icon-quote-right::before { content: "\f10e"; } + +.icon-laptop::before { content: "\f109"; } + +.icon-chart-bar::before { content: "\e81c"; } + +.icon-zoom-in::before { content: "\e820"; } + +.icon-gauge::before { content: "\f0e4"; } + +.icon-paper-plane-empty::before { content: "\f1d9"; } diff --git a/priv/static/index.html b/priv/static/index.html index 2fc0d5349..bf7ee958b 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -Pleroma
\ No newline at end of file +Pleroma
\ No newline at end of file diff --git a/priv/static/static/font/fontello.1580232989700.woff2 b/priv/static/static/font/fontello.1580232989700.woff2 deleted file mode 100644 index 73acac54f7ba19f29227791b351fef647555a96d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11280 zcmV+rEbr5IPew8T0RR9104xvy4*&oF095n<04uiu0RR9100000000000000000000 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 G8m|CpK+J#u diff --git a/priv/static/static/font/fontello.1580232989700.eot b/priv/static/static/font/fontello.1581007281335.eot similarity index 99% rename from priv/static/static/font/fontello.1580232989700.eot rename to priv/static/static/font/fontello.1581007281335.eot index 6be901301d56667e772f168e5f5e43da89777022..3aae7d472e4543a5d2974eec6dff2b2bf72b5967 100644 GIT binary patch delta 57 zcmbQUnsLr*#tAmeYhIX6w134Mpw6~ghq2O8+VsVmcz&C&4BX5w7=Ykb;>R^Gdh-FN GJ3;_zVim~% delta 57 zcmbQUnsLr*#tAmeS2R*5+P`A9X#2EThq2O8I#uIJJipCX25#mT3_x%zOmPv6-h9C6 Gjt~H4_7q|O diff --git a/priv/static/static/font/fontello.1580232989700.svg b/priv/static/static/font/fontello.1581007281335.svg similarity index 100% rename from priv/static/static/font/fontello.1580232989700.svg rename to priv/static/static/font/fontello.1581007281335.svg diff --git a/priv/static/static/font/fontello.1580232989700.ttf b/priv/static/static/font/fontello.1581007281335.ttf similarity index 99% rename from priv/static/static/font/fontello.1580232989700.ttf rename to priv/static/static/font/fontello.1581007281335.ttf index 51d3f1e08c6841aceb1fb85ffb06bd587f59f1f1..d3d19affe0a60063a3fb940129f022b2e1bac27f 100644 GIT binary patch delta 49 ycmeyelJUz*#tDtg0qSfUTPhu;O<$~u=ePOFz|H)E0SIm-ep~~iH%mC(5dr|>4ie!2 delta 49 ycmeyelJUz*#tDtg7Hyw4wp2Pwr)pe@=ePOFz|H)E0SIn|DK3K1nX>E-5oJ_Zjj0HeVUInO^`!7;YtgTmz&3fBm1tQo)=K T`DfkXjnS`FB4O$UTa41oq4gGyCLb&msq zvU5aKt2){LACHqGs!k2VzjVUIp1i^n-koFY#=2`IqVYB_Y=m|;D3Z+lg*)YYEUD>9^NUa& zYTLv00mwhw6acInxbg7s?RU?9Z)<&o6AFMb4lpy-NtD`ra*BT0{xVEVY;l0eoSAgT zQpo{S649zMi&etd!@7&3{guS-7l#OtS! z(k%?;hiMv0-s=0g2M)-~<$D0$^0CLi;{ZjxhAXm>bZPaa}cmJO!L_|*X#QFxP1j7lbOs-HG8X23InweXW6s=Ng zw3b#nYa3fTdk06xsWT`W{Ip?{yJ7GSjSRAhkCH>lqZCkzC?%9KN(H5gQbVbuG*Fr- zEtED&2c?VBL+PUoP=+WYlrhQ#Wr{LGnWHRFmMANfHOdBMi?T!6ugbE(I9Qe#M=M$w zCo3F`vlWAkixp29S1W#H+^m>n+^v{mJgk^yJgvlFysYHU__ES6#Azg$ z@O+?sBSH`ff;KH62+>(0=&Q$;1Q>$%7o_oH8`BXxgU;0_FDfQqz|DmhvS7ek{+ik+ zpwt*kL&ly?0aG-mb5L+!hcWV9IzXx7OZ&>TGN%=&cro}R-VT_VexPZvR@~)F!a$O^hmp)#YbQQdY zM98jvqs@$*QMYHwfikYNK=1-mDHEd#f+;7GW{m1<19GK}WWWULRUG`I^2{nw5sRU^ z5SwsUarAIqju%RsDx!vO&S!^s`#Q{%u);M0qFCnDG$})0yPD)BO1)8=>Y2S z(S(0gp66itavme{6RC}YcGBbc#uxf)!fbxxuivHNb?v1;zP>g_>>AUhjVPcKZl!K% zAtpzihuKt#xuSk0nkM2-L5X#`V9q0bmhv5j3`^0P%va8Ufbhw_s}EGOM~&c2n`OEq zi??V2Flj?1q0^%3ZEq7M3dr!rQx_aMR4waM8Hlw*T$S`&znBy)^j&^Zj6LVK74tk; zno+>CVrfCa6nIL&N+n>U6zr4(2bF@8%D_eC;HHYeg9&)BeGtqKL)FgpPlFfX3EBa{{`6}&R|E^ zN>;bamU&F%+SQ$@;L0dx0ej>=t?9%@vkLibT)i`|$ zPPx_OL=HlJnA(k`lo1^jOj?W2LKufi3DfW=$yzm(Fg~<}Ush6pG-@}k8Ks2ipUUr2T zNKi`G%k0TJ#M~7ee1$C)LvrM4Act=wwcEW8&1!m)ZkKBvs_4MFy74Foow?J|?zWv@ zW*NOEc3lxH=wA6(eW*%OH!IZI))g*hXhYsU_&s;PvUe!EY(vGf@3F-wkCkiTHl(bW z%*!+GA~C~$QnT<@)U~cgd#gpBe6SrG{zvpUPfT6tly~0JH6yexN+!VpvdeoK~e3COZSZqo~l$!9*f|y2^?e;m0lR*^nlKo|(YmpH`b&&>@mFA4NT6YLa; z4~0@n@#F;g*oSgCtgwHCEhI1G^Ju-4QAhjJWRX0$y-bfJPG*pPg}W$s!i9LX>$ow= zm=0Sp@AQh;_C|9PX)d`I%r=unwjgUg`RYjK2~ zcr)x?+|HACiQ9>ayjlJFnzM)|9A>

Dk;o(|4(^WDLbFexho=oT(IQ=4W3DZ>Wu{ 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 yC7U$P$;0}s tag\n\n// load the styles\nvar content = require(\"!!../../../node_modules/css-loader/index.js?minimize!../../../node_modules/vue-loader/lib/style-compiler/index.js?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!../../../node_modules/sass-loader/lib/loader.js!../../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./sticker_picker.vue\");\nif(typeof content === 'string') content = [[module.id, content, '']];\nif(content.locals) module.exports = content.locals;\n// add the styles to the DOM\nvar add = require(\"!../../../node_modules/vue-style-loader/lib/addStylesClient.js\").default\nvar update = add(\"cc6cdea4\", content, true, {});","exports = module.exports = require(\"../../../node_modules/css-loader/lib/css-base.js\")(false);\n// imports\n\n\n// module\nexports.push([module.id, \".sticker-picker{width:100%}.sticker-picker .contents{min-height:250px}.sticker-picker .contents .sticker-picker-content{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0 4px}.sticker-picker .contents .sticker-picker-content .sticker{display:-ms-flexbox;display:flex;-ms-flex:1 1 auto;flex:1 1 auto;margin:4px;width:56px;height:56px}.sticker-picker .contents .sticker-picker-content .sticker img{height:100%}.sticker-picker .contents .sticker-picker-content .sticker img:hover{filter:drop-shadow(0 0 5px var(--link,#d8a070))}\", \"\"]);\n\n// exports\n","/* eslint-env browser */\nimport statusPosterService from '../../services/status_poster/status_poster.service.js'\nimport TabSwitcher from '../tab_switcher/tab_switcher.js'\n\nconst StickerPicker = {\n components: {\n TabSwitcher\n },\n data () {\n return {\n meta: {\n stickers: []\n },\n path: ''\n }\n },\n computed: {\n pack () {\n return this.$store.state.instance.stickers || []\n }\n },\n methods: {\n clear () {\n this.meta = {\n stickers: []\n }\n },\n pick (sticker, name) {\n const store = this.$store\n // TODO remove this workaround by finding a way to bypass reuploads\n fetch(sticker)\n .then((res) => {\n res.blob().then((blob) => {\n var file = new File([blob], name, { mimetype: 'image/png' })\n var formData = new FormData()\n formData.append('file', file)\n statusPosterService.uploadMedia({ store, formData })\n .then((fileData) => {\n this.$emit('uploaded', fileData)\n this.clear()\n }, (error) => {\n console.warn(\"Can't attach sticker\")\n console.warn(error)\n this.$emit('upload-failed', 'default')\n })\n })\n })\n }\n }\n}\n\nexport default StickerPicker\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./sticker_picker.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./sticker_picker.js\"\nimport __vue_script__ from \"!!babel-loader!./sticker_picker.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-772c11f7\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./sticker_picker.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"sticker-picker\"},[_c('tab-switcher',{staticClass:\"tab-switcher\",attrs:{\"render-only-focused\":true,\"scrollable-tabs\":\"\"}},_vm._l((_vm.pack),function(stickerpack){return _c('div',{key:stickerpack.path,staticClass:\"sticker-picker-content\",attrs:{\"image-tooltip\":stickerpack.meta.title,\"image\":stickerpack.path + stickerpack.meta.tabIcon}},_vm._l((stickerpack.meta.stickers),function(sticker){return _c('div',{key:sticker,staticClass:\"sticker\",on:{\"click\":function($event){$event.stopPropagation();$event.preventDefault();_vm.pick(stickerpack.path + sticker, stickerpack.meta.title)}}},[_c('img',{attrs:{\"src\":stickerpack.path + sticker}})])}),0)}),0)],1)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///./src/components/sticker_picker/sticker_picker.vue?d6cd","webpack:///./src/components/sticker_picker/sticker_picker.vue?d5ea","webpack:///./src/components/sticker_picker/sticker_picker.js","webpack:///./src/components/sticker_picker/sticker_picker.vue","webpack:///./src/components/sticker_picker/sticker_picker.vue?53f6"],"names":["content","__webpack_require__","module","i","locals","exports","add","default","push","StickerPicker","components","TabSwitcher","data","meta","stickers","path","computed","pack","this","$store","state","instance","methods","clear","pick","sticker","name","_this","store","fetch","then","res","blob","file","File","mimetype","formData","FormData","append","statusPosterService","uploadMedia","fileData","$emit","error","console","warn","__vue_styles__","context","Component","Object","component_normalizer","sticker_picker","_vm","_h","$createElement","_c","_self","staticClass","attrs","render-only-focused","scrollable-tabs","_l","stickerpack","key","image-tooltip","title","image","tabIcon","on","click","$event","stopPropagation","preventDefault","src","__webpack_exports__"],"mappings":"6EAGA,IAAAA,EAAcC,EAAQ,KACtB,iBAAAD,MAAA,EAA4CE,EAAAC,EAASH,EAAA,MACrDA,EAAAI,SAAAF,EAAAG,QAAAL,EAAAI,SAGAE,EADUL,EAAQ,GAAgEM,SAClF,WAAAP,GAAA,4BCRAE,EAAAG,QAA2BJ,EAAQ,EAARA,EAA0D,IAKrFO,KAAA,CAAcN,EAAAC,EAAS,0iBAA0iB,0DC8CljBM,EA/CO,CACpBC,WAAY,CACVC,qBAEFC,KAJoB,WAKlB,MAAO,CACLC,KAAM,CACJC,SAAU,IAEZC,KAAM,KAGVC,SAAU,CACRC,KADQ,WAEN,OAAOC,KAAKC,OAAOC,MAAMC,SAASP,UAAY,KAGlDQ,QAAS,CACPC,MADO,WAELL,KAAKL,KAAO,CACVC,SAAU,KAGdU,KANO,SAMDC,EAASC,GAAM,IAAAC,EAAAT,KACbU,EAAQV,KAAKC,OAEnBU,MAAMJ,GACHK,KAAK,SAACC,GACLA,EAAIC,OAAOF,KAAK,SAACE,GACf,IAAIC,EAAO,IAAIC,KAAK,CAACF,GAAON,EAAM,CAAES,SAAU,cAC1CC,EAAW,IAAIC,SACnBD,EAASE,OAAO,OAAQL,GACxBM,IAAoBC,YAAY,CAAEZ,QAAOQ,aACtCN,KAAK,SAACW,GACLd,EAAKe,MAAM,WAAYD,GACvBd,EAAKJ,SACJ,SAACoB,GACFC,QAAQC,KAAK,wBACbD,QAAQC,KAAKF,GACbhB,EAAKe,MAAM,gBAAiB,2BCnC5C,IAEAI,EAVA,SAAAC,GACE9C,EAAQ,MAeV+C,EAAgBC,OAAAC,EAAA,EAAAD,CACdE,ECjBF,WAA0B,IAAAC,EAAAlC,KAAamC,EAAAD,EAAAE,eAA0BC,EAAAH,EAAAI,MAAAD,IAAAF,EAAwB,OAAAE,EAAA,OAAiBE,YAAA,kBAA6B,CAAAF,EAAA,gBAAqBE,YAAA,eAAAC,MAAA,CAAkCC,uBAAA,EAAAC,kBAAA,KAAiDR,EAAAS,GAAAT,EAAA,cAAAU,GAAyC,OAAAP,EAAA,OAAiBQ,IAAAD,EAAA/C,KAAA0C,YAAA,yBAAAC,MAAA,CAAiEM,gBAAAF,EAAAjD,KAAAoD,MAAAC,MAAAJ,EAAA/C,KAAA+C,EAAAjD,KAAAsD,UAA4Ff,EAAAS,GAAAC,EAAAjD,KAAA,kBAAAY,GAAsD,OAAA8B,EAAA,OAAiBQ,IAAAtC,EAAAgC,YAAA,UAAAW,GAAA,CAAsCC,MAAA,SAAAC,GAAyBA,EAAAC,kBAAyBD,EAAAE,iBAAwBpB,EAAA5B,KAAAsC,EAAA/C,KAAAU,EAAAqC,EAAAjD,KAAAoD,UAA+D,CAAAV,EAAA,OAAYG,MAAA,CAAOe,IAAAX,EAAA/C,KAAAU,SAAsC,KAAK,QAC1vB,IDOA,EAaAqB,EATA,KAEA,MAYe4B,EAAA,QAAA1B,EAAiB","file":"static/js/2.9be9f9ec29f7536c73c3.js","sourcesContent":["// style-loader: Adds some css to the DOM by adding a \n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./checkbox.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!../../../node_modules/vue-loader/lib/selector?type=script&index=0!./checkbox.vue\"\nimport __vue_script__ from \"!!babel-loader!../../../node_modules/vue-loader/lib/selector?type=script&index=0!./checkbox.vue\"\n/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-58c9b3c4\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./checkbox.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"checkbox\",class:{ disabled: _vm.disabled, indeterminate: _vm.indeterminate }},[_c('input',{attrs:{\"type\":\"checkbox\",\"disabled\":_vm.disabled},domProps:{\"checked\":_vm.checked,\"indeterminate\":_vm.indeterminate},on:{\"change\":function($event){_vm.$emit('change', $event.target.checked)}}}),_vm._v(\" \"),_c('i',{staticClass:\"checkbox-indicator\"}),_vm._v(\" \"),(!!_vm.$slots.default)?_c('span',{staticClass:\"label\"},[_vm._t(\"default\")],2):_vm._e()])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","// TODO this func might as well take the entire file and use its mimetype\n// or the entire service could be just mimetype service that only operates\n// on mimetypes and not files. Currently the naming is confusing.\nconst fileType = mimetype => {\n if (mimetype.match(/text\\/html/)) {\n return 'html'\n }\n\n if (mimetype.match(/image/)) {\n return 'image'\n }\n\n if (mimetype.match(/video/)) {\n return 'video'\n }\n\n if (mimetype.match(/audio/)) {\n return 'audio'\n }\n\n return 'unknown'\n}\n\nconst fileMatchesSomeType = (types, file) =>\n types.some(type => fileType(file.mimetype) === type)\n\nconst fileTypeService = {\n fileType,\n fileMatchesSomeType\n}\n\nexport default fileTypeService\n","const DialogModal = {\n props: {\n darkOverlay: {\n default: true,\n type: Boolean\n },\n onCancel: {\n default: () => {},\n type: Function\n }\n }\n}\n\nexport default DialogModal\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./dialog_modal.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./dialog_modal.js\"\nimport __vue_script__ from \"!!babel-loader!./dialog_modal.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-eafd78a6\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./dialog_modal.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('span',{class:{ 'dark-overlay': _vm.darkOverlay },on:{\"click\":function($event){if($event.target !== $event.currentTarget){ return null; }$event.stopPropagation();_vm.onCancel()}}},[_c('div',{staticClass:\"dialog-modal panel panel-default\",on:{\"click\":function($event){$event.stopPropagation();}}},[_c('div',{staticClass:\"panel-heading dialog-modal-heading\"},[_c('div',{staticClass:\"title\"},[_vm._t(\"header\")],2)]),_vm._v(\" \"),_c('div',{staticClass:\"dialog-modal-content\"},[_vm._t(\"default\")],2),_vm._v(\" \"),_c('div',{staticClass:\"dialog-modal-footer user-interactions panel-footer\"},[_vm._t(\"footer\")],2)])])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import DialogModal from '../dialog_modal/dialog_modal.vue'\n\nconst FORCE_NSFW = 'mrf_tag:media-force-nsfw'\nconst STRIP_MEDIA = 'mrf_tag:media-strip'\nconst FORCE_UNLISTED = 'mrf_tag:force-unlisted'\nconst DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription'\nconst DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'\nconst SANDBOX = 'mrf_tag:sandbox'\nconst QUARANTINE = 'mrf_tag:quarantine'\n\nconst ModerationTools = {\n props: [\n 'user'\n ],\n data () {\n return {\n showDropDown: false,\n tags: {\n FORCE_NSFW,\n STRIP_MEDIA,\n FORCE_UNLISTED,\n DISABLE_REMOTE_SUBSCRIPTION,\n DISABLE_ANY_SUBSCRIPTION,\n SANDBOX,\n QUARANTINE\n },\n showDeleteUserDialog: false\n }\n },\n components: {\n DialogModal\n },\n computed: {\n tagsSet () {\n return new Set(this.user.tags)\n },\n hasTagPolicy () {\n return this.$store.state.instance.tagPolicyAvailable\n }\n },\n methods: {\n hasTag (tagName) {\n return this.tagsSet.has(tagName)\n },\n toggleTag (tag) {\n const store = this.$store\n if (this.tagsSet.has(tag)) {\n store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {\n if (!response.ok) { return }\n store.commit('untagUser', { user: this.user, tag })\n })\n } else {\n store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {\n if (!response.ok) { return }\n store.commit('tagUser', { user: this.user, tag })\n })\n }\n },\n toggleRight (right) {\n const store = this.$store\n if (this.user.rights[right]) {\n store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => {\n if (!response.ok) { return }\n store.commit('updateRight', { user: this.user, right, value: false })\n })\n } else {\n store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => {\n if (!response.ok) { return }\n store.commit('updateRight', { user: this.user, right, value: true })\n })\n }\n },\n toggleActivationStatus () {\n this.$store.dispatch('toggleActivationStatus', { user: this.user })\n },\n deleteUserDialog (show) {\n this.showDeleteUserDialog = show\n },\n deleteUser () {\n const store = this.$store\n const user = this.user\n const { id, name } = user\n store.state.api.backendInteractor.deleteUser({ user })\n .then(e => {\n this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)\n const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'\n const isTargetUser = this.$route.params.name === name || this.$route.params.id === id\n if (isProfile && isTargetUser) {\n window.history.back()\n }\n })\n }\n }\n}\n\nexport default ModerationTools\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./moderation_tools.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./moderation_tools.js\"\nimport __vue_script__ from \"!!babel-loader!./moderation_tools.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-756a6c4e\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./moderation_tools.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('v-popover',{staticClass:\"moderation-tools-popover\",attrs:{\"trigger\":\"click\",\"placement\":\"bottom-end\"},on:{\"show\":function($event){_vm.showDropDown = true},\"hide\":function($event){_vm.showDropDown = false}}},[_c('div',{attrs:{\"slot\":\"popover\"},slot:\"popover\"},[_c('div',{staticClass:\"dropdown-menu\"},[(_vm.user.is_local)?_c('span',[_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleRight(\"admin\")}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t(!!_vm.user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin'))+\"\\n \")]),_vm._v(\" \"),_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleRight(\"moderator\")}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t(!!_vm.user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator'))+\"\\n \")]),_vm._v(\" \"),_c('div',{staticClass:\"dropdown-divider\",attrs:{\"role\":\"separator\"}})]):_vm._e(),_vm._v(\" \"),_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleActivationStatus()}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t(!!_vm.user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account'))+\"\\n \")]),_vm._v(\" \"),_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.deleteUserDialog(true)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.delete_account'))+\"\\n \")]),_vm._v(\" \"),(_vm.hasTagPolicy)?_c('div',{staticClass:\"dropdown-divider\",attrs:{\"role\":\"separator\"}}):_vm._e(),_vm._v(\" \"),(_vm.hasTagPolicy)?_c('span',[_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleTag(_vm.tags.FORCE_NSFW)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.force_nsfw'))+\"\\n \"),_c('span',{staticClass:\"menu-checkbox\",class:{ 'menu-checkbox-checked': _vm.hasTag(_vm.tags.FORCE_NSFW) }})]),_vm._v(\" \"),_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleTag(_vm.tags.STRIP_MEDIA)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.strip_media'))+\"\\n \"),_c('span',{staticClass:\"menu-checkbox\",class:{ 'menu-checkbox-checked': _vm.hasTag(_vm.tags.STRIP_MEDIA) }})]),_vm._v(\" \"),_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleTag(_vm.tags.FORCE_UNLISTED)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.force_unlisted'))+\"\\n \"),_c('span',{staticClass:\"menu-checkbox\",class:{ 'menu-checkbox-checked': _vm.hasTag(_vm.tags.FORCE_UNLISTED) }})]),_vm._v(\" \"),_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleTag(_vm.tags.SANDBOX)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.sandbox'))+\"\\n \"),_c('span',{staticClass:\"menu-checkbox\",class:{ 'menu-checkbox-checked': _vm.hasTag(_vm.tags.SANDBOX) }})]),_vm._v(\" \"),(_vm.user.is_local)?_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleTag(_vm.tags.DISABLE_REMOTE_SUBSCRIPTION)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.disable_remote_subscription'))+\"\\n \"),_c('span',{staticClass:\"menu-checkbox\",class:{ 'menu-checkbox-checked': _vm.hasTag(_vm.tags.DISABLE_REMOTE_SUBSCRIPTION) }})]):_vm._e(),_vm._v(\" \"),(_vm.user.is_local)?_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleTag(_vm.tags.DISABLE_ANY_SUBSCRIPTION)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.disable_any_subscription'))+\"\\n \"),_c('span',{staticClass:\"menu-checkbox\",class:{ 'menu-checkbox-checked': _vm.hasTag(_vm.tags.DISABLE_ANY_SUBSCRIPTION) }})]):_vm._e(),_vm._v(\" \"),(_vm.user.is_local)?_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){_vm.toggleTag(_vm.tags.QUARANTINE)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.quarantine'))+\"\\n \"),_c('span',{staticClass:\"menu-checkbox\",class:{ 'menu-checkbox-checked': _vm.hasTag(_vm.tags.QUARANTINE) }})]):_vm._e()]):_vm._e()])]),_vm._v(\" \"),_c('button',{staticClass:\"btn btn-default btn-block\",class:{ pressed: _vm.showDropDown }},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.moderation'))+\"\\n \")])]),_vm._v(\" \"),_c('portal',{attrs:{\"to\":\"modal\"}},[(_vm.showDeleteUserDialog)?_c('DialogModal',{attrs:{\"on-cancel\":_vm.deleteUserDialog.bind(this, false)}},[_c('template',{slot:\"header\"},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.delete_user'))+\"\\n \")]),_vm._v(\" \"),_c('p',[_vm._v(_vm._s(_vm.$t('user_card.admin_menu.delete_user_confirmation')))]),_vm._v(\" \"),_c('template',{slot:\"footer\"},[_c('button',{staticClass:\"btn btn-default\",on:{\"click\":function($event){_vm.deleteUserDialog(false)}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('general.cancel'))+\"\\n \")]),_vm._v(\" \"),_c('button',{staticClass:\"btn btn-default danger\",on:{\"click\":function($event){_vm.deleteUser()}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.admin_menu.delete_user'))+\"\\n \")])])],2):_vm._e()],1)],1)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import ProgressButton from '../progress_button/progress_button.vue'\n\nconst AccountActions = {\n props: [\n 'user'\n ],\n data () {\n return { }\n },\n components: {\n ProgressButton\n },\n methods: {\n showRepeats () {\n this.$store.dispatch('showReblogs', this.user.id)\n },\n hideRepeats () {\n this.$store.dispatch('hideReblogs', this.user.id)\n },\n blockUser () {\n this.$store.dispatch('blockUser', this.user.id)\n },\n unblockUser () {\n this.$store.dispatch('unblockUser', this.user.id)\n },\n reportUser () {\n this.$store.dispatch('openUserReportingModal', this.user.id)\n }\n }\n}\n\nexport default AccountActions\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./account_actions.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./account_actions.js\"\nimport __vue_script__ from \"!!babel-loader!./account_actions.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-3967dbf7\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./account_actions.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"account-actions\"},[_c('v-popover',{staticClass:\"account-tools-popover\",attrs:{\"trigger\":\"click\",\"container\":false,\"placement\":\"bottom-end\",\"offset\":5}},[_c('div',{attrs:{\"slot\":\"popover\"},slot:\"popover\"},[_c('div',{staticClass:\"dropdown-menu\"},[(_vm.user.following)?[(_vm.user.showing_reblogs)?_c('button',{staticClass:\"btn btn-default dropdown-item\",on:{\"click\":_vm.hideRepeats}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.hide_repeats'))+\"\\n \")]):_vm._e(),_vm._v(\" \"),(!_vm.user.showing_reblogs)?_c('button',{staticClass:\"btn btn-default dropdown-item\",on:{\"click\":_vm.showRepeats}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.show_repeats'))+\"\\n \")]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"dropdown-divider\",attrs:{\"role\":\"separator\"}})]:_vm._e(),_vm._v(\" \"),(_vm.user.statusnet_blocking)?_c('button',{staticClass:\"btn btn-default btn-block dropdown-item\",on:{\"click\":_vm.unblockUser}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.unblock'))+\"\\n \")]):_c('button',{staticClass:\"btn btn-default btn-block dropdown-item\",on:{\"click\":_vm.blockUser}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.block'))+\"\\n \")]),_vm._v(\" \"),_c('button',{staticClass:\"btn btn-default btn-block dropdown-item\",on:{\"click\":_vm.reportUser}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.report'))+\"\\n \")])],2)]),_vm._v(\" \"),_c('div',{staticClass:\"btn btn-default ellipsis-button\"},[_c('i',{staticClass:\"icon-ellipsis trigger-button\"})])])],1)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import UserAvatar from '../user_avatar/user_avatar.vue'\nimport RemoteFollow from '../remote_follow/remote_follow.vue'\nimport ProgressButton from '../progress_button/progress_button.vue'\nimport FollowButton from '../follow_button/follow_button.vue'\nimport ModerationTools from '../moderation_tools/moderation_tools.vue'\nimport AccountActions from '../account_actions/account_actions.vue'\nimport { hex2rgb } from '../../services/color_convert/color_convert.js'\nimport generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'\nimport { mapGetters } from 'vuex'\n\nexport default {\n props: [\n 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'\n ],\n data () {\n return {\n followRequestInProgress: false,\n betterShadow: this.$store.state.interface.browserSupport.cssFilter\n }\n },\n created () {\n this.$store.dispatch('fetchUserRelationship', this.user.id)\n },\n computed: {\n classes () {\n return [{\n 'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius\n 'user-card-rounded': this.rounded === true, // set border-radius for all sides\n 'user-card-bordered': this.bordered === true // set border for all sides\n }]\n },\n style () {\n const color = this.$store.getters.mergedConfig.customTheme.colors\n ? this.$store.getters.mergedConfig.customTheme.colors.bg // v2\n : this.$store.getters.mergedConfig.colors.bg // v1\n\n if (color) {\n const rgb = (typeof color === 'string') ? hex2rgb(color) : color\n const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`\n\n return {\n backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,\n backgroundImage: [\n `linear-gradient(to bottom, ${tintColor}, ${tintColor})`,\n `url(${this.user.cover_photo})`\n ].join(', ')\n }\n }\n },\n isOtherUser () {\n return this.user.id !== this.$store.state.users.currentUser.id\n },\n subscribeUrl () {\n // eslint-disable-next-line no-undef\n const serverUrl = new URL(this.user.statusnet_profile_url)\n return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`\n },\n loggedIn () {\n return this.$store.state.users.currentUser\n },\n dailyAvg () {\n const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))\n return Math.round(this.user.statuses_count / days)\n },\n userHighlightType: {\n get () {\n const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]\n return (data && data.type) || 'disabled'\n },\n set (type) {\n const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]\n if (type !== 'disabled') {\n this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: (data && data.color) || '#FFFFFF', type })\n } else {\n this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined })\n }\n },\n ...mapGetters(['mergedConfig'])\n },\n userHighlightColor: {\n get () {\n const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]\n return data && data.color\n },\n set (color) {\n this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })\n }\n },\n visibleRole () {\n const rights = this.user.rights\n if (!rights) { return }\n const validRole = rights.admin || rights.moderator\n const roleTitle = rights.admin ? 'admin' : 'moderator'\n return validRole && roleTitle\n },\n hideFollowsCount () {\n return this.isOtherUser && this.user.hide_follows_count\n },\n hideFollowersCount () {\n return this.isOtherUser && this.user.hide_followers_count\n },\n ...mapGetters(['mergedConfig'])\n },\n components: {\n UserAvatar,\n RemoteFollow,\n ModerationTools,\n AccountActions,\n ProgressButton,\n FollowButton\n },\n methods: {\n muteUser () {\n this.$store.dispatch('muteUser', this.user.id)\n },\n unmuteUser () {\n this.$store.dispatch('unmuteUser', this.user.id)\n },\n subscribeUser () {\n return this.$store.dispatch('subscribeUser', this.user.id)\n },\n unsubscribeUser () {\n return this.$store.dispatch('unsubscribeUser', this.user.id)\n },\n setProfileView (v) {\n if (this.switcher) {\n const store = this.$store\n store.commit('setProfileView', { v })\n }\n },\n linkClicked ({ target }) {\n if (target.tagName === 'SPAN') {\n target = target.parentNode\n }\n if (target.tagName === 'A') {\n window.open(target.href, '_blank')\n }\n },\n userProfileLink (user) {\n return generateProfileLink(\n user.id, user.screen_name,\n this.$store.state.instance.restrictedNicknames\n )\n },\n zoomAvatar () {\n const attachment = {\n url: this.user.profile_image_url_original,\n mimetype: 'image'\n }\n this.$store.dispatch('setMedia', [attachment])\n this.$store.dispatch('setCurrent', attachment)\n },\n mentionUser () {\n this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })\n }\n }\n}\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./user_card.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./user_card.js\"\nimport __vue_script__ from \"!!babel-loader!./user_card.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-d0b51e66\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./user_card.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"user-card\",class:_vm.classes},[_c('div',{staticClass:\"background-image\",class:{ 'hide-bio': _vm.hideBio },style:(_vm.style)}),_vm._v(\" \"),_c('div',{staticClass:\"panel-heading\"},[_c('div',{staticClass:\"user-info\"},[_c('div',{staticClass:\"container\"},[(_vm.allowZoomingAvatar)?_c('a',{staticClass:\"user-info-avatar-link\",on:{\"click\":_vm.zoomAvatar}},[_c('UserAvatar',{attrs:{\"better-shadow\":_vm.betterShadow,\"user\":_vm.user}}),_vm._v(\" \"),_vm._m(0)],1):_c('router-link',{attrs:{\"to\":_vm.userProfileLink(_vm.user)}},[_c('UserAvatar',{attrs:{\"better-shadow\":_vm.betterShadow,\"user\":_vm.user}})],1),_vm._v(\" \"),_c('div',{staticClass:\"user-summary\"},[_c('div',{staticClass:\"top-line\"},[(_vm.user.name_html)?_c('div',{staticClass:\"user-name\",attrs:{\"title\":_vm.user.name},domProps:{\"innerHTML\":_vm._s(_vm.user.name_html)}}):_c('div',{staticClass:\"user-name\",attrs:{\"title\":_vm.user.name}},[_vm._v(\"\\n \"+_vm._s(_vm.user.name)+\"\\n \")]),_vm._v(\" \"),(!_vm.isOtherUser)?_c('router-link',{attrs:{\"to\":{ name: 'user-settings' }}},[_c('i',{staticClass:\"button-icon icon-wrench usersettings\",attrs:{\"title\":_vm.$t('tool_tip.user_settings')}})]):_vm._e(),_vm._v(\" \"),(_vm.isOtherUser && !_vm.user.is_local)?_c('a',{attrs:{\"href\":_vm.user.statusnet_profile_url,\"target\":\"_blank\"}},[_c('i',{staticClass:\"icon-link-ext usersettings\"})]):_vm._e(),_vm._v(\" \"),(_vm.isOtherUser && _vm.loggedIn)?_c('AccountActions',{attrs:{\"user\":_vm.user}}):_vm._e()],1),_vm._v(\" \"),_c('div',{staticClass:\"bottom-line\"},[_c('router-link',{staticClass:\"user-screen-name\",attrs:{\"to\":_vm.userProfileLink(_vm.user)}},[_vm._v(\"\\n @\"+_vm._s(_vm.user.screen_name)+\"\\n \")]),_vm._v(\" \"),(!_vm.hideBio && !!_vm.visibleRole)?_c('span',{staticClass:\"alert staff\"},[_vm._v(_vm._s(_vm.visibleRole))]):_vm._e(),_vm._v(\" \"),(_vm.user.locked)?_c('span',[_c('i',{staticClass:\"icon icon-lock\"})]):_vm._e(),_vm._v(\" \"),(!_vm.mergedConfig.hideUserStats && !_vm.hideBio)?_c('span',{staticClass:\"dailyAvg\"},[_vm._v(_vm._s(_vm.dailyAvg)+\" \"+_vm._s(_vm.$t('user_card.per_day')))]):_vm._e()],1)])],1),_vm._v(\" \"),_c('div',{staticClass:\"user-meta\"},[(_vm.user.follows_you && _vm.loggedIn && _vm.isOtherUser)?_c('div',{staticClass:\"following\"},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.follows_you'))+\"\\n \")]):_vm._e(),_vm._v(\" \"),(_vm.isOtherUser && (_vm.loggedIn || !_vm.switcher))?_c('div',{staticClass:\"highlighter\"},[(_vm.userHighlightType !== 'disabled')?_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.userHighlightColor),expression:\"userHighlightColor\"}],staticClass:\"userHighlightText\",attrs:{\"id\":'userHighlightColorTx'+_vm.user.id,\"type\":\"text\"},domProps:{\"value\":(_vm.userHighlightColor)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.userHighlightColor=$event.target.value}}}):_vm._e(),_vm._v(\" \"),(_vm.userHighlightType !== 'disabled')?_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.userHighlightColor),expression:\"userHighlightColor\"}],staticClass:\"userHighlightCl\",attrs:{\"id\":'userHighlightColor'+_vm.user.id,\"type\":\"color\"},domProps:{\"value\":(_vm.userHighlightColor)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.userHighlightColor=$event.target.value}}}):_vm._e(),_vm._v(\" \"),_c('label',{staticClass:\"userHighlightSel select\",attrs:{\"for\":\"style-switcher\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.userHighlightType),expression:\"userHighlightType\"}],staticClass:\"userHighlightSel\",attrs:{\"id\":'userHighlightSel'+_vm.user.id},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.userHighlightType=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},[_c('option',{attrs:{\"value\":\"disabled\"}},[_vm._v(\"No highlight\")]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"solid\"}},[_vm._v(\"Solid bg\")]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"striped\"}},[_vm._v(\"Striped bg\")]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"side\"}},[_vm._v(\"Side stripe\")])]),_vm._v(\" \"),_c('i',{staticClass:\"icon-down-open\"})])]):_vm._e()]),_vm._v(\" \"),(_vm.loggedIn && _vm.isOtherUser)?_c('div',{staticClass:\"user-interactions\"},[_c('div',{staticClass:\"btn-group\"},[_c('FollowButton',{attrs:{\"user\":_vm.user}}),_vm._v(\" \"),(_vm.user.following)?[(!_vm.user.subscribed)?_c('ProgressButton',{staticClass:\"btn btn-default\",attrs:{\"click\":_vm.subscribeUser,\"title\":_vm.$t('user_card.subscribe')}},[_c('i',{staticClass:\"icon-bell-alt\"})]):_c('ProgressButton',{staticClass:\"btn btn-default pressed\",attrs:{\"click\":_vm.unsubscribeUser,\"title\":_vm.$t('user_card.unsubscribe')}},[_c('i',{staticClass:\"icon-bell-ringing-o\"})])]:_vm._e()],2),_vm._v(\" \"),_c('div',[(_vm.user.muted)?_c('button',{staticClass:\"btn btn-default btn-block pressed\",on:{\"click\":_vm.unmuteUser}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.muted'))+\"\\n \")]):_c('button',{staticClass:\"btn btn-default btn-block\",on:{\"click\":_vm.muteUser}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.mute'))+\"\\n \")])]),_vm._v(\" \"),_c('div',[_c('button',{staticClass:\"btn btn-default btn-block\",on:{\"click\":_vm.mentionUser}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('user_card.mention'))+\"\\n \")])]),_vm._v(\" \"),(_vm.loggedIn.role === \"admin\")?_c('ModerationTools',{attrs:{\"user\":_vm.user}}):_vm._e()],1):_vm._e(),_vm._v(\" \"),(!_vm.loggedIn && _vm.user.is_local)?_c('div',{staticClass:\"user-interactions\"},[_c('RemoteFollow',{attrs:{\"user\":_vm.user}})],1):_vm._e()])]),_vm._v(\" \"),(!_vm.hideBio)?_c('div',{staticClass:\"panel-body\"},[(!_vm.mergedConfig.hideUserStats && _vm.switcher)?_c('div',{staticClass:\"user-counts\"},[_c('div',{staticClass:\"user-count\",on:{\"click\":function($event){$event.preventDefault();_vm.setProfileView('statuses')}}},[_c('h5',[_vm._v(_vm._s(_vm.$t('user_card.statuses')))]),_vm._v(\" \"),_c('span',[_vm._v(_vm._s(_vm.user.statuses_count)+\" \"),_c('br')])]),_vm._v(\" \"),_c('div',{staticClass:\"user-count\",on:{\"click\":function($event){$event.preventDefault();_vm.setProfileView('friends')}}},[_c('h5',[_vm._v(_vm._s(_vm.$t('user_card.followees')))]),_vm._v(\" \"),_c('span',[_vm._v(_vm._s(_vm.hideFollowsCount ? _vm.$t('user_card.hidden') : _vm.user.friends_count))])]),_vm._v(\" \"),_c('div',{staticClass:\"user-count\",on:{\"click\":function($event){$event.preventDefault();_vm.setProfileView('followers')}}},[_c('h5',[_vm._v(_vm._s(_vm.$t('user_card.followers')))]),_vm._v(\" \"),_c('span',[_vm._v(_vm._s(_vm.hideFollowersCount ? _vm.$t('user_card.hidden') : _vm.user.followers_count))])])]):_vm._e(),_vm._v(\" \"),(!_vm.hideBio && _vm.user.description_html)?_c('p',{staticClass:\"user-card-bio\",domProps:{\"innerHTML\":_vm._s(_vm.user.description_html)},on:{\"click\":function($event){$event.preventDefault();return _vm.linkClicked($event)}}}):(!_vm.hideBio)?_c('p',{staticClass:\"user-card-bio\"},[_vm._v(\"\\n \"+_vm._s(_vm.user.description)+\"\\n \")]):_vm._e()]):_vm._e()])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"user-info-avatar-link-overlay\"},[_c('i',{staticClass:\"button-icon icon-zoom-in\"})])}]\nexport { render, staticRenderFns }","import StillImage from '../still-image/still-image.vue'\nimport VideoAttachment from '../video_attachment/video_attachment.vue'\nimport nsfwImage from '../../assets/nsfw.png'\nimport fileTypeService from '../../services/file_type/file_type.service.js'\nimport { mapGetters } from 'vuex'\n\nconst Attachment = {\n props: [\n 'attachment',\n 'nsfw',\n 'statusId',\n 'size',\n 'allowPlay',\n 'setMedia',\n 'naturalSizeLoad'\n ],\n data () {\n return {\n nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,\n hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,\n preloadImage: this.$store.getters.mergedConfig.preloadImage,\n loading: false,\n img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),\n modalOpen: false,\n showHidden: false\n }\n },\n components: {\n StillImage,\n VideoAttachment\n },\n computed: {\n usePlaceHolder () {\n return this.size === 'hide' || this.type === 'unknown'\n },\n referrerpolicy () {\n return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'\n },\n type () {\n return fileTypeService.fileType(this.attachment.mimetype)\n },\n hidden () {\n return this.nsfw && this.hideNsfwLocal && !this.showHidden\n },\n isEmpty () {\n return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'\n },\n isSmall () {\n return this.size === 'small'\n },\n fullwidth () {\n return this.type === 'html' || this.type === 'audio'\n },\n ...mapGetters(['mergedConfig'])\n },\n methods: {\n linkClicked ({ target }) {\n if (target.tagName === 'A') {\n window.open(target.href, '_blank')\n }\n },\n openModal (event) {\n const modalTypes = this.mergedConfig.playVideosInModal\n ? ['image', 'video']\n : ['image']\n if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||\n this.usePlaceHolder\n ) {\n event.stopPropagation()\n event.preventDefault()\n this.setMedia()\n this.$store.dispatch('setCurrent', this.attachment)\n }\n },\n toggleHidden (event) {\n if (\n (this.mergedConfig.useOneClickNsfw && !this.showHidden) &&\n (this.type !== 'video' || this.mergedConfig.playVideosInModal)\n ) {\n this.openModal(event)\n return\n }\n if (this.img && !this.preloadImage) {\n if (this.img.onload) {\n this.img.onload()\n } else {\n this.loading = true\n this.img.src = this.attachment.url\n this.img.onload = () => {\n this.loading = false\n this.showHidden = !this.showHidden\n }\n }\n } else {\n this.showHidden = !this.showHidden\n }\n },\n onImageLoad (image) {\n const width = image.naturalWidth\n const height = image.naturalHeight\n this.naturalSizeLoad && this.naturalSizeLoad({ width, height })\n }\n }\n}\n\nexport default Attachment\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./attachment.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./attachment.js\"\nimport __vue_script__ from \"!!babel-loader!./attachment.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-47ab0ca0\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./attachment.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {\nvar _obj;\nvar _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.usePlaceHolder)?_c('div',{on:{\"click\":_vm.openModal}},[(_vm.type !== 'html')?_c('a',{staticClass:\"placeholder\",attrs:{\"target\":\"_blank\",\"href\":_vm.attachment.url}},[_vm._v(\"\\n [\"+_vm._s(_vm.nsfw ? \"NSFW/\" : \"\")+_vm._s(_vm.type.toUpperCase())+\"]\\n \")]):_vm._e()]):_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(!_vm.isEmpty),expression:\"!isEmpty\"}],staticClass:\"attachment\",class:( _obj = {}, _obj[_vm.type] = true, _obj.loading = _vm.loading, _obj['fullwidth'] = _vm.fullwidth, _obj['nsfw-placeholder'] = _vm.hidden, _obj )},[(_vm.hidden)?_c('a',{staticClass:\"image-attachment\",attrs:{\"href\":_vm.attachment.url},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleHidden($event)}}},[_c('img',{key:_vm.nsfwImage,staticClass:\"nsfw\",class:{'small': _vm.isSmall},attrs:{\"src\":_vm.nsfwImage}}),_vm._v(\" \"),(_vm.type === 'video')?_c('i',{staticClass:\"play-icon icon-play-circled\"}):_vm._e()]):_vm._e(),_vm._v(\" \"),(_vm.nsfw && _vm.hideNsfwLocal && !_vm.hidden)?_c('div',{staticClass:\"hider\"},[_c('a',{attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleHidden($event)}}},[_vm._v(\"Hide\")])]):_vm._e(),_vm._v(\" \"),(_vm.type === 'image' && (!_vm.hidden || _vm.preloadImage))?_c('a',{staticClass:\"image-attachment\",class:{'hidden': _vm.hidden && _vm.preloadImage },attrs:{\"href\":_vm.attachment.url,\"target\":\"_blank\",\"title\":_vm.attachment.description},on:{\"click\":_vm.openModal}},[_c('StillImage',{attrs:{\"referrerpolicy\":_vm.referrerpolicy,\"mimetype\":_vm.attachment.mimetype,\"src\":_vm.attachment.large_thumb_url || _vm.attachment.url,\"image-load-handler\":_vm.onImageLoad}})],1):_vm._e(),_vm._v(\" \"),(_vm.type === 'video' && !_vm.hidden)?_c('a',{staticClass:\"video-container\",class:{'small': _vm.isSmall},attrs:{\"href\":_vm.allowPlay ? undefined : _vm.attachment.url},on:{\"click\":_vm.openModal}},[_c('VideoAttachment',{staticClass:\"video\",attrs:{\"attachment\":_vm.attachment,\"controls\":_vm.allowPlay}}),_vm._v(\" \"),(!_vm.allowPlay)?_c('i',{staticClass:\"play-icon icon-play-circled\"}):_vm._e()],1):_vm._e(),_vm._v(\" \"),(_vm.type === 'audio')?_c('audio',{attrs:{\"src\":_vm.attachment.url,\"controls\":\"\"}}):_vm._e(),_vm._v(\" \"),(_vm.type === 'html' && _vm.attachment.oembed)?_c('div',{staticClass:\"oembed\",on:{\"click\":function($event){$event.preventDefault();return _vm.linkClicked($event)}}},[(_vm.attachment.thumb_url)?_c('div',{staticClass:\"image\"},[_c('img',{attrs:{\"src\":_vm.attachment.thumb_url}})]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"text\"},[_c('h1',[_c('a',{attrs:{\"href\":_vm.attachment.url}},[_vm._v(_vm._s(_vm.attachment.oembed.title))])]),_vm._v(\" \"),_c('div',{domProps:{\"innerHTML\":_vm._s(_vm.attachment.oembed.oembedHTML)}})])]):_vm._e()])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import { mapGetters } from 'vuex'\n\nconst FavoriteButton = {\n props: ['status', 'loggedIn'],\n data () {\n return {\n animated: false\n }\n },\n methods: {\n favorite () {\n if (!this.status.favorited) {\n this.$store.dispatch('favorite', { id: this.status.id })\n } else {\n this.$store.dispatch('unfavorite', { id: this.status.id })\n }\n this.animated = true\n setTimeout(() => {\n this.animated = false\n }, 500)\n }\n },\n computed: {\n classes () {\n return {\n 'icon-star-empty': !this.status.favorited,\n 'icon-star': this.status.favorited,\n 'animate-spin': this.animated\n }\n },\n ...mapGetters(['mergedConfig'])\n }\n}\n\nexport default FavoriteButton\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./favorite_button.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./favorite_button.js\"\nimport __vue_script__ from \"!!babel-loader!./favorite_button.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-2ced002f\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./favorite_button.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.loggedIn)?_c('div',[_c('i',{staticClass:\"button-icon favorite-button fav-active\",class:_vm.classes,attrs:{\"title\":_vm.$t('tool_tip.favorite')},on:{\"click\":function($event){$event.preventDefault();_vm.favorite()}}}),_vm._v(\" \"),(!_vm.mergedConfig.hidePostStats && _vm.status.fave_num > 0)?_c('span',[_vm._v(_vm._s(_vm.status.fave_num))]):_vm._e()]):_c('div',[_c('i',{staticClass:\"button-icon favorite-button\",class:_vm.classes,attrs:{\"title\":_vm.$t('tool_tip.favorite')}}),_vm._v(\" \"),(!_vm.mergedConfig.hidePostStats && _vm.status.fave_num > 0)?_c('span',[_vm._v(_vm._s(_vm.status.fave_num))]):_vm._e()])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import { mapGetters } from 'vuex'\n\nconst ReactButton = {\n props: ['status', 'loggedIn'],\n data () {\n return {\n showTooltip: false,\n filterWord: '',\n popperOptions: {\n modifiers: {\n preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }\n }\n }\n }\n },\n methods: {\n openReactionSelect () {\n this.showTooltip = true\n this.filterWord = ''\n },\n closeReactionSelect () {\n this.showTooltip = false\n },\n addReaction (event, emoji) {\n this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })\n this.closeReactionSelect()\n }\n },\n computed: {\n commonEmojis () {\n return ['❤️', '😠', '👀', '😂', '🔥']\n },\n emojis () {\n if (this.filterWord !== '') {\n return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))\n }\n return this.$store.state.instance.emoji || []\n },\n ...mapGetters(['mergedConfig'])\n }\n}\n\nexport default ReactButton\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./react_button.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./react_button.js\"\nimport __vue_script__ from \"!!babel-loader!./react_button.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-7644d4ef\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./react_button.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('v-popover',{staticClass:\"react-button-popover\",attrs:{\"popper-options\":_vm.popperOptions,\"open\":_vm.showTooltip,\"trigger\":\"manual\",\"placement\":\"top\"},on:{\"hide\":_vm.closeReactionSelect}},[_c('div',{attrs:{\"slot\":\"popover\"},slot:\"popover\"},[_c('div',{staticClass:\"reaction-picker-filter\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.filterWord),expression:\"filterWord\"}],attrs:{\"placeholder\":_vm.$t('emoji.search_emoji')},domProps:{\"value\":(_vm.filterWord)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.filterWord=$event.target.value}}})]),_vm._v(\" \"),_c('div',{staticClass:\"reaction-picker\"},[_vm._l((_vm.commonEmojis),function(emoji){return _c('span',{key:emoji,staticClass:\"emoji-button\",on:{\"click\":function($event){_vm.addReaction($event, emoji)}}},[_vm._v(\"\\n \"+_vm._s(emoji)+\"\\n \")])}),_vm._v(\" \"),_c('div',{staticClass:\"reaction-picker-divider\"}),_vm._v(\" \"),_vm._l((_vm.emojis),function(emoji,key){return _c('span',{key:key,staticClass:\"emoji-button\",on:{\"click\":function($event){_vm.addReaction($event, emoji.replacement)}}},[_vm._v(\"\\n \"+_vm._s(emoji.replacement)+\"\\n \")])}),_vm._v(\" \"),_c('div',{staticClass:\"reaction-bottom-fader\"})],2)]),_vm._v(\" \"),(_vm.loggedIn)?_c('div',{on:{\"click\":function($event){$event.preventDefault();return _vm.openReactionSelect($event)}}},[_c('i',{staticClass:\"icon-smile button-icon add-reaction-button\",attrs:{\"title\":_vm.$t('tool_tip.add_reaction')}})]):_vm._e()])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import { mapGetters } from 'vuex'\n\nconst RetweetButton = {\n props: ['status', 'loggedIn', 'visibility'],\n data () {\n return {\n animated: false\n }\n },\n methods: {\n retweet () {\n if (!this.status.repeated) {\n this.$store.dispatch('retweet', { id: this.status.id })\n } else {\n this.$store.dispatch('unretweet', { id: this.status.id })\n }\n this.animated = true\n setTimeout(() => {\n this.animated = false\n }, 500)\n }\n },\n computed: {\n classes () {\n return {\n 'retweeted': this.status.repeated,\n 'retweeted-empty': !this.status.repeated,\n 'animate-spin': this.animated\n }\n },\n ...mapGetters(['mergedConfig'])\n }\n}\n\nexport default RetweetButton\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./retweet_button.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./retweet_button.js\"\nimport __vue_script__ from \"!!babel-loader!./retweet_button.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-538410cc\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./retweet_button.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.loggedIn)?_c('div',[(_vm.visibility !== 'private' && _vm.visibility !== 'direct')?[_c('i',{staticClass:\"button-icon retweet-button icon-retweet rt-active\",class:_vm.classes,attrs:{\"title\":_vm.$t('tool_tip.repeat')},on:{\"click\":function($event){$event.preventDefault();_vm.retweet()}}}),_vm._v(\" \"),(!_vm.mergedConfig.hidePostStats && _vm.status.repeat_num > 0)?_c('span',[_vm._v(_vm._s(_vm.status.repeat_num))]):_vm._e()]:[_c('i',{staticClass:\"button-icon icon-lock\",class:_vm.classes,attrs:{\"title\":_vm.$t('timeline.no_retweet_hint')}})]],2):(!_vm.loggedIn)?_c('div',[_c('i',{staticClass:\"button-icon icon-retweet\",class:_vm.classes,attrs:{\"title\":_vm.$t('tool_tip.repeat')}}),_vm._v(\" \"),(!_vm.mergedConfig.hidePostStats && _vm.status.repeat_num > 0)?_c('span',[_vm._v(_vm._s(_vm.status.repeat_num))]):_vm._e()]):_vm._e()}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import Timeago from '../timeago/timeago.vue'\nimport { forEach, map } from 'lodash'\n\nexport default {\n name: 'Poll',\n props: ['basePoll'],\n components: { Timeago },\n data () {\n return {\n loading: false,\n choices: []\n }\n },\n created () {\n if (!this.$store.state.polls.pollsObject[this.pollId]) {\n this.$store.dispatch('mergeOrAddPoll', this.basePoll)\n }\n this.$store.dispatch('trackPoll', this.pollId)\n },\n destroyed () {\n this.$store.dispatch('untrackPoll', this.pollId)\n },\n computed: {\n pollId () {\n return this.basePoll.id\n },\n poll () {\n const storePoll = this.$store.state.polls.pollsObject[this.pollId]\n return storePoll || {}\n },\n options () {\n return (this.poll && this.poll.options) || []\n },\n expiresAt () {\n return (this.poll && this.poll.expires_at) || 0\n },\n expired () {\n return (this.poll && this.poll.expired) || false\n },\n loggedIn () {\n return this.$store.state.users.currentUser\n },\n showResults () {\n return this.poll.voted || this.expired || !this.loggedIn\n },\n totalVotesCount () {\n return this.poll.votes_count\n },\n containerClass () {\n return {\n loading: this.loading\n }\n },\n choiceIndices () {\n // Convert array of booleans into an array of indices of the\n // items that were 'true', so [true, false, false, true] becomes\n // [0, 3].\n return this.choices\n .map((entry, index) => entry && index)\n .filter(value => typeof value === 'number')\n },\n isDisabled () {\n const noChoice = this.choiceIndices.length === 0\n return this.loading || noChoice\n }\n },\n methods: {\n percentageForOption (count) {\n return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)\n },\n resultTitle (option) {\n return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`\n },\n fetchPoll () {\n this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })\n },\n activateOption (index) {\n // forgive me father: doing checking the radio/checkboxes\n // in code because of customized input elements need either\n // a) an extra element for the actual graphic, or b) use a\n // pseudo element for the label. We use b) which mandates\n // using \"for\" and \"id\" matching which isn't nice when the\n // same poll appears multiple times on the site (notifs and\n // timeline for example). With code we can make sure it just\n // works without altering the pseudo element implementation.\n const allElements = this.$el.querySelectorAll('input')\n const clickedElement = this.$el.querySelector(`input[value=\"${index}\"]`)\n if (this.poll.multiple) {\n // Checkboxes, toggle only the clicked one\n clickedElement.checked = !clickedElement.checked\n } else {\n // Radio button, uncheck everything and check the clicked one\n forEach(allElements, element => { element.checked = false })\n clickedElement.checked = true\n }\n this.choices = map(allElements, e => e.checked)\n },\n optionId (index) {\n return `poll${this.poll.id}-${index}`\n },\n vote () {\n if (this.choiceIndices.length === 0) return\n this.loading = true\n this.$store.dispatch(\n 'votePoll',\n { id: this.statusId, pollId: this.poll.id, choices: this.choiceIndices }\n ).then(poll => {\n this.loading = false\n })\n }\n }\n}\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./poll.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./poll.js\"\nimport __vue_script__ from \"!!babel-loader!./poll.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-6feb4525\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./poll.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"poll\",class:_vm.containerClass},[_vm._l((_vm.options),function(option,index){return _c('div',{key:index,staticClass:\"poll-option\"},[(_vm.showResults)?_c('div',{staticClass:\"option-result\",attrs:{\"title\":_vm.resultTitle(option)}},[_c('div',{staticClass:\"option-result-label\"},[_c('span',{staticClass:\"result-percentage\"},[_vm._v(\"\\n \"+_vm._s(_vm.percentageForOption(option.votes_count))+\"%\\n \")]),_vm._v(\" \"),_c('span',[_vm._v(_vm._s(option.title))])]),_vm._v(\" \"),_c('div',{staticClass:\"result-fill\",style:({ 'width': ((_vm.percentageForOption(option.votes_count)) + \"%\") })})]):_c('div',{on:{\"click\":function($event){_vm.activateOption(index)}}},[(_vm.poll.multiple)?_c('input',{attrs:{\"type\":\"checkbox\",\"disabled\":_vm.loading},domProps:{\"value\":index}}):_c('input',{attrs:{\"type\":\"radio\",\"disabled\":_vm.loading},domProps:{\"value\":index}}),_vm._v(\" \"),_c('label',{staticClass:\"option-vote\"},[_c('div',[_vm._v(_vm._s(option.title))])])])])}),_vm._v(\" \"),_c('div',{staticClass:\"footer faint\"},[(!_vm.showResults)?_c('button',{staticClass:\"btn btn-default poll-vote-button\",attrs:{\"type\":\"button\",\"disabled\":_vm.isDisabled},on:{\"click\":_vm.vote}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('polls.vote'))+\"\\n \")]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"total\"},[_vm._v(\"\\n \"+_vm._s(_vm.totalVotesCount)+\" \"+_vm._s(_vm.$t(\"polls.votes\"))+\" · \\n \")]),_vm._v(\" \"),_c('i18n',{attrs:{\"path\":_vm.expired ? 'polls.expired' : 'polls.expires_in'}},[_c('Timeago',{attrs:{\"time\":_vm.expiresAt,\"auto-update\":60,\"now-threshold\":0}})],1)],1)],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","const ExtraButtons = {\n props: [ 'status' ],\n methods: {\n deleteStatus () {\n const confirmed = window.confirm(this.$t('status.delete_confirm'))\n if (confirmed) {\n this.$store.dispatch('deleteStatus', { id: this.status.id })\n }\n },\n pinStatus () {\n this.$store.dispatch('pinStatus', this.status.id)\n .then(() => this.$emit('onSuccess'))\n .catch(err => this.$emit('onError', err.error.error))\n },\n unpinStatus () {\n this.$store.dispatch('unpinStatus', this.status.id)\n .then(() => this.$emit('onSuccess'))\n .catch(err => this.$emit('onError', err.error.error))\n },\n muteConversation () {\n this.$store.dispatch('muteConversation', this.status.id)\n .then(() => this.$emit('onSuccess'))\n .catch(err => this.$emit('onError', err.error.error))\n },\n unmuteConversation () {\n this.$store.dispatch('unmuteConversation', this.status.id)\n .then(() => this.$emit('onSuccess'))\n .catch(err => this.$emit('onError', err.error.error))\n }\n },\n computed: {\n currentUser () { return this.$store.state.users.currentUser },\n canDelete () {\n if (!this.currentUser) { return }\n const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin\n return superuser || this.status.user.id === this.currentUser.id\n },\n ownStatus () {\n return this.status.user.id === this.currentUser.id\n },\n canPin () {\n return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')\n },\n canMute () {\n return !!this.currentUser\n }\n }\n}\n\nexport default ExtraButtons\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./extra_buttons.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./extra_buttons.js\"\nimport __vue_script__ from \"!!babel-loader!./extra_buttons.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-0caa0403\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./extra_buttons.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.canDelete || _vm.canMute || _vm.canPin)?_c('v-popover',{staticClass:\"extra-button-popover\",attrs:{\"trigger\":\"click\",\"placement\":\"top\"}},[_c('div',{attrs:{\"slot\":\"popover\"},slot:\"popover\"},[_c('div',{staticClass:\"dropdown-menu\"},[(_vm.canMute && !_vm.status.thread_muted)?_c('button',{staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":function($event){$event.preventDefault();return _vm.muteConversation($event)}}},[_c('i',{staticClass:\"icon-eye-off\"}),_c('span',[_vm._v(_vm._s(_vm.$t(\"status.mute_conversation\")))])]):_vm._e(),_vm._v(\" \"),(_vm.canMute && _vm.status.thread_muted)?_c('button',{staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":function($event){$event.preventDefault();return _vm.unmuteConversation($event)}}},[_c('i',{staticClass:\"icon-eye-off\"}),_c('span',[_vm._v(_vm._s(_vm.$t(\"status.unmute_conversation\")))])]):_vm._e(),_vm._v(\" \"),(!_vm.status.pinned && _vm.canPin)?_c('button',{directives:[{name:\"close-popover\",rawName:\"v-close-popover\"}],staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":function($event){$event.preventDefault();return _vm.pinStatus($event)}}},[_c('i',{staticClass:\"icon-pin\"}),_c('span',[_vm._v(_vm._s(_vm.$t(\"status.pin\")))])]):_vm._e(),_vm._v(\" \"),(_vm.status.pinned && _vm.canPin)?_c('button',{directives:[{name:\"close-popover\",rawName:\"v-close-popover\"}],staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":function($event){$event.preventDefault();return _vm.unpinStatus($event)}}},[_c('i',{staticClass:\"icon-pin\"}),_c('span',[_vm._v(_vm._s(_vm.$t(\"status.unpin\")))])]):_vm._e(),_vm._v(\" \"),(_vm.canDelete)?_c('button',{directives:[{name:\"close-popover\",rawName:\"v-close-popover\"}],staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":function($event){$event.preventDefault();return _vm.deleteStatus($event)}}},[_c('i',{staticClass:\"icon-cancel\"}),_c('span',[_vm._v(_vm._s(_vm.$t(\"status.delete\")))])]):_vm._e()])]),_vm._v(\" \"),_c('div',{staticClass:\"button-icon\"},[_c('i',{staticClass:\"icon-ellipsis\"})])]):_vm._e()}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import Attachment from '../attachment/attachment.vue'\nimport { chunk, last, dropRight, sumBy } from 'lodash'\n\nconst Gallery = {\n props: [\n 'attachments',\n 'nsfw',\n 'setMedia'\n ],\n data () {\n return {\n sizes: {}\n }\n },\n components: { Attachment },\n computed: {\n rows () {\n if (!this.attachments) {\n return []\n }\n const rows = chunk(this.attachments, 3)\n if (last(rows).length === 1 && rows.length > 1) {\n // if 1 attachment on last row -> add it to the previous row instead\n const lastAttachment = last(rows)[0]\n const allButLastRow = dropRight(rows)\n last(allButLastRow).push(lastAttachment)\n return allButLastRow\n }\n return rows\n },\n useContainFit () {\n return this.$store.getters.mergedConfig.useContainFit\n }\n },\n methods: {\n onNaturalSizeLoad (id, size) {\n this.$set(this.sizes, id, size)\n },\n rowStyle (itemsPerRow) {\n return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }\n },\n itemStyle (id, row) {\n const total = sumBy(row, item => this.getAspectRatio(item.id))\n return { flex: `${this.getAspectRatio(id) / total} 1 0%` }\n },\n getAspectRatio (id) {\n const size = this.sizes[id]\n return size ? size.width / size.height : 1\n }\n }\n}\n\nexport default Gallery\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./gallery.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./gallery.js\"\nimport __vue_script__ from \"!!babel-loader!./gallery.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-68a574b8\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./gallery.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{ref:\"galleryContainer\",staticStyle:{\"width\":\"100%\"}},_vm._l((_vm.rows),function(row,index){return _c('div',{key:index,staticClass:\"gallery-row\",class:{ 'contain-fit': _vm.useContainFit, 'cover-fit': !_vm.useContainFit },style:(_vm.rowStyle(row.length))},[_c('div',{staticClass:\"gallery-row-inner\"},_vm._l((row),function(attachment){return _c('attachment',{key:attachment.id,style:(_vm.itemStyle(attachment.id, row)),attrs:{\"set-media\":_vm.setMedia,\"nsfw\":_vm.nsfw,\"attachment\":attachment,\"allow-play\":false,\"natural-size-load\":_vm.onNaturalSizeLoad.bind(null, attachment.id)}})}),1)])}),0)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","const LinkPreview = {\n name: 'LinkPreview',\n props: [\n 'card',\n 'size',\n 'nsfw'\n ],\n data () {\n return {\n imageLoaded: false\n }\n },\n computed: {\n useImage () {\n // Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid\n // as it makes sure to hide the image if somehow NSFW tagged preview can\n // exist.\n return this.card.image && !this.nsfw && this.size !== 'hide'\n },\n useDescription () {\n return this.card.description && /\\S/.test(this.card.description)\n }\n },\n created () {\n if (this.useImage) {\n const newImg = new Image()\n newImg.onload = () => {\n this.imageLoaded = true\n }\n newImg.src = this.card.image\n }\n }\n}\n\nexport default LinkPreview\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./link-preview.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./link-preview.js\"\nimport __vue_script__ from \"!!babel-loader!./link-preview.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-7c8d99ac\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./link-preview.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('a',{staticClass:\"link-preview-card\",attrs:{\"href\":_vm.card.url,\"target\":\"_blank\",\"rel\":\"noopener\"}},[(_vm.useImage && _vm.imageLoaded)?_c('div',{staticClass:\"card-image\",class:{ 'small-image': _vm.size === 'small' }},[_c('img',{attrs:{\"src\":_vm.card.image}})]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"card-content\"},[_c('span',{staticClass:\"card-host faint\"},[_vm._v(_vm._s(_vm.card.provider_name))]),_vm._v(\" \"),_c('h4',{staticClass:\"card-title\"},[_vm._v(_vm._s(_vm.card.title))]),_vm._v(\" \"),(_vm.useDescription)?_c('p',{staticClass:\"card-description\"},[_vm._v(_vm._s(_vm.card.description))]):_vm._e()])])])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import UserAvatar from '../user_avatar/user_avatar.vue'\nimport generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'\n\nconst AvatarList = {\n props: ['users'],\n computed: {\n slicedUsers () {\n return this.users ? this.users.slice(0, 15) : []\n }\n },\n components: {\n UserAvatar\n },\n methods: {\n userProfileLink (user) {\n return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)\n }\n }\n}\n\nexport default AvatarList\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./avatar_list.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./avatar_list.js\"\nimport __vue_script__ from \"!!babel-loader!./avatar_list.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-4cea5bcf\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./avatar_list.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"avatars\"},_vm._l((_vm.slicedUsers),function(user){return _c('router-link',{key:user.id,staticClass:\"avatars-item\",attrs:{\"to\":_vm.userProfileLink(user)}},[_c('UserAvatar',{staticClass:\"avatar-small\",attrs:{\"user\":user}})],1)}),1)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import { find } from 'lodash'\n\nconst StatusPopover = {\n name: 'StatusPopover',\n props: [\n 'statusId'\n ],\n data () {\n return {\n popperOptions: {\n modifiers: {\n preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }\n }\n }\n }\n },\n computed: {\n status () {\n return find(this.$store.state.statuses.allStatuses, { id: this.statusId })\n }\n },\n components: {\n Status: () => import('../status/status.vue')\n },\n methods: {\n enter () {\n if (!this.status) {\n this.$store.dispatch('fetchStatus', this.statusId)\n }\n }\n }\n}\n\nexport default StatusPopover\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./status_popover.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./status_popover.js\"\nimport __vue_script__ from \"!!babel-loader!./status_popover.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-ef621460\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./status_popover.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('v-popover',{attrs:{\"popover-class\":\"status-popover\",\"placement\":\"top-start\",\"popper-options\":_vm.popperOptions},on:{\"show\":function($event){_vm.enter()}}},[_c('template',{slot:\"popover\"},[(_vm.status)?_c('Status',{attrs:{\"is-preview\":true,\"statusoid\":_vm.status,\"compact\":true}}):_c('div',{staticClass:\"status-preview-loading\"},[_c('i',{staticClass:\"icon-spin4 animate-spin\"})])],1),_vm._v(\" \"),_vm._t(\"default\")],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","\nconst EmojiReactions = {\n name: 'EmojiReactions',\n props: ['status'],\n computed: {\n emojiReactions () {\n return this.status.emoji_reactions\n }\n },\n methods: {\n reactedWith (emoji) {\n const user = this.$store.state.users.currentUser\n const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji)\n return reaction.accounts && reaction.accounts.find(u => u.id === user.id)\n },\n reactWith (emoji) {\n this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })\n },\n unreact (emoji) {\n this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })\n },\n emojiOnClick (emoji, event) {\n if (this.reactedWith(emoji)) {\n this.unreact(emoji)\n } else {\n this.reactWith(emoji)\n }\n }\n }\n}\n\nexport default EmojiReactions\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./emoji_reactions.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./emoji_reactions.js\"\nimport __vue_script__ from \"!!babel-loader!./emoji_reactions.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-2817b91c\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./emoji_reactions.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"emoji-reactions\"},_vm._l((_vm.emojiReactions),function(reaction){return _c('button',{key:reaction.emoji,staticClass:\"emoji-reaction btn btn-default\",class:{ 'picked-reaction': _vm.reactedWith(reaction.emoji) },on:{\"click\":function($event){_vm.emojiOnClick(reaction.emoji, $event)}}},[_c('span',{staticClass:\"reaction-emoji\"},[_vm._v(_vm._s(reaction.emoji))]),_vm._v(\" \"),_c('span',[_vm._v(_vm._s(reaction.count))])])}),0)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import Attachment from '../attachment/attachment.vue'\nimport FavoriteButton from '../favorite_button/favorite_button.vue'\nimport ReactButton from '../react_button/react_button.vue'\nimport RetweetButton from '../retweet_button/retweet_button.vue'\nimport Poll from '../poll/poll.vue'\nimport ExtraButtons from '../extra_buttons/extra_buttons.vue'\nimport PostStatusForm from '../post_status_form/post_status_form.vue'\nimport UserCard from '../user_card/user_card.vue'\nimport UserAvatar from '../user_avatar/user_avatar.vue'\nimport Gallery from '../gallery/gallery.vue'\nimport LinkPreview from '../link-preview/link-preview.vue'\nimport AvatarList from '../avatar_list/avatar_list.vue'\nimport Timeago from '../timeago/timeago.vue'\nimport StatusPopover from '../status_popover/status_popover.vue'\nimport EmojiReactions from '../emoji_reactions/emoji_reactions.vue'\nimport generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'\nimport fileType from 'src/services/file_type/file_type.service'\nimport { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'\nimport { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'\nimport { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'\nimport { filter, unescape, uniqBy } from 'lodash'\nimport { mapGetters, mapState } from 'vuex'\n\nconst Status = {\n name: 'Status',\n props: [\n 'statusoid',\n 'expandable',\n 'inConversation',\n 'focused',\n 'highlight',\n 'compact',\n 'replies',\n 'isPreview',\n 'noHeading',\n 'inlineExpanded',\n 'showPinned',\n 'inProfile',\n 'profileUserId'\n ],\n data () {\n return {\n replying: false,\n unmuted: false,\n userExpanded: false,\n showingTall: this.inConversation && this.focused,\n showingLongSubject: false,\n error: null,\n // not as computed because it sets the initial state which will be changed later\n expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject\n }\n },\n computed: {\n localCollapseSubjectDefault () {\n return this.mergedConfig.collapseMessageWithSubject\n },\n muteWords () {\n return this.mergedConfig.muteWords\n },\n repeaterClass () {\n const user = this.statusoid.user\n return highlightClass(user)\n },\n userClass () {\n const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user\n return highlightClass(user)\n },\n deleted () {\n return this.statusoid.deleted\n },\n repeaterStyle () {\n const user = this.statusoid.user\n const highlight = this.mergedConfig.highlight\n return highlightStyle(highlight[user.screen_name])\n },\n userStyle () {\n if (this.noHeading) return\n const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user\n const highlight = this.mergedConfig.highlight\n return highlightStyle(highlight[user.screen_name])\n },\n hideAttachments () {\n return (this.mergedConfig.hideAttachments && !this.inConversation) ||\n (this.mergedConfig.hideAttachmentsInConv && this.inConversation)\n },\n userProfileLink () {\n return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)\n },\n replyProfileLink () {\n if (this.isReply) {\n return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName)\n }\n },\n retweet () { return !!this.statusoid.retweeted_status },\n retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name },\n retweeterHtml () { return this.statusoid.user.name_html },\n retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },\n status () {\n if (this.retweet) {\n return this.statusoid.retweeted_status\n } else {\n return this.statusoid\n }\n },\n statusFromGlobalRepository () {\n // NOTE: Consider to replace status with statusFromGlobalRepository\n return this.$store.state.statuses.allStatusesObject[this.status.id]\n },\n loggedIn () {\n return !!this.currentUser\n },\n muteWordHits () {\n const statusText = this.status.text.toLowerCase()\n const statusSummary = this.status.summary.toLowerCase()\n const hits = filter(this.muteWords, (muteWord) => {\n return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())\n })\n\n return hits\n },\n muted () { return !this.unmuted && ((!(this.inProfile && this.status.user.id === this.profileUserId) && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },\n hideFilteredStatuses () {\n return this.mergedConfig.hideFilteredStatuses\n },\n hideStatus () {\n return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)\n },\n isFocused () {\n // retweet or root of an expanded conversation\n if (this.focused) {\n return true\n } else if (!this.inConversation) {\n return false\n }\n // use conversation highlight only when in conversation\n return this.status.id === this.highlight\n },\n // This is a bit hacky, but we want to approximate post height before rendering\n // so we count newlines (masto uses

for paragraphs, GS uses
between them)\n // as well as approximate line count by counting characters and approximating ~80\n // per line.\n //\n // Using max-height + overflow: auto for status components resulted in false positives\n // very often with japanese characters, and it was very annoying.\n tallStatus () {\n const lengthScore = this.status.statusnet_html.split(/ 20\n },\n longSubject () {\n return this.status.summary.length > 900\n },\n isReply () {\n return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)\n },\n replyToName () {\n if (this.status.in_reply_to_screen_name) {\n return this.status.in_reply_to_screen_name\n } else {\n const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)\n return user && user.screen_name\n }\n },\n hideReply () {\n if (this.mergedConfig.replyVisibility === 'all') {\n return false\n }\n if (this.inConversation || !this.isReply) {\n return false\n }\n if (this.status.user.id === this.currentUser.id) {\n return false\n }\n if (this.status.type === 'retweet') {\n return false\n }\n const checkFollowing = this.mergedConfig.replyVisibility === 'following'\n for (var i = 0; i < this.status.attentions.length; ++i) {\n if (this.status.user.id === this.status.attentions[i].id) {\n continue\n }\n const taggedUser = this.$store.getters.findUser(this.status.attentions[i].id)\n if (checkFollowing && taggedUser && taggedUser.following) {\n return false\n }\n if (this.status.attentions[i].id === this.currentUser.id) {\n return false\n }\n }\n return this.status.attentions.length > 0\n },\n hideSubjectStatus () {\n if (this.tallStatus && !this.localCollapseSubjectDefault) {\n return false\n }\n return !this.expandingSubject && this.status.summary\n },\n hideTallStatus () {\n if (this.status.summary && this.localCollapseSubjectDefault) {\n return false\n }\n if (this.showingTall) {\n return false\n }\n return this.tallStatus\n },\n showingMore () {\n return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)\n },\n nsfwClickthrough () {\n if (!this.status.nsfw) {\n return false\n }\n if (this.status.summary && this.localCollapseSubjectDefault) {\n return false\n }\n return true\n },\n replySubject () {\n if (!this.status.summary) return ''\n const decodedSummary = unescape(this.status.summary)\n const behavior = this.mergedConfig.subjectLineBehavior\n const startsWithRe = decodedSummary.match(/^re[: ]/i)\n if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {\n return decodedSummary\n } else if (behavior === 'email') {\n return 're: '.concat(decodedSummary)\n } else if (behavior === 'noop') {\n return ''\n }\n },\n attachmentSize () {\n if ((this.mergedConfig.hideAttachments && !this.inConversation) ||\n (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||\n (this.status.attachments.length > this.maxThumbnails)) {\n return 'hide'\n } else if (this.compact) {\n return 'small'\n }\n return 'normal'\n },\n galleryTypes () {\n if (this.attachmentSize === 'hide') {\n return []\n }\n return this.mergedConfig.playVideosInModal\n ? ['image', 'video']\n : ['image']\n },\n galleryAttachments () {\n return this.status.attachments.filter(\n file => fileType.fileMatchesSomeType(this.galleryTypes, file)\n )\n },\n nonGalleryAttachments () {\n return this.status.attachments.filter(\n file => !fileType.fileMatchesSomeType(this.galleryTypes, file)\n )\n },\n maxThumbnails () {\n return this.mergedConfig.maxThumbnails\n },\n postBodyHtml () {\n const html = this.status.statusnet_html\n\n if (this.mergedConfig.greentext) {\n try {\n if (html.includes('>')) {\n // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works\n return processHtml(html, (string) => {\n if (string.includes('>') &&\n string\n .replace(/<[^>]+?>/gi, '') // remove all tags\n .replace(/@\\w+/gi, '') // remove mentions (even failed ones)\n .trim()\n .startsWith('>')) {\n return `${string}`\n } else {\n return string\n }\n })\n } else {\n return html\n }\n } catch (e) {\n console.err('Failed to process status html', e)\n return html\n }\n } else {\n return html\n }\n },\n contentHtml () {\n if (!this.status.summary_html) {\n return this.postBodyHtml\n }\n return this.status.summary_html + '
' + this.postBodyHtml\n },\n combinedFavsAndRepeatsUsers () {\n // Use the status from the global status repository since favs and repeats are saved in it\n const combinedUsers = [].concat(\n this.statusFromGlobalRepository.favoritedBy,\n this.statusFromGlobalRepository.rebloggedBy\n )\n return uniqBy(combinedUsers, 'id')\n },\n ownStatus () {\n return this.status.user.id === this.currentUser.id\n },\n tags () {\n return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')\n },\n hidePostStats () {\n return this.mergedConfig.hidePostStats\n },\n ...mapGetters(['mergedConfig']),\n ...mapState({\n betterShadow: state => state.interface.browserSupport.cssFilter,\n currentUser: state => state.users.currentUser\n })\n },\n components: {\n Attachment,\n FavoriteButton,\n ReactButton,\n RetweetButton,\n ExtraButtons,\n PostStatusForm,\n Poll,\n UserCard,\n UserAvatar,\n Gallery,\n LinkPreview,\n AvatarList,\n Timeago,\n StatusPopover,\n EmojiReactions\n },\n methods: {\n visibilityIcon (visibility) {\n switch (visibility) {\n case 'private':\n return 'icon-lock'\n case 'unlisted':\n return 'icon-lock-open-alt'\n case 'direct':\n return 'icon-mail-alt'\n default:\n return 'icon-globe'\n }\n },\n showError (error) {\n this.error = error\n },\n clearError () {\n this.error = undefined\n },\n linkClicked (event) {\n const target = event.target.closest('.status-content a')\n if (target) {\n if (target.className.match(/mention/)) {\n const href = target.href\n const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))\n if (attn) {\n event.stopPropagation()\n event.preventDefault()\n const link = this.generateUserProfileLink(attn.id, attn.screen_name)\n this.$router.push(link)\n return\n }\n }\n if (target.rel.match(/(?:^|\\s)tag(?:$|\\s)/) || target.className.match(/hashtag/)) {\n // Extract tag name from link url\n const tag = extractTagFromUrl(target.href)\n if (tag) {\n const link = this.generateTagLink(tag)\n this.$router.push(link)\n return\n }\n }\n window.open(target.href, '_blank')\n }\n },\n toggleReplying () {\n this.replying = !this.replying\n },\n gotoOriginal (id) {\n if (this.inConversation) {\n this.$emit('goto', id)\n }\n },\n toggleExpanded () {\n this.$emit('toggleExpanded')\n },\n toggleMute () {\n this.unmuted = !this.unmuted\n },\n toggleUserExpanded () {\n this.userExpanded = !this.userExpanded\n },\n toggleShowMore () {\n if (this.showingTall) {\n this.showingTall = false\n } else if (this.expandingSubject && this.status.summary) {\n this.expandingSubject = false\n } else if (this.hideTallStatus) {\n this.showingTall = true\n } else if (this.hideSubjectStatus && this.status.summary) {\n this.expandingSubject = true\n }\n },\n generateUserProfileLink (id, name) {\n return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)\n },\n generateTagLink (tag) {\n return `/tag/${tag}`\n },\n setMedia () {\n const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments\n return () => this.$store.dispatch('setMedia', attachments)\n }\n },\n watch: {\n 'highlight': function (id) {\n if (this.status.id === id) {\n let rect = this.$el.getBoundingClientRect()\n if (rect.top < 100) {\n // Post is above screen, match its top to screen top\n window.scrollBy(0, rect.top - 100)\n } else if (rect.height >= (window.innerHeight - 50)) {\n // Post we want to see is taller than screen so match its top to screen top\n window.scrollBy(0, rect.top - 100)\n } else if (rect.bottom > window.innerHeight - 50) {\n // Post is below screen, match its bottom to screen bottom\n window.scrollBy(0, rect.bottom - window.innerHeight + 50)\n }\n }\n },\n 'status.repeat_num': function (num) {\n // refetch repeats when repeat_num is changed in any way\n if (this.isFocused && this.statusFromGlobalRepository.rebloggedBy && this.statusFromGlobalRepository.rebloggedBy.length !== num) {\n this.$store.dispatch('fetchRepeats', this.status.id)\n }\n },\n 'status.fave_num': function (num) {\n // refetch favs when fave_num is changed in any way\n if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {\n this.$store.dispatch('fetchFavs', this.status.id)\n }\n }\n },\n filters: {\n capitalize: function (str) {\n return str.charAt(0).toUpperCase() + str.slice(1)\n }\n }\n}\n\nexport default Status\n","/**\n * This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and\n * allows it to be processed, useful for greentexting, mostly\n *\n * known issue: doesn't handle CDATA so nested CDATA might not work well\n *\n * @param {Object} input - input data\n * @param {(string) => string} processor - function that will be called on every line\n * @return {string} processed html\n */\nexport const processHtml = (html, processor) => {\n const handledTags = new Set(['p', 'br', 'div'])\n const openCloseTags = new Set(['p', 'div'])\n\n let buffer = '' // Current output buffer\n const level = [] // How deep we are in tags and which tags were there\n let textBuffer = '' // Current line content\n let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag\n\n // Extracts tag name from tag, i.e. => span\n const getTagName = (tag) => {\n const result = /(?:<\\/(\\w+)>|<(\\w+)\\s?[^/]*?\\/?>)/gi.exec(tag)\n return result && (result[1] || result[2])\n }\n\n const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer\n if (textBuffer.trim().length > 0) {\n buffer += processor(textBuffer)\n } else {\n buffer += textBuffer\n }\n textBuffer = ''\n }\n\n const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing\n flush()\n buffer += tag\n }\n\n const handleOpen = (tag) => { // handles opening tags\n flush()\n buffer += tag\n level.push(tag)\n }\n\n const handleClose = (tag) => { // handles closing tags\n flush()\n buffer += tag\n if (level[level.length - 1] === tag) {\n level.pop()\n }\n }\n\n for (let i = 0; i < html.length; i++) {\n const char = html[i]\n if (char === '<' && tagBuffer === null) {\n tagBuffer = char\n } else if (char !== '>' && tagBuffer !== null) {\n tagBuffer += char\n } else if (char === '>' && tagBuffer !== null) {\n tagBuffer += char\n const tagFull = tagBuffer\n tagBuffer = null\n const tagName = getTagName(tagFull)\n if (handledTags.has(tagName)) {\n if (tagName === 'br') {\n handleBr(tagFull)\n } else if (openCloseTags.has(tagName)) {\n if (tagFull[1] === '/') {\n handleClose(tagFull)\n } else if (tagFull[tagFull.length - 2] === '/') {\n // self-closing\n handleBr(tagFull)\n } else {\n handleOpen(tagFull)\n }\n }\n } else {\n textBuffer += tagFull\n }\n } else if (char === '\\n') {\n handleBr(char)\n } else {\n textBuffer += char\n }\n }\n if (tagBuffer) {\n textBuffer += tagBuffer\n }\n\n flush()\n\n return buffer\n}\n","export const mentionMatchesUrl = (attention, url) => {\n if (url === attention.statusnet_profile_url) {\n return true\n }\n const [namepart, instancepart] = attention.screen_name.split('@')\n const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')\n\n return !!url.match(matchstring)\n}\n\n/**\n * Extract tag name from pleroma or mastodon url.\n * i.e https://bikeshed.party/tag/photo or https://quey.org/tags/sky\n * @param {string} url\n */\nexport const extractTagFromUrl = (url) => {\n const regex = /tag[s]*\\/(\\w+)$/g\n const result = regex.exec(url)\n if (!result) {\n return false\n }\n return result[1]\n}\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./status.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./status.js\"\nimport __vue_script__ from \"!!babel-loader!./status.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-795261e2\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./status.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.hideStatus)?_c('div',{staticClass:\"status-el\",class:[{ 'status-el_focused': _vm.isFocused }, { 'status-conversation': _vm.inlineExpanded }]},[(_vm.error)?_c('div',{staticClass:\"alert error\"},[_vm._v(\"\\n \"+_vm._s(_vm.error)+\"\\n \"),_c('i',{staticClass:\"button-icon icon-cancel\",on:{\"click\":_vm.clearError}})]):_vm._e(),_vm._v(\" \"),(_vm.muted && !_vm.isPreview)?[_c('div',{staticClass:\"media status container muted\"},[_c('small',[_c('router-link',{attrs:{\"to\":_vm.userProfileLink}},[_vm._v(\"\\n \"+_vm._s(_vm.status.user.screen_name)+\"\\n \")])],1),_vm._v(\" \"),_c('small',{staticClass:\"muteWords\"},[_vm._v(_vm._s(_vm.muteWordHits.join(', ')))]),_vm._v(\" \"),_c('a',{staticClass:\"unmute\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleMute($event)}}},[_c('i',{staticClass:\"button-icon icon-eye-off\"})])])]:[(_vm.showPinned)?_c('div',{staticClass:\"status-pin\"},[_c('i',{staticClass:\"fa icon-pin faint\"}),_vm._v(\" \"),_c('span',{staticClass:\"faint\"},[_vm._v(_vm._s(_vm.$t('status.pinned')))])]):_vm._e(),_vm._v(\" \"),(_vm.retweet && !_vm.noHeading && !_vm.inConversation)?_c('div',{staticClass:\"media container retweet-info\",class:[_vm.repeaterClass, { highlighted: _vm.repeaterStyle }],style:([_vm.repeaterStyle])},[(_vm.retweet)?_c('UserAvatar',{staticClass:\"media-left\",attrs:{\"better-shadow\":_vm.betterShadow,\"user\":_vm.statusoid.user}}):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"media-body faint\"},[_c('span',{staticClass:\"user-name\"},[(_vm.retweeterHtml)?_c('router-link',{attrs:{\"to\":_vm.retweeterProfileLink},domProps:{\"innerHTML\":_vm._s(_vm.retweeterHtml)}}):_c('router-link',{attrs:{\"to\":_vm.retweeterProfileLink}},[_vm._v(_vm._s(_vm.retweeter))])],1),_vm._v(\" \"),_c('i',{staticClass:\"fa icon-retweet retweeted\",attrs:{\"title\":_vm.$t('tool_tip.repeat')}}),_vm._v(\"\\n \"+_vm._s(_vm.$t('timeline.repeated'))+\"\\n \")])],1):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"media status\",class:[_vm.userClass, { highlighted: _vm.userStyle, 'is-retweet': _vm.retweet && !_vm.inConversation }],style:([ _vm.userStyle ]),attrs:{\"data-tags\":_vm.tags}},[(!_vm.noHeading)?_c('div',{staticClass:\"media-left\"},[_c('router-link',{attrs:{\"to\":_vm.userProfileLink},nativeOn:{\"!click\":function($event){$event.stopPropagation();$event.preventDefault();return _vm.toggleUserExpanded($event)}}},[_c('UserAvatar',{attrs:{\"compact\":_vm.compact,\"better-shadow\":_vm.betterShadow,\"user\":_vm.status.user}})],1)],1):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"status-body\"},[(_vm.userExpanded)?_c('UserCard',{staticClass:\"status-usercard\",attrs:{\"user\":_vm.status.user,\"rounded\":true,\"bordered\":true}}):_vm._e(),_vm._v(\" \"),(!_vm.noHeading)?_c('div',{staticClass:\"media-heading\"},[_c('div',{staticClass:\"heading-name-row\"},[_c('div',{staticClass:\"name-and-account-name\"},[(_vm.status.user.name_html)?_c('h4',{staticClass:\"user-name\",domProps:{\"innerHTML\":_vm._s(_vm.status.user.name_html)}}):_c('h4',{staticClass:\"user-name\"},[_vm._v(\"\\n \"+_vm._s(_vm.status.user.name)+\"\\n \")]),_vm._v(\" \"),_c('router-link',{staticClass:\"account-name\",attrs:{\"to\":_vm.userProfileLink}},[_vm._v(\"\\n \"+_vm._s(_vm.status.user.screen_name)+\"\\n \")])],1),_vm._v(\" \"),_c('span',{staticClass:\"heading-right\"},[_c('router-link',{staticClass:\"timeago faint-link\",attrs:{\"to\":{ name: 'conversation', params: { id: _vm.status.id } }}},[_c('Timeago',{attrs:{\"time\":_vm.status.created_at,\"auto-update\":60}})],1),_vm._v(\" \"),(_vm.status.visibility)?_c('div',{staticClass:\"button-icon visibility-icon\"},[_c('i',{class:_vm.visibilityIcon(_vm.status.visibility),attrs:{\"title\":_vm._f(\"capitalize\")(_vm.status.visibility)}})]):_vm._e(),_vm._v(\" \"),(!_vm.status.is_local && !_vm.isPreview)?_c('a',{staticClass:\"source_url\",attrs:{\"href\":_vm.status.external_url,\"target\":\"_blank\",\"title\":\"Source\"}},[_c('i',{staticClass:\"button-icon icon-link-ext-alt\"})]):_vm._e(),_vm._v(\" \"),(_vm.expandable && !_vm.isPreview)?[_c('a',{attrs:{\"href\":\"#\",\"title\":\"Expand\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleExpanded($event)}}},[_c('i',{staticClass:\"button-icon icon-plus-squared\"})])]:_vm._e(),_vm._v(\" \"),(_vm.unmuted)?_c('a',{attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleMute($event)}}},[_c('i',{staticClass:\"button-icon icon-eye-off\"})]):_vm._e()],2)]),_vm._v(\" \"),_c('div',{staticClass:\"heading-reply-row\"},[(_vm.isReply)?_c('div',{staticClass:\"reply-to-and-accountname\"},[(!_vm.isPreview)?_c('StatusPopover',{attrs:{\"status-id\":_vm.status.in_reply_to_status_id}},[_c('a',{staticClass:\"reply-to\",attrs:{\"href\":\"#\",\"aria-label\":_vm.$t('tool_tip.reply')},on:{\"click\":function($event){$event.preventDefault();_vm.gotoOriginal(_vm.status.in_reply_to_status_id)}}},[_c('i',{staticClass:\"button-icon icon-reply\"}),_vm._v(\" \"),_c('span',{staticClass:\"faint-link reply-to-text\"},[_vm._v(_vm._s(_vm.$t('status.reply_to')))])])]):_c('span',{staticClass:\"reply-to\"},[_c('span',{staticClass:\"reply-to-text\"},[_vm._v(_vm._s(_vm.$t('status.reply_to')))])]),_vm._v(\" \"),_c('router-link',{attrs:{\"to\":_vm.replyProfileLink}},[_vm._v(\"\\n \"+_vm._s(_vm.replyToName)+\"\\n \")]),_vm._v(\" \"),(_vm.replies && _vm.replies.length)?_c('span',{staticClass:\"faint replies-separator\"},[_vm._v(\"\\n -\\n \")]):_vm._e()],1):_vm._e(),_vm._v(\" \"),(_vm.inConversation && !_vm.isPreview && _vm.replies && _vm.replies.length)?_c('div',{staticClass:\"replies\"},[_c('span',{staticClass:\"faint\"},[_vm._v(_vm._s(_vm.$t('status.replies_list')))]),_vm._v(\" \"),_vm._l((_vm.replies),function(reply){return _c('StatusPopover',{key:reply.id,attrs:{\"status-id\":reply.id}},[_c('a',{staticClass:\"reply-link\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();_vm.gotoOriginal(reply.id)}}},[_vm._v(_vm._s(reply.name))])])})],2):_vm._e()])]):_vm._e(),_vm._v(\" \"),(_vm.longSubject)?_c('div',{staticClass:\"status-content-wrapper\",class:{ 'tall-status': !_vm.showingLongSubject }},[(!_vm.showingLongSubject)?_c('a',{staticClass:\"tall-status-hider\",class:{ 'tall-status-hider_focused': _vm.isFocused },attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();_vm.showingLongSubject=true}}},[_vm._v(_vm._s(_vm.$t(\"general.show_more\")))]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"status-content media-body\",domProps:{\"innerHTML\":_vm._s(_vm.contentHtml)},on:{\"click\":function($event){$event.preventDefault();return _vm.linkClicked($event)}}}),_vm._v(\" \"),(_vm.showingLongSubject)?_c('a',{staticClass:\"status-unhider\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();_vm.showingLongSubject=false}}},[_vm._v(_vm._s(_vm.$t(\"general.show_less\")))]):_vm._e()]):_c('div',{staticClass:\"status-content-wrapper\",class:{'tall-status': _vm.hideTallStatus}},[(_vm.hideTallStatus)?_c('a',{staticClass:\"tall-status-hider\",class:{ 'tall-status-hider_focused': _vm.isFocused },attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleShowMore($event)}}},[_vm._v(_vm._s(_vm.$t(\"general.show_more\")))]):_vm._e(),_vm._v(\" \"),(!_vm.hideSubjectStatus)?_c('div',{staticClass:\"status-content media-body\",domProps:{\"innerHTML\":_vm._s(_vm.contentHtml)},on:{\"click\":function($event){$event.preventDefault();return _vm.linkClicked($event)}}}):_c('div',{staticClass:\"status-content media-body\",domProps:{\"innerHTML\":_vm._s(_vm.status.summary_html)},on:{\"click\":function($event){$event.preventDefault();return _vm.linkClicked($event)}}}),_vm._v(\" \"),(_vm.hideSubjectStatus)?_c('a',{staticClass:\"cw-status-hider\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleShowMore($event)}}},[_vm._v(_vm._s(_vm.$t(\"general.show_more\")))]):_vm._e(),_vm._v(\" \"),(_vm.showingMore)?_c('a',{staticClass:\"status-unhider\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleShowMore($event)}}},[_vm._v(_vm._s(_vm.$t(\"general.show_less\")))]):_vm._e()]),_vm._v(\" \"),(_vm.status.poll && _vm.status.poll.options)?_c('div',[_c('poll',{attrs:{\"base-poll\":_vm.status.poll}})],1):_vm._e(),_vm._v(\" \"),(_vm.status.attachments && (!_vm.hideSubjectStatus || _vm.showingLongSubject))?_c('div',{staticClass:\"attachments media-body\"},[_vm._l((_vm.nonGalleryAttachments),function(attachment){return _c('attachment',{key:attachment.id,staticClass:\"non-gallery\",attrs:{\"size\":_vm.attachmentSize,\"nsfw\":_vm.nsfwClickthrough,\"attachment\":attachment,\"allow-play\":true,\"set-media\":_vm.setMedia()}})}),_vm._v(\" \"),(_vm.galleryAttachments.length > 0)?_c('gallery',{attrs:{\"nsfw\":_vm.nsfwClickthrough,\"attachments\":_vm.galleryAttachments,\"set-media\":_vm.setMedia()}}):_vm._e()],2):_vm._e(),_vm._v(\" \"),(_vm.status.card && !_vm.hideSubjectStatus && !_vm.noHeading)?_c('div',{staticClass:\"link-preview media-body\"},[_c('link-preview',{attrs:{\"card\":_vm.status.card,\"size\":_vm.attachmentSize,\"nsfw\":_vm.nsfwClickthrough}})],1):_vm._e(),_vm._v(\" \"),_c('transition',{attrs:{\"name\":\"fade\"}},[(!_vm.hidePostStats && _vm.isFocused && _vm.combinedFavsAndRepeatsUsers.length > 0)?_c('div',{staticClass:\"favs-repeated-users\"},[_c('div',{staticClass:\"stats\"},[(_vm.statusFromGlobalRepository.rebloggedBy && _vm.statusFromGlobalRepository.rebloggedBy.length > 0)?_c('div',{staticClass:\"stat-count\"},[_c('a',{staticClass:\"stat-title\"},[_vm._v(_vm._s(_vm.$t('status.repeats')))]),_vm._v(\" \"),_c('div',{staticClass:\"stat-number\"},[_vm._v(\"\\n \"+_vm._s(_vm.statusFromGlobalRepository.rebloggedBy.length)+\"\\n \")])]):_vm._e(),_vm._v(\" \"),(_vm.statusFromGlobalRepository.favoritedBy && _vm.statusFromGlobalRepository.favoritedBy.length > 0)?_c('div',{staticClass:\"stat-count\"},[_c('a',{staticClass:\"stat-title\"},[_vm._v(_vm._s(_vm.$t('status.favorites')))]),_vm._v(\" \"),_c('div',{staticClass:\"stat-number\"},[_vm._v(\"\\n \"+_vm._s(_vm.statusFromGlobalRepository.favoritedBy.length)+\"\\n \")])]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"avatar-row\"},[_c('AvatarList',{attrs:{\"users\":_vm.combinedFavsAndRepeatsUsers}})],1)])]):_vm._e()]),_vm._v(\" \"),_c('EmojiReactions',{attrs:{\"status\":_vm.status}}),_vm._v(\" \"),(!_vm.noHeading && !_vm.isPreview)?_c('div',{staticClass:\"status-actions media-body\"},[_c('div',[(_vm.loggedIn)?_c('i',{staticClass:\"button-icon icon-reply\",class:{'button-icon-active': _vm.replying},attrs:{\"title\":_vm.$t('tool_tip.reply')},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleReplying($event)}}}):_c('i',{staticClass:\"button-icon button-icon-disabled icon-reply\",attrs:{\"title\":_vm.$t('tool_tip.reply')}}),_vm._v(\" \"),(_vm.status.replies_count > 0)?_c('span',[_vm._v(_vm._s(_vm.status.replies_count))]):_vm._e()]),_vm._v(\" \"),_c('retweet-button',{attrs:{\"visibility\":_vm.status.visibility,\"logged-in\":_vm.loggedIn,\"status\":_vm.status}}),_vm._v(\" \"),_c('favorite-button',{attrs:{\"logged-in\":_vm.loggedIn,\"status\":_vm.status}}),_vm._v(\" \"),_c('ReactButton',{attrs:{\"logged-in\":_vm.loggedIn,\"status\":_vm.status}}),_vm._v(\" \"),_c('extra-buttons',{attrs:{\"status\":_vm.status},on:{\"onError\":_vm.showError,\"onSuccess\":_vm.clearError}})],1):_vm._e()],1)]),_vm._v(\" \"),(_vm.replying)?_c('div',{staticClass:\"container\"},[_c('PostStatusForm',{staticClass:\"reply-body\",attrs:{\"reply-to\":_vm.status.id,\"attentions\":_vm.status.attentions,\"replied-user\":_vm.status.user,\"copy-message-scope\":_vm.status.visibility,\"subject\":_vm.replySubject},on:{\"posted\":_vm.toggleReplying}})],1):_vm._e()]],2):_vm._e()}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import StillImage from '../still-image/still-image.vue'\n\nconst UserAvatar = {\n props: [\n 'user',\n 'betterShadow',\n 'compact'\n ],\n data () {\n return {\n showPlaceholder: false\n }\n },\n components: {\n StillImage\n },\n computed: {\n imgSrc () {\n return this.showPlaceholder ? '/images/avi.png' : this.user.profile_image_url_original\n }\n },\n methods: {\n imageLoadError () {\n this.showPlaceholder = true\n }\n },\n watch: {\n src () {\n this.showPlaceholder = false\n }\n }\n}\n\nexport default UserAvatar\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./user_avatar.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./user_avatar.js\"\nimport __vue_script__ from \"!!babel-loader!./user_avatar.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-056a5e34\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./user_avatar.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('StillImage',{staticClass:\"avatar\",class:{ 'avatar-compact': _vm.compact, 'better-shadow': _vm.betterShadow },attrs:{\"alt\":_vm.user.screen_name,\"title\":_vm.user.screen_name,\"src\":_vm.imgSrc,\"image-load-error\":_vm.imageLoadError}})}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","export const SECOND = 1000\nexport const MINUTE = 60 * SECOND\nexport const HOUR = 60 * MINUTE\nexport const DAY = 24 * HOUR\nexport const WEEK = 7 * DAY\nexport const MONTH = 30 * DAY\nexport const YEAR = 365.25 * DAY\n\nexport const relativeTime = (date, nowThreshold = 1) => {\n if (typeof date === 'string') date = Date.parse(date)\n const round = Date.now() > date ? Math.floor : Math.ceil\n const d = Math.abs(Date.now() - date)\n let r = { num: round(d / YEAR), key: 'time.years' }\n if (d < nowThreshold * SECOND) {\n r.num = 0\n r.key = 'time.now'\n } else if (d < MINUTE) {\n r.num = round(d / SECOND)\n r.key = 'time.seconds'\n } else if (d < HOUR) {\n r.num = round(d / MINUTE)\n r.key = 'time.minutes'\n } else if (d < DAY) {\n r.num = round(d / HOUR)\n r.key = 'time.hours'\n } else if (d < WEEK) {\n r.num = round(d / DAY)\n r.key = 'time.days'\n } else if (d < MONTH) {\n r.num = round(d / WEEK)\n r.key = 'time.weeks'\n } else if (d < YEAR) {\n r.num = round(d / MONTH)\n r.key = 'time.months'\n }\n // Remove plural form when singular\n if (r.num === 1) r.key = r.key.slice(0, -1)\n return r\n}\n\nexport const relativeTimeShort = (date, nowThreshold = 1) => {\n const r = relativeTime(date, nowThreshold)\n r.key += '_short'\n return r\n}\n","\n\n\n","/* script */\nexport * from \"!!babel-loader!../../../node_modules/vue-loader/lib/selector?type=script&index=0!./progress_button.vue\"\nimport __vue_script__ from \"!!babel-loader!../../../node_modules/vue-loader/lib/selector?type=script&index=0!./progress_button.vue\"\n/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-9f751ae6\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./progress_button.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = null\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('button',{attrs:{\"disabled\":_vm.progress || _vm.disabled},on:{\"click\":_vm.onClick}},[(_vm.progress && _vm.$slots.progress)?[_vm._t(\"progress\")]:[_vm._t(\"default\")]],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import { hex2rgb } from '../color_convert/color_convert.js'\nconst highlightStyle = (prefs) => {\n if (prefs === undefined) return\n const { color, type } = prefs\n if (typeof color !== 'string') return\n const rgb = hex2rgb(color)\n if (rgb == null) return\n const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`\n const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`\n const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`\n if (type === 'striped') {\n return {\n backgroundImage: [\n 'repeating-linear-gradient(135deg,',\n `${tintColor} ,`,\n `${tintColor} 20px,`,\n `${tintColor2} 20px,`,\n `${tintColor2} 40px`\n ].join(' '),\n backgroundPosition: '0 0'\n }\n } else if (type === 'solid') {\n return {\n backgroundColor: tintColor2\n }\n } else if (type === 'side') {\n return {\n backgroundImage: [\n 'linear-gradient(to right,',\n `${solidColor} ,`,\n `${solidColor} 2px,`,\n `transparent 6px`\n ].join(' '),\n backgroundPosition: '0 0'\n }\n }\n}\n\nconst highlightClass = (user) => {\n return 'USER____' + user.screen_name\n .replace(/\\./g, '_')\n .replace(/@/g, '_AT_')\n}\n\nexport {\n highlightClass,\n highlightStyle\n}\n","import Vue from 'vue'\n\nimport './tab_switcher.scss'\n\nexport default Vue.component('tab-switcher', {\n name: 'TabSwitcher',\n props: {\n renderOnlyFocused: {\n required: false,\n type: Boolean,\n default: false\n },\n onSwitch: {\n required: false,\n type: Function,\n default: undefined\n },\n activeTab: {\n required: false,\n type: String,\n default: undefined\n },\n scrollableTabs: {\n required: false,\n type: Boolean,\n default: false\n }\n },\n data () {\n return {\n active: this.$slots.default.findIndex(_ => _.tag)\n }\n },\n computed: {\n activeIndex () {\n // In case of controlled component\n if (this.activeTab) {\n return this.$slots.default.findIndex(slot => this.activeTab === slot.key)\n } else {\n return this.active\n }\n }\n },\n beforeUpdate () {\n const currentSlot = this.$slots.default[this.active]\n if (!currentSlot.tag) {\n this.active = this.$slots.default.findIndex(_ => _.tag)\n }\n },\n methods: {\n activateTab (index) {\n return (e) => {\n e.preventDefault()\n if (typeof this.onSwitch === 'function') {\n this.onSwitch.call(null, this.$slots.default[index].key)\n }\n this.active = index\n }\n }\n },\n render (h) {\n const tabs = this.$slots.default\n .map((slot, index) => {\n if (!slot.tag) return\n const classesTab = ['tab']\n const classesWrapper = ['tab-wrapper']\n\n if (this.activeIndex === index) {\n classesTab.push('active')\n classesWrapper.push('active')\n }\n if (slot.data.attrs.image) {\n return (\n

\n \n \n {slot.data.attrs.label ? '' : slot.data.attrs.label}\n \n
\n )\n }\n return (\n
\n \n {slot.data.attrs.label}\n
\n )\n })\n\n const contents = this.$slots.default.map((slot, index) => {\n if (!slot.tag) return\n const active = this.activeIndex === index\n if (this.renderOnlyFocused) {\n return active\n ?
{slot}
\n :
\n }\n return
{slot}
\n })\n\n return (\n
\n
\n {tabs}\n
\n
\n {contents}\n
\n
\n )\n }\n})\n","/* eslint-env browser */\nimport statusPosterService from '../../services/status_poster/status_poster.service.js'\nimport fileSizeFormatService from '../../services/file_size_format/file_size_format.js'\n\nconst mediaUpload = {\n data () {\n return {\n uploading: false,\n uploadReady: true\n }\n },\n methods: {\n uploadFile (file) {\n const self = this\n const store = this.$store\n if (file.size > store.state.instance.uploadlimit) {\n const filesize = fileSizeFormatService.fileSizeFormat(file.size)\n const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit)\n self.$emit('upload-failed', 'file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })\n return\n }\n const formData = new FormData()\n formData.append('file', file)\n\n self.$emit('uploading')\n self.uploading = true\n\n statusPosterService.uploadMedia({ store, formData })\n .then((fileData) => {\n self.$emit('uploaded', fileData)\n self.uploading = false\n }, (error) => { // eslint-disable-line handle-callback-err\n self.$emit('upload-failed', 'default')\n self.uploading = false\n })\n },\n fileDrop (e) {\n if (e.dataTransfer.files.length > 0) {\n e.preventDefault() // allow dropping text like before\n this.uploadFile(e.dataTransfer.files[0])\n }\n },\n fileDrag (e) {\n let types = e.dataTransfer.types\n if (types.contains('Files')) {\n e.dataTransfer.dropEffect = 'copy'\n } else {\n e.dataTransfer.dropEffect = 'none'\n }\n },\n clearFile () {\n this.uploadReady = false\n this.$nextTick(() => {\n this.uploadReady = true\n })\n },\n change ({ target }) {\n for (var i = 0; i < target.files.length; i++) {\n let file = target.files[i]\n this.uploadFile(file)\n }\n }\n },\n props: [\n 'dropFiles'\n ],\n watch: {\n 'dropFiles': function (fileInfos) {\n if (!this.uploading) {\n this.uploadFile(fileInfos[0])\n }\n }\n }\n}\n\nexport default mediaUpload\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./media_upload.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./media_upload.js\"\nimport __vue_script__ from \"!!babel-loader!./media_upload.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-74382032\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./media_upload.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"media-upload\",on:{\"drop\":[function($event){$event.preventDefault();},_vm.fileDrop],\"dragover\":function($event){$event.preventDefault();return _vm.fileDrag($event)}}},[_c('label',{staticClass:\"label\",attrs:{\"title\":_vm.$t('tool_tip.media_upload')}},[(_vm.uploading)?_c('i',{staticClass:\"progress-icon icon-spin4 animate-spin\"}):_vm._e(),_vm._v(\" \"),(!_vm.uploading)?_c('i',{staticClass:\"new-icon icon-upload\"}):_vm._e(),_vm._v(\" \"),(_vm.uploadReady)?_c('input',{staticStyle:{\"position\":\"fixed\",\"top\":\"-100em\"},attrs:{\"type\":\"file\",\"multiple\":\"true\"},on:{\"change\":_vm.change}}):_vm._e()])])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import * as DateUtils from 'src/services/date_utils/date_utils.js'\nimport { uniq } from 'lodash'\n\nexport default {\n name: 'PollForm',\n props: ['visible'],\n data: () => ({\n pollType: 'single',\n options: ['', ''],\n expiryAmount: 10,\n expiryUnit: 'minutes'\n }),\n computed: {\n pollLimits () {\n return this.$store.state.instance.pollLimits\n },\n maxOptions () {\n return this.pollLimits.max_options\n },\n maxLength () {\n return this.pollLimits.max_option_chars\n },\n expiryUnits () {\n const allUnits = ['minutes', 'hours', 'days']\n const expiry = this.convertExpiryFromUnit\n return allUnits.filter(\n unit => this.pollLimits.max_expiration >= expiry(unit, 1)\n )\n },\n minExpirationInCurrentUnit () {\n return Math.ceil(\n this.convertExpiryToUnit(\n this.expiryUnit,\n this.pollLimits.min_expiration\n )\n )\n },\n maxExpirationInCurrentUnit () {\n return Math.floor(\n this.convertExpiryToUnit(\n this.expiryUnit,\n this.pollLimits.max_expiration\n )\n )\n }\n },\n methods: {\n clear () {\n this.pollType = 'single'\n this.options = ['', '']\n this.expiryAmount = 10\n this.expiryUnit = 'minutes'\n },\n nextOption (index) {\n const element = this.$el.querySelector(`#poll-${index + 1}`)\n if (element) {\n element.focus()\n } else {\n // Try adding an option and try focusing on it\n const addedOption = this.addOption()\n if (addedOption) {\n this.$nextTick(function () {\n this.nextOption(index)\n })\n }\n }\n },\n addOption () {\n if (this.options.length < this.maxOptions) {\n this.options.push('')\n return true\n }\n return false\n },\n deleteOption (index, event) {\n if (this.options.length > 2) {\n this.options.splice(index, 1)\n }\n },\n convertExpiryToUnit (unit, amount) {\n // Note: we want seconds and not milliseconds\n switch (unit) {\n case 'minutes': return (1000 * amount) / DateUtils.MINUTE\n case 'hours': return (1000 * amount) / DateUtils.HOUR\n case 'days': return (1000 * amount) / DateUtils.DAY\n }\n },\n convertExpiryFromUnit (unit, amount) {\n // Note: we want seconds and not milliseconds\n switch (unit) {\n case 'minutes': return 0.001 * amount * DateUtils.MINUTE\n case 'hours': return 0.001 * amount * DateUtils.HOUR\n case 'days': return 0.001 * amount * DateUtils.DAY\n }\n },\n expiryAmountChange () {\n this.expiryAmount =\n Math.max(this.minExpirationInCurrentUnit, this.expiryAmount)\n this.expiryAmount =\n Math.min(this.maxExpirationInCurrentUnit, this.expiryAmount)\n this.updatePollToParent()\n },\n updatePollToParent () {\n const expiresIn = this.convertExpiryFromUnit(\n this.expiryUnit,\n this.expiryAmount\n )\n\n const options = uniq(this.options.filter(option => option !== ''))\n if (options.length < 2) {\n this.$emit('update-poll', { error: this.$t('polls.not_enough_options') })\n return\n }\n this.$emit('update-poll', {\n options,\n multiple: this.pollType === 'multiple',\n expiresIn\n })\n }\n }\n}\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./poll_form.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./poll_form.js\"\nimport __vue_script__ from \"!!babel-loader!./poll_form.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-1f896331\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./poll_form.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.visible)?_c('div',{staticClass:\"poll-form\"},[_vm._l((_vm.options),function(option,index){return _c('div',{key:index,staticClass:\"poll-option\"},[_c('div',{staticClass:\"input-container\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.options[index]),expression:\"options[index]\"}],staticClass:\"poll-option-input\",attrs:{\"id\":(\"poll-\" + index),\"type\":\"text\",\"placeholder\":_vm.$t('polls.option'),\"maxlength\":_vm.maxLength},domProps:{\"value\":(_vm.options[index])},on:{\"change\":_vm.updatePollToParent,\"keydown\":function($event){if(!('button' in $event)&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }$event.stopPropagation();$event.preventDefault();_vm.nextOption(index)},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(_vm.options, index, $event.target.value)}}})]),_vm._v(\" \"),(_vm.options.length > 2)?_c('div',{staticClass:\"icon-container\"},[_c('i',{staticClass:\"icon-cancel\",on:{\"click\":function($event){_vm.deleteOption(index)}}})]):_vm._e()])}),_vm._v(\" \"),(_vm.options.length < _vm.maxOptions)?_c('a',{staticClass:\"add-option faint\",on:{\"click\":_vm.addOption}},[_c('i',{staticClass:\"icon-plus\"}),_vm._v(\"\\n \"+_vm._s(_vm.$t(\"polls.add_option\"))+\"\\n \")]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"poll-type-expiry\"},[_c('div',{staticClass:\"poll-type\",attrs:{\"title\":_vm.$t('polls.type')}},[_c('label',{staticClass:\"select\",attrs:{\"for\":\"poll-type-selector\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.pollType),expression:\"pollType\"}],staticClass:\"select\",on:{\"change\":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.pollType=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},_vm.updatePollToParent]}},[_c('option',{attrs:{\"value\":\"single\"}},[_vm._v(_vm._s(_vm.$t('polls.single_choice')))]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"multiple\"}},[_vm._v(_vm._s(_vm.$t('polls.multiple_choices')))])]),_vm._v(\" \"),_c('i',{staticClass:\"icon-down-open\"})])]),_vm._v(\" \"),_c('div',{staticClass:\"poll-expiry\",attrs:{\"title\":_vm.$t('polls.expiry')}},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.expiryAmount),expression:\"expiryAmount\"}],staticClass:\"expiry-amount hide-number-spinner\",attrs:{\"type\":\"number\",\"min\":_vm.minExpirationInCurrentUnit,\"max\":_vm.maxExpirationInCurrentUnit},domProps:{\"value\":(_vm.expiryAmount)},on:{\"change\":_vm.expiryAmountChange,\"input\":function($event){if($event.target.composing){ return; }_vm.expiryAmount=$event.target.value}}}),_vm._v(\" \"),_c('label',{staticClass:\"expiry-unit select\"},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.expiryUnit),expression:\"expiryUnit\"}],on:{\"change\":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.expiryUnit=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},_vm.expiryAmountChange]}},_vm._l((_vm.expiryUnits),function(unit){return _c('option',{key:unit,domProps:{\"value\":unit}},[_vm._v(\"\\n \"+_vm._s(_vm.$t((\"time.\" + unit + \"_short\"), ['']))+\"\\n \")])}),0),_vm._v(\" \"),_c('i',{staticClass:\"icon-down-open\"})])])])],2):_vm._e()}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import statusPoster from '../../services/status_poster/status_poster.service.js'\nimport MediaUpload from '../media_upload/media_upload.vue'\nimport ScopeSelector from '../scope_selector/scope_selector.vue'\nimport EmojiInput from '../emoji_input/emoji_input.vue'\nimport PollForm from '../poll/poll_form.vue'\nimport fileTypeService from '../../services/file_type/file_type.service.js'\nimport { findOffset } from '../../services/offset_finder/offset_finder.service.js'\nimport { reject, map, uniqBy } from 'lodash'\nimport suggestor from '../emoji_input/suggestor.js'\nimport { mapGetters } from 'vuex'\nimport Checkbox from '../checkbox/checkbox.vue'\n\nconst buildMentionsString = ({ user, attentions = [] }, currentUser) => {\n let allAttentions = [...attentions]\n\n allAttentions.unshift(user)\n\n allAttentions = uniqBy(allAttentions, 'id')\n allAttentions = reject(allAttentions, { id: currentUser.id })\n\n let mentions = map(allAttentions, (attention) => {\n return `@${attention.screen_name}`\n })\n\n return mentions.length > 0 ? mentions.join(' ') + ' ' : ''\n}\n\nconst PostStatusForm = {\n props: [\n 'replyTo',\n 'repliedUser',\n 'attentions',\n 'copyMessageScope',\n 'subject'\n ],\n components: {\n MediaUpload,\n EmojiInput,\n PollForm,\n ScopeSelector,\n Checkbox\n },\n mounted () {\n this.resize(this.$refs.textarea)\n const textLength = this.$refs.textarea.value.length\n this.$refs.textarea.setSelectionRange(textLength, textLength)\n\n if (this.replyTo) {\n this.$refs.textarea.focus()\n }\n },\n data () {\n const preset = this.$route.query.message\n let statusText = preset || ''\n\n const { scopeCopy } = this.$store.getters.mergedConfig\n\n if (this.replyTo) {\n const currentUser = this.$store.state.users.currentUser\n statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)\n }\n\n const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct')\n ? this.copyMessageScope\n : this.$store.state.users.currentUser.default_scope\n\n const { postContentType: contentType } = this.$store.getters.mergedConfig\n\n return {\n dropFiles: [],\n submitDisabled: false,\n error: null,\n posting: false,\n highlighted: 0,\n newStatus: {\n spoilerText: this.subject || '',\n status: statusText,\n nsfw: false,\n files: [],\n poll: {},\n visibility: scope,\n contentType\n },\n caret: 0,\n pollFormVisible: false\n }\n },\n computed: {\n users () {\n return this.$store.state.users.users\n },\n userDefaultScope () {\n return this.$store.state.users.currentUser.default_scope\n },\n showAllScopes () {\n return !this.mergedConfig.minimalScopesMode\n },\n emojiUserSuggestor () {\n return suggestor({\n emoji: [\n ...this.$store.state.instance.emoji,\n ...this.$store.state.instance.customEmoji\n ],\n users: this.$store.state.users.users,\n updateUsersList: (input) => this.$store.dispatch('searchUsers', input)\n })\n },\n emojiSuggestor () {\n return suggestor({\n emoji: [\n ...this.$store.state.instance.emoji,\n ...this.$store.state.instance.customEmoji\n ]\n })\n },\n emoji () {\n return this.$store.state.instance.emoji || []\n },\n customEmoji () {\n return this.$store.state.instance.customEmoji || []\n },\n statusLength () {\n return this.newStatus.status.length\n },\n spoilerTextLength () {\n return this.newStatus.spoilerText.length\n },\n statusLengthLimit () {\n return this.$store.state.instance.textlimit\n },\n hasStatusLengthLimit () {\n return this.statusLengthLimit > 0\n },\n charactersLeft () {\n return this.statusLengthLimit - (this.statusLength + this.spoilerTextLength)\n },\n isOverLengthLimit () {\n return this.hasStatusLengthLimit && (this.charactersLeft < 0)\n },\n minimalScopesMode () {\n return this.$store.state.instance.minimalScopesMode\n },\n alwaysShowSubject () {\n return this.mergedConfig.alwaysShowSubjectInput\n },\n postFormats () {\n return this.$store.state.instance.postFormats || []\n },\n safeDMEnabled () {\n return this.$store.state.instance.safeDM\n },\n pollsAvailable () {\n return this.$store.state.instance.pollsAvailable &&\n this.$store.state.instance.pollLimits.max_options >= 2\n },\n hideScopeNotice () {\n return this.$store.getters.mergedConfig.hideScopeNotice\n },\n pollContentError () {\n return this.pollFormVisible &&\n this.newStatus.poll &&\n this.newStatus.poll.error\n },\n ...mapGetters(['mergedConfig'])\n },\n methods: {\n postStatus (newStatus) {\n if (this.posting) { return }\n if (this.submitDisabled) { return }\n\n if (this.newStatus.status === '') {\n if (this.newStatus.files.length === 0) {\n this.error = 'Cannot post an empty status with no files'\n return\n }\n }\n\n const poll = this.pollFormVisible ? this.newStatus.poll : {}\n if (this.pollContentError) {\n this.error = this.pollContentError\n return\n }\n\n this.posting = true\n statusPoster.postStatus({\n status: newStatus.status,\n spoilerText: newStatus.spoilerText || null,\n visibility: newStatus.visibility,\n sensitive: newStatus.nsfw,\n media: newStatus.files,\n store: this.$store,\n inReplyToStatusId: this.replyTo,\n contentType: newStatus.contentType,\n poll\n }).then((data) => {\n if (!data.error) {\n this.newStatus = {\n status: '',\n spoilerText: '',\n files: [],\n visibility: newStatus.visibility,\n contentType: newStatus.contentType,\n poll: {}\n }\n this.pollFormVisible = false\n this.$refs.mediaUpload.clearFile()\n this.clearPollForm()\n this.$emit('posted')\n let el = this.$el.querySelector('textarea')\n el.style.height = 'auto'\n el.style.height = undefined\n this.error = null\n } else {\n this.error = data.error\n }\n this.posting = false\n })\n },\n addMediaFile (fileInfo) {\n this.newStatus.files.push(fileInfo)\n this.enableSubmit()\n },\n removeMediaFile (fileInfo) {\n let index = this.newStatus.files.indexOf(fileInfo)\n this.newStatus.files.splice(index, 1)\n },\n uploadFailed (errString, templateArgs) {\n templateArgs = templateArgs || {}\n this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)\n this.enableSubmit()\n },\n disableSubmit () {\n this.submitDisabled = true\n },\n enableSubmit () {\n this.submitDisabled = false\n },\n type (fileInfo) {\n return fileTypeService.fileType(fileInfo.mimetype)\n },\n paste (e) {\n this.resize(e)\n if (e.clipboardData.files.length > 0) {\n // prevent pasting of file as text\n e.preventDefault()\n // Strangely, files property gets emptied after event propagation\n // Trying to wrap it in array doesn't work. Plus I doubt it's possible\n // to hold more than one file in clipboard.\n this.dropFiles = [e.clipboardData.files[0]]\n }\n },\n fileDrop (e) {\n if (e.dataTransfer.files.length > 0) {\n e.preventDefault() // allow dropping text like before\n this.dropFiles = e.dataTransfer.files\n }\n },\n fileDrag (e) {\n e.dataTransfer.dropEffect = 'copy'\n },\n onEmojiInputInput (e) {\n this.$nextTick(() => {\n this.resize(this.$refs['textarea'])\n })\n },\n resize (e) {\n const target = e.target || e\n if (!(target instanceof window.Element)) { return }\n\n // Reset to default height for empty form, nothing else to do here.\n if (target.value === '') {\n target.style.height = null\n this.$refs['emoji-input'].resize()\n return\n }\n\n const formRef = this.$refs['form']\n const bottomRef = this.$refs['bottom']\n /* Scroller is either `window` (replies in TL), sidebar (main post form,\n * replies in notifs) or mobile post form. Note that getting and setting\n * scroll is different for `Window` and `Element`s\n */\n const bottomBottomPaddingStr = window.getComputedStyle(bottomRef)['padding-bottom']\n const bottomBottomPadding = Number(bottomBottomPaddingStr.substring(0, bottomBottomPaddingStr.length - 2))\n\n const scrollerRef = this.$el.closest('.sidebar-scroller') ||\n this.$el.closest('.post-form-modal-view') ||\n window\n\n // Getting info about padding we have to account for, removing 'px' part\n const topPaddingStr = window.getComputedStyle(target)['padding-top']\n const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom']\n const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))\n const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2))\n const vertPadding = topPadding + bottomPadding\n\n /* Explanation:\n *\n * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight\n * scrollHeight returns element's scrollable content height, i.e. visible\n * element + overscrolled parts of it. We use it to determine when text\n * inside the textarea exceeded its height, so we can set height to prevent\n * overscroll, i.e. make textarea grow with the text. HOWEVER, since we\n * explicitly set new height, scrollHeight won't go below that, so we can't\n * SHRINK the textarea when there's extra space. To workaround that we set\n * height to 'auto' which makes textarea tiny again, so that scrollHeight\n * will match text height again. HOWEVER, shrinking textarea can screw with\n * the scroll since there might be not enough padding around form-bottom to even\n * warrant a scroll, so it will jump to 0 and refuse to move anywhere,\n * so we check current scroll position before shrinking and then restore it\n * with needed delta.\n */\n\n // this part has to be BEFORE the content size update\n const currentScroll = scrollerRef === window\n ? scrollerRef.scrollY\n : scrollerRef.scrollTop\n const scrollerHeight = scrollerRef === window\n ? scrollerRef.innerHeight\n : scrollerRef.offsetHeight\n const scrollerBottomBorder = currentScroll + scrollerHeight\n\n // BEGIN content size update\n target.style.height = 'auto'\n const newHeight = target.scrollHeight - vertPadding\n target.style.height = `${newHeight}px`\n // END content size update\n\n // We check where the bottom border of form-bottom element is, this uses findOffset\n // to find offset relative to scrollable container (scroller)\n const bottomBottomBorder = bottomRef.offsetHeight + findOffset(bottomRef, scrollerRef).top + bottomBottomPadding\n\n const isBottomObstructed = scrollerBottomBorder < bottomBottomBorder\n const isFormBiggerThanScroller = scrollerHeight < formRef.offsetHeight\n const bottomChangeDelta = bottomBottomBorder - scrollerBottomBorder\n // The intention is basically this;\n // Keep form-bottom always visible so that submit button is in view EXCEPT\n // if form element bigger than scroller and caret isn't at the end, so that\n // if you scroll up and edit middle of text you won't get scrolled back to bottom\n const shouldScrollToBottom = isBottomObstructed &&\n !(isFormBiggerThanScroller &&\n this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)\n const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0\n const targetScroll = currentScroll + totalDelta\n\n if (scrollerRef === window) {\n scrollerRef.scroll(0, targetScroll)\n } else {\n scrollerRef.scrollTop = targetScroll\n }\n\n this.$refs['emoji-input'].resize()\n },\n showEmojiPicker () {\n this.$refs['textarea'].focus()\n this.$refs['emoji-input'].triggerShowPicker()\n },\n clearError () {\n this.error = null\n },\n changeVis (visibility) {\n this.newStatus.visibility = visibility\n },\n togglePollForm () {\n this.pollFormVisible = !this.pollFormVisible\n },\n setPoll (poll) {\n this.newStatus.poll = poll\n },\n clearPollForm () {\n if (this.$refs.pollForm) {\n this.$refs.pollForm.clear()\n }\n },\n dismissScopeNotice () {\n this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })\n }\n }\n}\n\nexport default PostStatusForm\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./post_status_form.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./post_status_form.js\"\nimport __vue_script__ from \"!!babel-loader!./post_status_form.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-c2ba770c\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./post_status_form.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{ref:\"form\",staticClass:\"post-status-form\"},[_c('form',{attrs:{\"autocomplete\":\"off\"},on:{\"submit\":function($event){$event.preventDefault();_vm.postStatus(_vm.newStatus)}}},[_c('div',{staticClass:\"form-group\"},[(!_vm.$store.state.users.currentUser.locked && _vm.newStatus.visibility == 'private')?_c('i18n',{staticClass:\"visibility-notice\",attrs:{\"path\":\"post_status.account_not_locked_warning\",\"tag\":\"p\"}},[_c('router-link',{attrs:{\"to\":{ name: 'user-settings' }}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('post_status.account_not_locked_warning_link'))+\"\\n \")])],1):_vm._e(),_vm._v(\" \"),(!_vm.hideScopeNotice && _vm.newStatus.visibility === 'public')?_c('p',{staticClass:\"visibility-notice notice-dismissible\"},[_c('span',[_vm._v(_vm._s(_vm.$t('post_status.scope_notice.public')))]),_vm._v(\" \"),_c('a',{staticClass:\"button-icon dismiss\",on:{\"click\":function($event){$event.preventDefault();_vm.dismissScopeNotice()}}},[_c('i',{staticClass:\"icon-cancel\"})])]):(!_vm.hideScopeNotice && _vm.newStatus.visibility === 'unlisted')?_c('p',{staticClass:\"visibility-notice notice-dismissible\"},[_c('span',[_vm._v(_vm._s(_vm.$t('post_status.scope_notice.unlisted')))]),_vm._v(\" \"),_c('a',{staticClass:\"button-icon dismiss\",on:{\"click\":function($event){$event.preventDefault();_vm.dismissScopeNotice()}}},[_c('i',{staticClass:\"icon-cancel\"})])]):(!_vm.hideScopeNotice && _vm.newStatus.visibility === 'private' && _vm.$store.state.users.currentUser.locked)?_c('p',{staticClass:\"visibility-notice notice-dismissible\"},[_c('span',[_vm._v(_vm._s(_vm.$t('post_status.scope_notice.private')))]),_vm._v(\" \"),_c('a',{staticClass:\"button-icon dismiss\",on:{\"click\":function($event){$event.preventDefault();_vm.dismissScopeNotice()}}},[_c('i',{staticClass:\"icon-cancel\"})])]):(_vm.newStatus.visibility === 'direct')?_c('p',{staticClass:\"visibility-notice\"},[(_vm.safeDMEnabled)?_c('span',[_vm._v(_vm._s(_vm.$t('post_status.direct_warning_to_first_only')))]):_c('span',[_vm._v(_vm._s(_vm.$t('post_status.direct_warning_to_all')))])]):_vm._e(),_vm._v(\" \"),(_vm.newStatus.spoilerText || _vm.alwaysShowSubject)?_c('EmojiInput',{staticClass:\"form-control\",attrs:{\"enable-emoji-picker\":\"\",\"suggest\":_vm.emojiSuggestor},model:{value:(_vm.newStatus.spoilerText),callback:function ($$v) {_vm.$set(_vm.newStatus, \"spoilerText\", $$v)},expression:\"newStatus.spoilerText\"}},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.newStatus.spoilerText),expression:\"newStatus.spoilerText\"}],staticClass:\"form-post-subject\",attrs:{\"type\":\"text\",\"placeholder\":_vm.$t('post_status.content_warning')},domProps:{\"value\":(_vm.newStatus.spoilerText)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.$set(_vm.newStatus, \"spoilerText\", $event.target.value)}}})]):_vm._e(),_vm._v(\" \"),_c('EmojiInput',{ref:\"emoji-input\",staticClass:\"form-control main-input\",attrs:{\"suggest\":_vm.emojiUserSuggestor,\"enable-emoji-picker\":\"\",\"hide-emoji-button\":\"\",\"enable-sticker-picker\":\"\"},on:{\"input\":_vm.onEmojiInputInput,\"sticker-uploaded\":_vm.addMediaFile,\"sticker-upload-failed\":_vm.uploadFailed},model:{value:(_vm.newStatus.status),callback:function ($$v) {_vm.$set(_vm.newStatus, \"status\", $$v)},expression:\"newStatus.status\"}},[_c('textarea',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.newStatus.status),expression:\"newStatus.status\"}],ref:\"textarea\",staticClass:\"form-post-body\",attrs:{\"placeholder\":_vm.$t('post_status.default'),\"rows\":\"1\",\"disabled\":_vm.posting},domProps:{\"value\":(_vm.newStatus.status)},on:{\"keydown\":function($event){if(!('button' in $event)&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }if(!$event.metaKey){ return null; }_vm.postStatus(_vm.newStatus)},\"keyup\":function($event){if(!('button' in $event)&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }if(!$event.ctrlKey){ return null; }_vm.postStatus(_vm.newStatus)},\"drop\":_vm.fileDrop,\"dragover\":function($event){$event.preventDefault();return _vm.fileDrag($event)},\"input\":[function($event){if($event.target.composing){ return; }_vm.$set(_vm.newStatus, \"status\", $event.target.value)},_vm.resize],\"compositionupdate\":_vm.resize,\"paste\":_vm.paste}}),_vm._v(\" \"),(_vm.hasStatusLengthLimit)?_c('p',{staticClass:\"character-counter faint\",class:{ error: _vm.isOverLengthLimit }},[_vm._v(\"\\n \"+_vm._s(_vm.charactersLeft)+\"\\n \")]):_vm._e()]),_vm._v(\" \"),_c('div',{staticClass:\"visibility-tray\"},[_c('scope-selector',{attrs:{\"show-all\":_vm.showAllScopes,\"user-default\":_vm.userDefaultScope,\"original-scope\":_vm.copyMessageScope,\"initial-scope\":_vm.newStatus.visibility,\"on-scope-change\":_vm.changeVis}}),_vm._v(\" \"),(_vm.postFormats.length > 1)?_c('div',{staticClass:\"text-format\"},[_c('label',{staticClass:\"select\",attrs:{\"for\":\"post-content-type\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.newStatus.contentType),expression:\"newStatus.contentType\"}],staticClass:\"form-control\",attrs:{\"id\":\"post-content-type\"},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.$set(_vm.newStatus, \"contentType\", $event.target.multiple ? $$selectedVal : $$selectedVal[0])}}},_vm._l((_vm.postFormats),function(postFormat){return _c('option',{key:postFormat,domProps:{\"value\":postFormat}},[_vm._v(\"\\n \"+_vm._s(_vm.$t((\"post_status.content_type[\\\"\" + postFormat + \"\\\"]\")))+\"\\n \")])}),0),_vm._v(\" \"),_c('i',{staticClass:\"icon-down-open\"})])]):_vm._e(),_vm._v(\" \"),(_vm.postFormats.length === 1 && _vm.postFormats[0] !== 'text/plain')?_c('div',{staticClass:\"text-format\"},[_c('span',{staticClass:\"only-format\"},[_vm._v(\"\\n \"+_vm._s(_vm.$t((\"post_status.content_type[\\\"\" + (_vm.postFormats[0]) + \"\\\"]\")))+\"\\n \")])]):_vm._e()],1)],1),_vm._v(\" \"),(_vm.pollsAvailable)?_c('poll-form',{ref:\"pollForm\",attrs:{\"visible\":_vm.pollFormVisible},on:{\"update-poll\":_vm.setPoll}}):_vm._e(),_vm._v(\" \"),_c('div',{ref:\"bottom\",staticClass:\"form-bottom\"},[_c('div',{staticClass:\"form-bottom-left\"},[_c('media-upload',{ref:\"mediaUpload\",staticClass:\"media-upload-icon\",attrs:{\"drop-files\":_vm.dropFiles},on:{\"uploading\":_vm.disableSubmit,\"uploaded\":_vm.addMediaFile,\"upload-failed\":_vm.uploadFailed}}),_vm._v(\" \"),_c('div',{staticClass:\"emoji-icon\"},[_c('i',{staticClass:\"icon-smile btn btn-default\",attrs:{\"title\":_vm.$t('emoji.add_emoji')},on:{\"click\":_vm.showEmojiPicker}})]),_vm._v(\" \"),(_vm.pollsAvailable)?_c('div',{staticClass:\"poll-icon\",class:{ selected: _vm.pollFormVisible }},[_c('i',{staticClass:\"icon-chart-bar btn btn-default\",attrs:{\"title\":_vm.$t('polls.add_poll')},on:{\"click\":_vm.togglePollForm}})]):_vm._e()],1),_vm._v(\" \"),(_vm.posting)?_c('button',{staticClass:\"btn btn-default\",attrs:{\"disabled\":\"\"}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('post_status.posting'))+\"\\n \")]):(_vm.isOverLengthLimit)?_c('button',{staticClass:\"btn btn-default\",attrs:{\"disabled\":\"\"}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('general.submit'))+\"\\n \")]):_c('button',{staticClass:\"btn btn-default\",attrs:{\"disabled\":_vm.submitDisabled,\"type\":\"submit\"}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('general.submit'))+\"\\n \")])]),_vm._v(\" \"),(_vm.error)?_c('div',{staticClass:\"alert error\"},[_vm._v(\"\\n Error: \"+_vm._s(_vm.error)+\"\\n \"),_c('i',{staticClass:\"button-icon icon-cancel\",on:{\"click\":_vm.clearError}})]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"attachments\"},_vm._l((_vm.newStatus.files),function(file){return _c('div',{key:file.url,staticClass:\"media-upload-wrapper\"},[_c('i',{staticClass:\"fa button-icon icon-cancel\",on:{\"click\":function($event){_vm.removeMediaFile(file)}}}),_vm._v(\" \"),_c('div',{staticClass:\"media-upload-container attachment\"},[(_vm.type(file) === 'image')?_c('img',{staticClass:\"thumbnail media-upload\",attrs:{\"src\":file.url}}):_vm._e(),_vm._v(\" \"),(_vm.type(file) === 'video')?_c('video',{attrs:{\"src\":file.url,\"controls\":\"\"}}):_vm._e(),_vm._v(\" \"),(_vm.type(file) === 'audio')?_c('audio',{attrs:{\"src\":file.url,\"controls\":\"\"}}):_vm._e(),_vm._v(\" \"),(_vm.type(file) === 'unknown')?_c('a',{attrs:{\"href\":file.url}},[_vm._v(_vm._s(file.url))]):_vm._e()])])}),0),_vm._v(\" \"),(_vm.newStatus.files.length > 0)?_c('div',{staticClass:\"upload_settings\"},[_c('Checkbox',{model:{value:(_vm.newStatus.nsfw),callback:function ($$v) {_vm.$set(_vm.newStatus, \"nsfw\", $$v)},expression:\"newStatus.nsfw\"}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('post_status.attachments_sensitive'))+\"\\n \")])],1):_vm._e()],1)])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","const StillImage = {\n props: [\n 'src',\n 'referrerpolicy',\n 'mimetype',\n 'imageLoadError',\n 'imageLoadHandler'\n ],\n data () {\n return {\n stopGifs: this.$store.getters.mergedConfig.stopGifs\n }\n },\n computed: {\n animated () {\n return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif'))\n }\n },\n methods: {\n onLoad () {\n this.imageLoadHandler && this.imageLoadHandler(this.$refs.src)\n const canvas = this.$refs.canvas\n if (!canvas) return\n const width = this.$refs.src.naturalWidth\n const height = this.$refs.src.naturalHeight\n canvas.width = width\n canvas.height = height\n canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)\n },\n onError () {\n this.imageLoadError && this.imageLoadError()\n }\n }\n}\n\nexport default StillImage\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./still-image.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./still-image.js\"\nimport __vue_script__ from \"!!babel-loader!./still-image.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-1bc509fc\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./still-image.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"still-image\",class:{ animated: _vm.animated }},[(_vm.animated)?_c('canvas',{ref:\"canvas\"}):_vm._e(),_vm._v(\" \"),_c('img',{key:_vm.src,ref:\"src\",attrs:{\"src\":_vm.src,\"referrerpolicy\":_vm.referrerpolicy},on:{\"load\":_vm.onLoad,\"error\":_vm.onError}})])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","\n\n\n","/* script */\nexport * from \"!!babel-loader!../../../node_modules/vue-loader/lib/selector?type=script&index=0!./timeago.vue\"\nimport __vue_script__ from \"!!babel-loader!../../../node_modules/vue-loader/lib/selector?type=script&index=0!./timeago.vue\"\n/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-ac499830\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./timeago.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = null\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('time',{attrs:{\"datetime\":_vm.time,\"title\":_vm.localeDateString}},[_vm._v(\"\\n \"+_vm._s(_vm.$t(_vm.relativeTime.key, [_vm.relativeTime.num]))+\"\\n\")])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","const fileSizeFormat = (num) => {\n var exponent\n var unit\n var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']\n if (num < 1) {\n return num + ' ' + units[0]\n }\n\n exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)\n num = (num / Math.pow(1024, exponent)).toFixed(2) * 1\n unit = units[exponent]\n return { num: num, unit: unit }\n}\nconst fileSizeFormatService = {\n fileSizeFormat\n}\nexport default fileSizeFormatService\n","import { debounce } from 'lodash'\n/**\n * suggest - generates a suggestor function to be used by emoji-input\n * data: object providing source information for specific types of suggestions:\n * data.emoji - optional, an array of all emoji available i.e.\n * (state.instance.emoji + state.instance.customEmoji)\n * data.users - optional, an array of all known users\n * updateUsersList - optional, a function to search and append to users\n *\n * Depending on data present one or both (or none) can be present, so if field\n * doesn't support user linking you can just provide only emoji.\n */\n\nconst debounceUserSearch = debounce((data, input) => {\n data.updateUsersList(input)\n}, 500, { leading: true, trailing: false })\n\nexport default data => input => {\n const firstChar = input[0]\n if (firstChar === ':' && data.emoji) {\n return suggestEmoji(data.emoji)(input)\n }\n if (firstChar === '@' && data.users) {\n return suggestUsers(data)(input)\n }\n return []\n}\n\nexport const suggestEmoji = emojis => input => {\n const noPrefix = input.toLowerCase().substr(1)\n return emojis\n .filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))\n .sort((a, b) => {\n let aScore = 0\n let bScore = 0\n\n // Make custom emojis a priority\n aScore += a.imageUrl ? 10 : 0\n bScore += b.imageUrl ? 10 : 0\n\n // Sort alphabetically\n const alphabetically = a.displayText > b.displayText ? 1 : -1\n\n return bScore - aScore + alphabetically\n })\n}\n\nexport const suggestUsers = data => input => {\n const noPrefix = input.toLowerCase().substr(1)\n const users = data.users\n\n const newUsers = users.filter(\n user =>\n user.screen_name.toLowerCase().startsWith(noPrefix) ||\n user.name.toLowerCase().startsWith(noPrefix)\n\n /* taking only 20 results so that sorting is a bit cheaper, we display\n * only 5 anyway. could be inaccurate, but we ideally we should query\n * backend anyway\n */\n ).slice(0, 20).sort((a, b) => {\n let aScore = 0\n let bScore = 0\n\n // Matches on screen name (i.e. user@instance) makes a priority\n aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0\n bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0\n\n // Matches on name takes second priority\n aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0\n bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0\n\n const diff = (bScore - aScore) * 10\n\n // Then sort alphabetically\n const nameAlphabetically = a.name > b.name ? 1 : -1\n const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1\n\n return diff + nameAlphabetically + screenNameAlphabetically\n /* eslint-disable camelcase */\n }).map(({ screen_name, name, profile_image_url_original }) => ({\n displayText: screen_name,\n detailText: name,\n imageUrl: profile_image_url_original,\n replacement: '@' + screen_name + ' '\n }))\n\n // BE search users if there are no matches\n if (newUsers.length === 0 && data.updateUsersList) {\n debounceUserSearch(data, noPrefix)\n }\n return newUsers\n /* eslint-enable camelcase */\n}\n","import { map } from 'lodash'\nimport apiService from '../api/api.service.js'\n\nconst postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {\n const mediaIds = map(media, 'id')\n\n return apiService.postStatus({\n credentials: store.state.users.currentUser.credentials,\n status,\n spoilerText,\n visibility,\n sensitive,\n mediaIds,\n inReplyToStatusId,\n contentType,\n poll })\n .then((data) => {\n if (!data.error) {\n store.dispatch('addNewStatuses', {\n statuses: [data],\n timeline: 'friends',\n showImmediately: true,\n noIdUpdate: true // To prevent missing notices on next pull.\n })\n }\n return data\n })\n .catch((err) => {\n return {\n error: err.message\n }\n })\n}\n\nconst uploadMedia = ({ store, formData }) => {\n const credentials = store.state.users.currentUser.credentials\n\n return apiService.uploadMedia({ credentials, formData })\n}\n\nconst statusPosterService = {\n postStatus,\n uploadMedia\n}\n\nexport default statusPosterService\n","export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadding = true) => {\n const result = {\n top: top + child.offsetTop,\n left: left + child.offsetLeft\n }\n if (!ignorePadding && child !== window) {\n const { topPadding, leftPadding } = findPadding(child)\n result.top += ignorePadding ? 0 : topPadding\n result.left += ignorePadding ? 0 : leftPadding\n }\n\n if (child.offsetParent && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {\n return findOffset(child.offsetParent, parent, result, false)\n } else {\n if (parent !== window) {\n const { topPadding, leftPadding } = findPadding(parent)\n result.top += topPadding\n result.left += leftPadding\n }\n return result\n }\n}\n\nconst findPadding = (el) => {\n const topPaddingStr = window.getComputedStyle(el)['padding-top']\n const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))\n const leftPaddingStr = window.getComputedStyle(el)['padding-left']\n const leftPadding = Number(leftPaddingStr.substring(0, leftPaddingStr.length - 2))\n\n return { topPadding, leftPadding }\n}\n","import { reduce, find } from 'lodash'\n\nexport const replaceWord = (str, toReplace, replacement) => {\n return str.slice(0, toReplace.start) + replacement + str.slice(toReplace.end)\n}\n\nexport const wordAtPosition = (str, pos) => {\n const words = splitIntoWords(str)\n const wordsWithPosition = addPositionToWords(words)\n\n return find(wordsWithPosition, ({ start, end }) => start <= pos && end > pos)\n}\n\nexport const addPositionToWords = (words) => {\n return reduce(words, (result, word) => {\n const data = {\n word,\n start: 0,\n end: word.length\n }\n\n if (result.length > 0) {\n const previous = result.pop()\n\n data.start += previous.end\n data.end += previous.end\n\n result.push(previous)\n }\n\n result.push(data)\n\n return result\n }, [])\n}\n\nexport const splitIntoWords = (str) => {\n // Split at word boundaries\n const regex = /\\b/\n const triggers = /[@#:]+$/\n\n let split = str.split(regex)\n\n // Add trailing @ and # to the following word.\n const words = reduce(split, (result, word) => {\n if (result.length > 0) {\n let previous = result.pop()\n const matches = previous.match(triggers)\n if (matches) {\n previous = previous.replace(triggers, '')\n word = matches[0] + word\n }\n result.push(previous)\n }\n result.push(word)\n\n return result\n }, [])\n\n return words\n}\n\nconst completion = {\n wordAtPosition,\n addPositionToWords,\n splitIntoWords,\n replaceWord\n}\n\nexport default completion\n","import Checkbox from '../checkbox/checkbox.vue'\n\n// At widest, approximately 20 emoji are visible in a row,\n// loading 3 rows, could be overkill for narrow picker\nconst LOAD_EMOJI_BY = 60\n\n// When to start loading new batch emoji, in pixels\nconst LOAD_EMOJI_MARGIN = 64\n\nconst filterByKeyword = (list, keyword = '') => {\n return list.filter(x => x.displayText.includes(keyword))\n}\n\nconst EmojiPicker = {\n props: {\n enableStickerPicker: {\n required: false,\n type: Boolean,\n default: false\n }\n },\n data () {\n return {\n keyword: '',\n activeGroup: 'custom',\n showingStickers: false,\n groupsScrolledClass: 'scrolled-top',\n keepOpen: false,\n customEmojiBufferSlice: LOAD_EMOJI_BY,\n customEmojiTimeout: null,\n customEmojiLoadAllConfirmed: false\n }\n },\n components: {\n StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),\n Checkbox\n },\n methods: {\n onStickerUploaded (e) {\n this.$emit('sticker-uploaded', e)\n },\n onStickerUploadFailed (e) {\n this.$emit('sticker-upload-failed', e)\n },\n onEmoji (emoji) {\n const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement\n this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })\n },\n onScroll (e) {\n const target = (e && e.target) || this.$refs['emoji-groups']\n this.updateScrolledClass(target)\n this.scrolledGroup(target)\n this.triggerLoadMore(target)\n },\n highlight (key) {\n const ref = this.$refs['group-' + key]\n const top = ref[0].offsetTop\n this.setShowStickers(false)\n this.activeGroup = key\n this.$nextTick(() => {\n this.$refs['emoji-groups'].scrollTop = top + 1\n })\n },\n updateScrolledClass (target) {\n if (target.scrollTop <= 5) {\n this.groupsScrolledClass = 'scrolled-top'\n } else if (target.scrollTop >= target.scrollTopMax - 5) {\n this.groupsScrolledClass = 'scrolled-bottom'\n } else {\n this.groupsScrolledClass = 'scrolled-middle'\n }\n },\n triggerLoadMore (target) {\n const ref = this.$refs['group-end-custom'][0]\n if (!ref) return\n const bottom = ref.offsetTop + ref.offsetHeight\n\n const scrollerBottom = target.scrollTop + target.clientHeight\n const scrollerTop = target.scrollTop\n const scrollerMax = target.scrollHeight\n\n // Loads more emoji when they come into view\n const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN\n // Always load when at the very top in case there's no scroll space yet\n const atTop = scrollerTop < 5\n // Don't load when looking at unicode category or at the very bottom\n const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax\n if (!bottomAboveViewport && (approachingBottom || atTop)) {\n this.loadEmoji()\n }\n },\n scrolledGroup (target) {\n const top = target.scrollTop + 5\n this.$nextTick(() => {\n this.emojisView.forEach(group => {\n const ref = this.$refs['group-' + group.id]\n if (ref[0].offsetTop <= top) {\n this.activeGroup = group.id\n }\n })\n })\n },\n loadEmoji () {\n const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length\n\n if (allLoaded) {\n return\n }\n\n this.customEmojiBufferSlice += LOAD_EMOJI_BY\n },\n startEmojiLoad (forceUpdate = false) {\n if (!forceUpdate) {\n this.keyword = ''\n }\n this.$nextTick(() => {\n this.$refs['emoji-groups'].scrollTop = 0\n })\n const bufferSize = this.customEmojiBuffer.length\n const bufferPrefilledAll = bufferSize === this.filteredEmoji.length\n if (bufferPrefilledAll && !forceUpdate) {\n return\n }\n this.customEmojiBufferSlice = LOAD_EMOJI_BY\n },\n toggleStickers () {\n this.showingStickers = !this.showingStickers\n },\n setShowStickers (value) {\n this.showingStickers = value\n }\n },\n watch: {\n keyword () {\n this.customEmojiLoadAllConfirmed = false\n this.onScroll()\n this.startEmojiLoad(true)\n }\n },\n computed: {\n activeGroupView () {\n return this.showingStickers ? '' : this.activeGroup\n },\n stickersAvailable () {\n if (this.$store.state.instance.stickers) {\n return this.$store.state.instance.stickers.length > 0\n }\n return 0\n },\n filteredEmoji () {\n return filterByKeyword(\n this.$store.state.instance.customEmoji || [],\n this.keyword\n )\n },\n customEmojiBuffer () {\n return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)\n },\n emojis () {\n const standardEmojis = this.$store.state.instance.emoji || []\n const customEmojis = this.customEmojiBuffer\n\n return [\n {\n id: 'custom',\n text: this.$t('emoji.custom'),\n icon: 'icon-smile',\n emojis: customEmojis\n },\n {\n id: 'standard',\n text: this.$t('emoji.unicode'),\n icon: 'icon-picture',\n emojis: filterByKeyword(standardEmojis, this.keyword)\n }\n ]\n },\n emojisView () {\n return this.emojis.filter(value => value.emojis.length > 0)\n },\n stickerPickerEnabled () {\n return (this.$store.state.instance.stickers || []).length !== 0\n }\n }\n}\n\nexport default EmojiPicker\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!./emoji_picker.scss\")\n}\n/* script */\nexport * from \"!!babel-loader!./emoji_picker.js\"\nimport __vue_script__ from \"!!babel-loader!./emoji_picker.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-47d21b3b\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./emoji_picker.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"emoji-picker panel panel-default panel-body\"},[_c('div',{staticClass:\"heading\"},[_c('span',{staticClass:\"emoji-tabs\"},_vm._l((_vm.emojis),function(group){return _c('span',{key:group.id,staticClass:\"emoji-tabs-item\",class:{\n active: _vm.activeGroupView === group.id,\n disabled: group.emojis.length === 0\n },attrs:{\"title\":group.text},on:{\"click\":function($event){$event.preventDefault();_vm.highlight(group.id)}}},[_c('i',{class:group.icon})])}),0),_vm._v(\" \"),(_vm.stickerPickerEnabled)?_c('span',{staticClass:\"additional-tabs\"},[_c('span',{staticClass:\"stickers-tab-icon additional-tabs-item\",class:{active: _vm.showingStickers},attrs:{\"title\":_vm.$t('emoji.stickers')},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleStickers($event)}}},[_c('i',{staticClass:\"icon-star\"})])]):_vm._e()]),_vm._v(\" \"),_c('div',{staticClass:\"content\"},[_c('div',{staticClass:\"emoji-content\",class:{hidden: _vm.showingStickers}},[_c('div',{staticClass:\"emoji-search\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.keyword),expression:\"keyword\"}],staticClass:\"form-control\",attrs:{\"type\":\"text\",\"placeholder\":_vm.$t('emoji.search_emoji')},domProps:{\"value\":(_vm.keyword)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.keyword=$event.target.value}}})]),_vm._v(\" \"),_c('div',{ref:\"emoji-groups\",staticClass:\"emoji-groups\",class:_vm.groupsScrolledClass,on:{\"scroll\":_vm.onScroll}},_vm._l((_vm.emojisView),function(group){return _c('div',{key:group.id,staticClass:\"emoji-group\"},[_c('h6',{ref:'group-' + group.id,refInFor:true,staticClass:\"emoji-group-title\"},[_vm._v(\"\\n \"+_vm._s(group.text)+\"\\n \")]),_vm._v(\" \"),_vm._l((group.emojis),function(emoji){return _c('span',{key:group.id + emoji.displayText,staticClass:\"emoji-item\",attrs:{\"title\":emoji.displayText},on:{\"click\":function($event){$event.stopPropagation();$event.preventDefault();_vm.onEmoji(emoji)}}},[(!emoji.imageUrl)?_c('span',[_vm._v(_vm._s(emoji.replacement))]):_c('img',{attrs:{\"src\":emoji.imageUrl}})])}),_vm._v(\" \"),_c('span',{ref:'group-end-' + group.id,refInFor:true})],2)}),0),_vm._v(\" \"),_c('div',{staticClass:\"keep-open\"},[_c('Checkbox',{model:{value:(_vm.keepOpen),callback:function ($$v) {_vm.keepOpen=$$v},expression:\"keepOpen\"}},[_vm._v(\"\\n \"+_vm._s(_vm.$t('emoji.keep_open'))+\"\\n \")])],1)]),_vm._v(\" \"),(_vm.showingStickers)?_c('div',{staticClass:\"stickers-content\"},[_c('sticker-picker',{on:{\"uploaded\":_vm.onStickerUploaded,\"upload-failed\":_vm.onStickerUploadFailed}})],1):_vm._e()])])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import Completion from '../../services/completion/completion.js'\nimport EmojiPicker from '../emoji_picker/emoji_picker.vue'\nimport { take } from 'lodash'\nimport { findOffset } from '../../services/offset_finder/offset_finder.service.js'\n\n/**\n * EmojiInput - augmented inputs for emoji and autocomplete support in inputs\n * without having to give up the comfort of and