From 3189c44a0cf6821976819112e93e93ad811cba53 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 24 Mar 2020 15:21:40 +0400 Subject: [PATCH 001/118] Remove some TwitterAPI endpoints --- lib/pleroma/web/router.ex | 4 - .../controllers/util_controller.ex | 83 ------------ test/web/twitter_api/util_controller_test.exs | 121 ------------------ 3 files changed, 208 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 3f36f6c1a..c3ea7b626 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -481,10 +481,6 @@ defmodule Pleroma.Web.Router do scope "/api", Pleroma.Web do pipe_through(:config) - get("/help/test", TwitterAPI.UtilController, :help_test) - post("/help/test", TwitterAPI.UtilController, :help_test) - get("/statusnet/config", TwitterAPI.UtilController, :config) - get("/statusnet/version", TwitterAPI.UtilController, :version) get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations) end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 537f9f778..bb08f5426 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -13,7 +13,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Notification alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User - alias Pleroma.Web alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger @@ -48,12 +47,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) - plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) - - def help_test(conn, _params) do - json(conn, "ok") - end - def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do with %User{} = user <- User.get_cached_by_nickname(nick), avatar = User.avatar_url(user) do @@ -95,70 +88,6 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_ end end - def config(%{assigns: %{format: "xml"}} = conn, _params) do - instance = Pleroma.Config.get(:instance) - - response = """ - - - #{Keyword.get(instance, :name)} - #{Web.base_url()} - #{Keyword.get(instance, :limit)} - #{!Keyword.get(instance, :registrations_open)} - - - """ - - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) - end - - def config(conn, _params) do - instance = Pleroma.Config.get(:instance) - - vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - - uploadlimit = %{ - uploadlimit: to_string(Keyword.get(instance, :upload_limit)), - avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), - backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), - bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) - } - - data = %{ - name: Keyword.get(instance, :name), - description: Keyword.get(instance, :description), - server: Web.base_url(), - textlimit: to_string(Keyword.get(instance, :limit)), - uploadlimit: uploadlimit, - closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"), - private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"), - vapidPublicKey: vapid_public_key, - accountActivationRequired: - bool_to_val(Keyword.get(instance, :account_activation_required, false)), - invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)), - safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions])) - } - - managed_config = Keyword.get(instance, :managed_config) - - data = - if managed_config do - pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) - Map.put(data, "pleromafe", pleroma_fe) - else - data - end - - json(conn, %{site: data}) - end - - defp bool_to_val(true), do: "1" - defp bool_to_val(_), do: "0" - defp bool_to_val(true, val, _), do: val - defp bool_to_val(_, _, val), do: val - def frontend_configurations(conn, _params) do config = Pleroma.Config.get(:frontend_configurations, %{}) @@ -167,18 +96,6 @@ def frontend_configurations(conn, _params) do json(conn, config) end - def version(%{assigns: %{format: "xml"}} = conn, _params) do - version = Pleroma.Application.named_version() - - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, "#{version}") - end - - def version(conn, _params) do - json(conn, Pleroma.Application.named_version()) - end - def emoji(conn, _params) do emoji = Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc -> diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 30e54bebd..5ad682b0b 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -177,105 +177,6 @@ test "it updates notification privacy option", %{user: user, conn: conn} do end end - describe "GET /api/statusnet/config" do - test "it returns config in xml format", %{conn: conn} do - instance = Config.get(:instance) - - response = - conn - |> put_req_header("accept", "application/xml") - |> get("/api/statusnet/config") - |> response(:ok) - - assert response == - "\n\n#{Keyword.get(instance, :name)}\n#{ - Pleroma.Web.base_url() - }\n#{Keyword.get(instance, :limit)}\n#{ - !Keyword.get(instance, :registrations_open) - }\n\n\n" - end - - test "it returns config in json format", %{conn: conn} do - instance = Config.get(:instance) - Config.put([:instance, :managed_config], true) - Config.put([:instance, :registrations_open], false) - Config.put([:instance, :invites_enabled], true) - Config.put([:instance, :public], false) - Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) - - response = - conn - |> put_req_header("accept", "application/json") - |> get("/api/statusnet/config") - |> json_response(:ok) - - expected_data = %{ - "site" => %{ - "accountActivationRequired" => "0", - "closed" => "1", - "description" => Keyword.get(instance, :description), - "invitesEnabled" => "1", - "name" => Keyword.get(instance, :name), - "pleromafe" => %{"theme" => "asuka-hospital"}, - "private" => "1", - "safeDMMentionsEnabled" => "0", - "server" => Pleroma.Web.base_url(), - "textlimit" => to_string(Keyword.get(instance, :limit)), - "uploadlimit" => %{ - "avatarlimit" => to_string(Keyword.get(instance, :avatar_upload_limit)), - "backgroundlimit" => to_string(Keyword.get(instance, :background_upload_limit)), - "bannerlimit" => to_string(Keyword.get(instance, :banner_upload_limit)), - "uploadlimit" => to_string(Keyword.get(instance, :upload_limit)) - }, - "vapidPublicKey" => Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - } - } - - assert response == expected_data - end - - test "returns the state of safe_dm_mentions flag", %{conn: conn} do - Config.put([:instance, :safe_dm_mentions], true) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - assert response["site"]["safeDMMentionsEnabled"] == "1" - - Config.put([:instance, :safe_dm_mentions], false) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - assert response["site"]["safeDMMentionsEnabled"] == "0" - end - - test "it returns the managed config", %{conn: conn} do - Config.put([:instance, :managed_config], false) - Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - refute response["site"]["pleromafe"] - - Config.put([:instance, :managed_config], true) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - assert response["site"]["pleromafe"] == %{"theme" => "asuka-hospital"} - end - end - describe "GET /api/pleroma/frontend_configurations" do test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do config = [ @@ -404,28 +305,6 @@ test "with valid permissions and invalid password, it returns an error", %{conn: end end - describe "GET /api/statusnet/version" do - test "it returns version in xml format", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/xml") - |> get("/api/statusnet/version") - |> response(:ok) - - assert response == "#{Pleroma.Application.named_version()}" - end - - test "it returns version in json format", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/json") - |> get("/api/statusnet/version") - |> json_response(:ok) - - assert response == "#{Pleroma.Application.named_version()}" - end - end - describe "POST /main/ostatus - remote_subscribe/2" do setup do: clear_config([:instance, :federating], true) From 1c3f3a12edcdd4f11433e9ed5422b381afd3c5c4 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 26 Mar 2020 16:20:20 +0400 Subject: [PATCH 002/118] Add `characterLimit` and `vapidPublicKey` to nodeinfo --- lib/pleroma/web/nodeinfo/nodeinfo_controller.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 30838b1eb..6947c82b9 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -106,6 +106,7 @@ def raw_nodeinfo do }, staffAccounts: staff_accounts, federation: federation_response, + characterLimit: Config.get([:instance, :limit]), pollLimits: Config.get([:instance, :poll_limits]), postFormats: Config.get([:instance, :allowed_post_formats]), uploadLimits: %{ @@ -125,7 +126,8 @@ def raw_nodeinfo do mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false), features: features, restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), - skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) + skipThreadContainment: Config.get([:instance, :skip_thread_containment], false), + vapidPublicKey: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) } } end From 94a6590e3cb9d5c340bfd589880c19717160706f Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 26 Mar 2020 17:59:45 +0400 Subject: [PATCH 003/118] Partially restore `/api/statusnet/config.json` --- lib/pleroma/web/router.ex | 3 +++ .../web/twitter_api/controllers/util_controller.ex | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c3ea7b626..322b074c2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -482,6 +482,9 @@ defmodule Pleroma.Web.Router do pipe_through(:config) get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations) + + # Deprecated + get("/statusnet/config", TwitterAPI.UtilController, :config) end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index bb08f5426..2fc60da5a 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -88,6 +88,18 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_ end end + # Deprecated in favor of `/nodeinfo` + # https://git.pleroma.social/pleroma/pleroma/-/merge_requests/2327 + # https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1084 + def config(conn, _params) do + json(conn, %{ + site: %{ + textlimit: to_string(Config.get([:instance, :limit])), + vapidPublicKey: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + } + }) + end + def frontend_configurations(conn, _params) do config = Pleroma.Config.get(:frontend_configurations, %{}) From 9cf4c4fa73e68f03791c5cc70505b710be39b677 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 23 Apr 2020 14:12:42 +0400 Subject: [PATCH 004/118] Remove vapidPublicKey from Nodeinfo --- lib/pleroma/web/nodeinfo/nodeinfo_controller.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 6947c82b9..c90d4c009 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -126,8 +126,7 @@ def raw_nodeinfo do mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false), features: features, restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), - skipThreadContainment: Config.get([:instance, :skip_thread_containment], false), - vapidPublicKey: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) } } end From f378e93bf4ca4bc9547f242e76e6258e25852972 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 26 Jun 2020 16:15:27 +0200 Subject: [PATCH 005/118] AccountController: Return scope in proper format. --- lib/pleroma/web/api_spec/operations/account_operation.ex | 4 ++-- .../web/mastodon_api/controllers/account_controller.ex | 2 +- .../mastodon_api/controllers/account_controller_test.exs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 9bde8fc0d..d94dae374 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -446,13 +446,13 @@ defp create_response do properties: %{ token_type: %Schema{type: :string}, access_token: %Schema{type: :string}, - scope: %Schema{type: :array, items: %Schema{type: :string}}, + scope: %Schema{type: :string}, created_at: %Schema{type: :integer, format: :"date-time"} }, example: %{ "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", "created_at" => 1_585_918_714, - "scope" => ["read", "write", "follow", "push"], + "scope" => "read write follow push", "token_type" => "Bearer" } } diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 7a88a847c..a87dddddf 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -104,7 +104,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do json(conn, %{ token_type: "Bearer", access_token: token.token, - scope: app.scopes, + scope: app.scopes |> Enum.join(" "), created_at: Token.Utils.format_created_at(token) }) else diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index ebfcedd01..fcc1e792b 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -905,7 +905,7 @@ test "Account registration via Application", %{conn: conn} do %{ "access_token" => token, "created_at" => _created_at, - "scope" => _scope, + "scope" => ^scope, "token_type" => "Bearer" } = json_response_and_validate_schema(conn, 200) @@ -1067,7 +1067,7 @@ test "registration from trusted app" do assert %{ "access_token" => access_token, "created_at" => _, - "scope" => ["read", "write", "follow", "push"], + "scope" => "read write follow push", "token_type" => "Bearer" } = response @@ -1185,7 +1185,7 @@ test "creates an account and returns 200 if captcha is valid", %{conn: conn} do assert %{ "access_token" => access_token, "created_at" => _, - "scope" => ["read"], + "scope" => "read", "token_type" => "Bearer" } = conn From a5bbfa21a1fabe97bfff1cc80348d2944319f3ad Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 26 Jun 2020 16:27:39 +0200 Subject: [PATCH 006/118] StaticFE: Prioritize json in requests. --- lib/pleroma/plugs/static_fe_plug.ex | 11 +++++++---- test/web/static_fe/static_fe_controller_test.exs | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index 156e6788e..7c69b2dac 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do def init(options), do: options def call(conn, _) do - if enabled?() and accepts_html?(conn) do + if enabled?() and requires_html?(conn) do conn |> StaticFEController.call(:show) |> halt() @@ -20,10 +20,13 @@ def call(conn, _) do defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) - defp accepts_html?(conn) do + defp requires_html?(conn) do case get_req_header(conn, "accept") do - [accept | _] -> String.contains?(accept, "text/html") - _ -> false + [accept | _] -> + !String.contains?(accept, "json") && String.contains?(accept, "text/html") + + _ -> + false end end end diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs index a49ab002f..1598bf675 100644 --- a/test/web/static_fe/static_fe_controller_test.exs +++ b/test/web/static_fe/static_fe_controller_test.exs @@ -87,6 +87,20 @@ test "single notice page", %{conn: conn, user: user} do assert html =~ "testing a thing!" end + test "redirects to json if requested", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) + + conn = + conn + |> put_req_header( + "accept", + "Accept: application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", text/html" + ) + |> get("/notice/#{activity.id}") + + assert redirected_to(conn, 302) =~ activity.data["object"] + end + test "filters HTML tags", %{conn: conn} do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: ""}) From bb168ed94a6b4d02879472e30149a494d7b7ebb5 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 29 Jun 2020 13:39:09 +0200 Subject: [PATCH 007/118] OAuth: Extract view-type functions to a view. --- lib/pleroma/web/oauth/mfa_controller.ex | 3 +- lib/pleroma/web/oauth/mfa_view.ex | 9 ++++++ lib/pleroma/web/oauth/oauth_controller.ex | 18 +++++------ lib/pleroma/web/oauth/oauth_view.ex | 22 +++++++++++++ lib/pleroma/web/oauth/token/response.ex | 39 ----------------------- 5 files changed, 41 insertions(+), 50 deletions(-) diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex index 53e19f82e..f102c93e7 100644 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ b/lib/pleroma/web/oauth/mfa_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.MFAController do alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.OAuth.MFAView, as: View alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Token plug(:fetch_session when action in [:show, :verify]) @@ -74,7 +75,7 @@ def challenge(conn, %{"mfa_token" => mfa_token} = params) do {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), {:ok, _} <- validates_challenge(user, params), {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, Token.Response.build(user, token)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else _error -> conn diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex index 41d5578dc..5d87db268 100644 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ b/lib/pleroma/web/oauth/mfa_view.ex @@ -5,4 +5,13 @@ defmodule Pleroma.Web.OAuth.MFAView do use Pleroma.Web, :view import Phoenix.HTML.Form + alias Pleroma.MFA + + def render("mfa_response.json", %{token: token, user: user}) do + %{ + error: "mfa_required", + mfa_token: token.token, + supported_challenge_types: MFA.supported_methods(user) + } + end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index c557778ca..3da104933 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do use Pleroma.Web, :controller alias Pleroma.Helpers.UriHelper - alias Pleroma.Maps alias Pleroma.MFA + alias Pleroma.Maps alias Pleroma.Plugs.RateLimiter alias Pleroma.Registration alias Pleroma.Repo @@ -17,6 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.MFAController + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.MFAView alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken @@ -233,9 +235,7 @@ def token_exchange( with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), {:ok, token} <- RefreshToken.grant(token) do - response_attrs = %{created_at: Token.Utils.format_created_at(token)} - - json(conn, Token.Response.build(user, token, response_attrs)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else _error -> render_invalid_credentials_error(conn) end @@ -247,9 +247,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} {:ok, auth} <- Authorization.get_by_token(app, fixed_token), %User{} = user <- User.get_cached_by_id(auth.user_id), {:ok, token} <- Token.exchange_token(app, auth) do - response_attrs = %{created_at: Token.Utils.format_created_at(token)} - - json(conn, Token.Response.build(user, token, response_attrs)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else error -> handle_token_exchange_error(conn, error) @@ -267,7 +265,7 @@ def token_exchange( {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, Token.Response.build(user, token)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else error -> handle_token_exchange_error(conn, error) @@ -290,7 +288,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, auth} <- Authorization.create_authorization(app, %User{}), {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, Token.Response.build_for_client_credentials(token)) + json(conn, OAuthView.render("token.json", %{token: token})) else _error -> handle_token_exchange_error(conn, :invalid_credentails) @@ -548,7 +546,7 @@ defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), defp build_and_response_mfa_token(user, auth) do with {:ok, token} <- MFA.Token.create_token(user, auth) do - Token.Response.build_for_mfa_token(user, token) + MFAView.render("mfa_response.json", %{token: token, user: user}) end end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex index 94ddaf913..f55247ebd 100644 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ b/lib/pleroma/web/oauth/oauth_view.ex @@ -5,4 +5,26 @@ defmodule Pleroma.Web.OAuth.OAuthView do use Pleroma.Web, :view import Phoenix.HTML.Form + + alias Pleroma.Web.OAuth.Token.Utils + + def render("token.json", %{token: token} = opts) do + response = %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: expires_in(), + scope: Enum.join(token.scopes, " "), + created_at: Utils.format_created_at(token) + } + + if user = opts[:user] do + response + |> Map.put(:me, user.ap_id) + else + response + end + end + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex index 0e72c31e9..a12a6865c 100644 --- a/lib/pleroma/web/oauth/token/response.ex +++ b/lib/pleroma/web/oauth/token/response.ex @@ -3,43 +3,4 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth.Token.Response do - @moduledoc false - - alias Pleroma.MFA - alias Pleroma.User - alias Pleroma.Web.OAuth.Token.Utils - - @doc false - def build(%User{} = user, token, opts \\ %{}) do - %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - expires_in: expires_in(), - scope: Enum.join(token.scopes, " "), - me: user.ap_id - } - |> Map.merge(opts) - end - - def build_for_client_credentials(token) do - %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - created_at: Utils.format_created_at(token), - expires_in: expires_in(), - scope: Enum.join(token.scopes, " ") - } - end - - def build_for_mfa_token(user, mfa_token) do - %{ - error: "mfa_required", - mfa_token: mfa_token.token, - supported_challenge_types: MFA.supported_methods(user) - } - end - - defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end From e374872fe7d10aa659723ee31003f3e9188edfdd Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 29 Jun 2020 13:49:48 +0200 Subject: [PATCH 008/118] AccountOperation: Correctly describe create response. --- .../web/api_spec/operations/account_operation.ex | 11 +++++++++-- .../mastodon_api/controllers/account_controller.ex | 8 ++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index d94dae374..f3ffa1ad4 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -438,6 +438,7 @@ defp create_request do } end + # TODO: This is actually a token respone, but there's no oauth operation file yet. defp create_response do %Schema{ title: "AccountCreateResponse", @@ -446,14 +447,20 @@ defp create_response do properties: %{ token_type: %Schema{type: :string}, access_token: %Schema{type: :string}, + refresh_token: %Schema{type: :string}, scope: %Schema{type: :string}, - created_at: %Schema{type: :integer, format: :"date-time"} + created_at: %Schema{type: :integer, format: :"date-time"}, + me: %Schema{type: :string}, + expires_in: %Schema{type: :integer} }, example: %{ + "token_type" => "Bearer", "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", + "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz", "created_at" => 1_585_918_714, + "expires_in" => 600, "scope" => "read write follow push", - "token_type" => "Bearer" + "me" => "https://gensokyo.2hu/users/raymoo" } } end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index a87dddddf..a143675ec 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -28,6 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.TwitterAPI.TwitterAPI plug(Pleroma.Web.ApiSpec.CastAndValidate) @@ -101,12 +102,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do :ok <- TwitterAPI.validate_captcha(app, params), {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do - json(conn, %{ - token_type: "Bearer", - access_token: token.token, - scope: app.scopes |> Enum.join(" "), - created_at: Token.Utils.format_created_at(token) - }) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else {:error, error} -> json_response(conn, :bad_request, %{error: error}) end From f308196b7528fab92b3cfba12ea71c464e2f9ab0 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 29 Jun 2020 13:52:50 +0200 Subject: [PATCH 009/118] Token Response: Remove empty file. --- lib/pleroma/web/oauth/token/response.ex | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 lib/pleroma/web/oauth/token/response.ex diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex deleted file mode 100644 index a12a6865c..000000000 --- a/lib/pleroma/web/oauth/token/response.ex +++ /dev/null @@ -1,6 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Response do -end From 59540131c189afb10faf98d1bfeccf8f94985a90 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 29 Jun 2020 14:09:03 +0200 Subject: [PATCH 010/118] Credo fixes. --- .../web/mastodon_api/controllers/account_controller.ex | 2 +- lib/pleroma/web/oauth/oauth_controller.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index a143675ec..2942ed336 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -27,8 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Token alias Pleroma.Web.TwitterAPI.TwitterAPI plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 3da104933..7683589cf 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do use Pleroma.Web, :controller alias Pleroma.Helpers.UriHelper - alias Pleroma.MFA alias Pleroma.Maps + alias Pleroma.MFA alias Pleroma.Plugs.RateLimiter alias Pleroma.Registration alias Pleroma.Repo @@ -17,8 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.MFAController - alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.MFAView + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken From 8693e01799308295011a39c8fab71f8a49d3a9bd Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 29 Jun 2020 16:29:51 +0400 Subject: [PATCH 011/118] Fix warning --- lib/pleroma/web/twitter_api/controllers/util_controller.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 4ec523a4e..76f4bb8f4 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.TwitterAPI.UtilView alias Pleroma.Web.WebFinger plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) From 67d92ac7b7b977debac8f8e580db1f0e1ef3ed52 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 29 Jun 2020 17:00:37 +0400 Subject: [PATCH 012/118] Remove `/statusnet/config` --- lib/pleroma/web/router.ex | 3 --- .../web/twitter_api/controllers/util_controller.ex | 12 ------------ 2 files changed, 15 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 930bf7314..9eee74e6c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -517,9 +517,6 @@ defmodule Pleroma.Web.Router do pipe_through(:config) get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations) - - # Deprecated - get("/statusnet/config", TwitterAPI.UtilController, :config) end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 76f4bb8f4..8314e75b4 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -81,18 +81,6 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_ end end - # Deprecated in favor of `/nodeinfo` - # https://git.pleroma.social/pleroma/pleroma/-/merge_requests/2327 - # https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1084 - def config(conn, _params) do - json(conn, %{ - site: %{ - textlimit: to_string(Config.get([:instance, :limit])), - vapidPublicKey: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - } - }) - end - def frontend_configurations(conn, _params) do config = Pleroma.Config.get(:frontend_configurations, %{}) From 8ad166e8e385b7baea79dc3949b438edba25c69f Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 12:46:28 +0200 Subject: [PATCH 013/118] Migrations: Add `accepts_chat_messages` to users. --- .../20200703101031_add_chat_acceptance_to_users.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs diff --git a/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs b/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs new file mode 100644 index 000000000..4ae3c4201 --- /dev/null +++ b/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs @@ -0,0 +1,12 @@ +defmodule Pleroma.Repo.Migrations.AddChatAcceptanceToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add(:accepts_chat_messages, :boolean, nullable: false, default: false) + end + + # Looks stupid but makes the update much faster + execute("update users set accepts_chat_messages = local where local = true") + end +end From 98bfdba108d4213eea82dc4d63edb8bb834118fb Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 12:47:05 +0200 Subject: [PATCH 014/118] User: On registration, set `accepts_chat_messages` to true. --- lib/pleroma/user.ex | 5 ++++- test/user_test.exs | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 8a54546d6..79e094a79 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -138,6 +138,7 @@ defmodule Pleroma.User do field(:also_known_as, {:array, :string}, default: []) field(:inbox, :string) field(:shared_inbox, :string) + field(:accepts_chat_messages, :boolean, default: false) embeds_one( :notification_settings, @@ -623,6 +624,7 @@ def force_password_reset(user), do: update_password_reset_pending(user, true) def register_changeset(struct, params \\ %{}, opts \\ []) do bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) + params = Map.put_new(params, :accepts_chat_messages, true) need_confirmation? = if is_nil(opts[:need_confirmation]) do @@ -641,7 +643,8 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do :nickname, :password, :password_confirmation, - :emoji + :emoji, + :accepts_chat_messages ]) |> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_confirmation(:password) diff --git a/test/user_test.exs b/test/user_test.exs index 7126bb539..9788e09d9 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -486,6 +486,15 @@ test "it sets the password_hash and ap_id" do } setup do: clear_config([:instance, :account_activation_required], true) + test "it sets the 'accepts_chat_messages' set to true" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.accepts_chat_messages + end + test "it creates unconfirmed user" do changeset = User.register_changeset(%User{}, @full_user_data) assert changeset.valid? From 3250228be9719b0afa24c97b64f56d2275c4fe67 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 13:07:33 +0200 Subject: [PATCH 015/118] AccountView: Add 'accepts_chat_messages' to view. --- lib/pleroma/web/mastodon_api/views/account_view.ex | 3 ++- test/web/mastodon_api/views/account_view_test.exs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a6e64b4ab..6a643bfcc 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -245,7 +245,8 @@ defp do_render("show.json", %{user: user} = opts) do hide_favorites: user.hide_favorites, relationship: relationship, skip_thread_containment: user.skip_thread_containment, - background_image: image_url(user.background) |> MediaProxy.url() + background_image: image_url(user.background) |> MediaProxy.url(), + accepts_chat_messages: user.accepts_chat_messages } } |> maybe_put_role(user, opts[:for]) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 80b1f734c..3234a26a2 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -85,7 +85,8 @@ test "Represent a user account" do hide_followers_count: false, hide_follows_count: false, relationship: %{}, - skip_thread_containment: false + skip_thread_containment: false, + accepts_chat_messages: false } } @@ -162,7 +163,8 @@ test "Represent a Service(bot) account" do hide_followers_count: false, hide_follows_count: false, relationship: %{}, - skip_thread_containment: false + skip_thread_containment: false, + accepts_chat_messages: false } } From 37fdb05058d17abde11fd3e55ce896464c7d22e4 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 13:12:23 +0200 Subject: [PATCH 016/118] User, Migration: Change `accepts_chat_messages` to be nullable This is to model the ambiguous state of most users. --- lib/pleroma/user.ex | 2 +- .../20200703101031_add_chat_acceptance_to_users.exs | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 79e094a79..7a684b192 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -138,7 +138,7 @@ defmodule Pleroma.User do field(:also_known_as, {:array, :string}, default: []) field(:inbox, :string) field(:shared_inbox, :string) - field(:accepts_chat_messages, :boolean, default: false) + field(:accepts_chat_messages, :boolean, default: nil) embeds_one( :notification_settings, diff --git a/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs b/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs index 4ae3c4201..8dfda89f1 100644 --- a/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs +++ b/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs @@ -1,12 +1,17 @@ defmodule Pleroma.Repo.Migrations.AddChatAcceptanceToUsers do use Ecto.Migration - def change do + def up do alter table(:users) do - add(:accepts_chat_messages, :boolean, nullable: false, default: false) + add(:accepts_chat_messages, :boolean, nullable: true) end - # Looks stupid but makes the update much faster - execute("update users set accepts_chat_messages = local where local = true") + execute("update users set accepts_chat_messages = true where local = true") + end + + def down do + alter table(:users) do + remove(:accepts_chat_messages) + end end end From db76c26469f234ca36e9c16deb01de63055535ae Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 13:24:16 +0200 Subject: [PATCH 017/118] AccountViewTest: Fix test. --- test/web/mastodon_api/views/account_view_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 3234a26a2..4aba6aaf1 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -86,7 +86,7 @@ test "Represent a user account" do hide_follows_count: false, relationship: %{}, skip_thread_containment: false, - accepts_chat_messages: false + accepts_chat_messages: nil } } @@ -164,7 +164,7 @@ test "Represent a Service(bot) account" do hide_follows_count: false, relationship: %{}, skip_thread_containment: false, - accepts_chat_messages: false + accepts_chat_messages: nil } } From 26a7cc3f003d79d6026d67a3a8370516b13c2c90 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 13:38:59 +0200 Subject: [PATCH 018/118] UserView: Add acceptsChatMessages field --- lib/pleroma/web/activity_pub/views/user_view.ex | 10 ++++++++++ test/web/activity_pub/views/user_view_test.exs | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 4a02b09a1..d062d6230 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -81,6 +81,15 @@ def render("user.json", %{user: user}) do fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) + chat_message_acceptance = + if is_boolean(user.accepts_chat_messages) do + %{ + "acceptsChatMessages" => user.accepts_chat_messages + } + else + %{} + end + %{ "id" => user.ap_id, "type" => user.actor_type, @@ -103,6 +112,7 @@ def render("user.json", %{user: user}) do "tag" => emoji_tags, "discoverable" => user.discoverable } + |> Map.merge(chat_message_acceptance) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(Utils.make_json_ld_header()) diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index bec15a996..3b4a1bcde 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -158,4 +158,16 @@ test "sets correct totalItems when follows are hidden but the follow counter is assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) end end + + describe "acceptsChatMessages" do + test "it returns this value if it is set" do + true_user = insert(:user, accepts_chat_messages: true) + false_user = insert(:user, accepts_chat_messages: false) + nil_user = insert(:user, accepts_chat_messages: nil) + + assert %{"acceptsChatMessages" => true} = UserView.render("user.json", user: true_user) + assert %{"acceptsChatMessages" => false} = UserView.render("user.json", user: false_user) + refute Map.has_key?(UserView.render("user.json", user: nil_user), "acceptsChatMessages") + end + end end From 8289ec67a80697a1a4843c0ea50e66b01bf3bb00 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 13:39:21 +0200 Subject: [PATCH 019/118] Litepub: Add acceptsChatMessages to schema. --- priv/static/schemas/litepub-0.1.jsonld | 1 + 1 file changed, 1 insertion(+) diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 7cc3fee40..c1bcad0f8 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -13,6 +13,7 @@ }, "discoverable": "toot:discoverable", "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "acceptsChatMessages": "litepub:acceptsChatMessages", "ostatus": "http://ostatus.org#", "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", From 5c0bf4c4721f03bd854d4466e77aa08e260c9299 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 13:58:34 +0200 Subject: [PATCH 020/118] ActivityPub: Ingest information about chat acceptance. --- lib/pleroma/user.ex | 3 +- lib/pleroma/web/activity_pub/activity_pub.ex | 4 +- .../tesla_mock/admin@mastdon.example.org.json | 1 + test/web/activity_pub/activity_pub_test.exs | 63 ++++++++++--------- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7a684b192..a4130c89f 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -437,7 +437,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :discoverable, :invisible, :actor_type, - :also_known_as + :also_known_as, + :accepts_chat_messages ] ) |> validate_required([:name, :ap_id]) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 94117202c..86428b861 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1224,6 +1224,7 @@ defp object_to_user_data(data) do end) locked = data["manuallyApprovesFollowers"] || false + accepts_chat_messages = data["acceptsChatMessages"] data = Transmogrifier.maybe_fix_user_object(data) discoverable = data["discoverable"] || false invisible = data["invisible"] || false @@ -1262,7 +1263,8 @@ defp object_to_user_data(data) do also_known_as: Map.get(data, "alsoKnownAs", []), public_key: public_key, inbox: data["inbox"], - shared_inbox: shared_inbox + shared_inbox: shared_inbox, + accepts_chat_messages: accepts_chat_messages } # nickname can be nil because of virtual actors diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json index 9fdd6557c..f5cf174be 100644 --- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json +++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json @@ -26,6 +26,7 @@ "summary": "\u003cp\u003e\u003c/p\u003e", "url": "http://mastodon.example.org/@admin", "manuallyApprovesFollowers": false, + "acceptsChatMessages": true, "publicKey": { "id": "http://mastodon.example.org/users/admin#main-key", "owner": "http://mastodon.example.org/users/admin", diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 575e0c5db..ef69f3d91 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -184,38 +184,45 @@ test "it returns a user that is invisible" do assert User.invisible?(user) end - test "it fetches the appropriate tag-restricted posts" do - user = insert(:user) + test "it returns a user that accepts chat messages" do + user_id = "http://mastodon.example.org/users/admin" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) - {:ok, status_one} = CommonAPI.post(user, %{status: ". #test"}) - {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) - {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) - - fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) - - fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) - - fetch_three = - ActivityPub.fetch_activities([], %{ - type: "Create", - tag: ["test", "essais"], - tag_reject: ["reject"] - }) - - fetch_four = - ActivityPub.fetch_activities([], %{ - type: "Create", - tag: ["test"], - tag_all: ["test", "reject"] - }) - - assert fetch_one == [status_one, status_three] - assert fetch_two == [status_one, status_two, status_three] - assert fetch_three == [status_one, status_two] - assert fetch_four == [status_three] + assert user.accepts_chat_messages end end + test "it fetches the appropriate tag-restricted posts" do + user = insert(:user) + + {:ok, status_one} = CommonAPI.post(user, %{status: ". #test"}) + {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) + {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) + + fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) + + fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) + + fetch_three = + ActivityPub.fetch_activities([], %{ + type: "Create", + tag: ["test", "essais"], + tag_reject: ["reject"] + }) + + fetch_four = + ActivityPub.fetch_activities([], %{ + type: "Create", + tag: ["test"], + tag_all: ["test", "reject"] + }) + + assert fetch_one == [status_one, status_three] + assert fetch_two == [status_one, status_two, status_three] + assert fetch_three == [status_one, status_two] + assert fetch_four == [status_three] + end + describe "insertion" do test "drops activities beyond a certain limit" do limit = Config.get([:instance, :remote_limit]) From b374fd622b120668bb828155e32f9b4f4a142911 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 14:24:54 +0200 Subject: [PATCH 021/118] Docs: Document the added `accepts_chat_messages` user property. --- docs/API/differences_in_mastoapi_responses.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 72b5984ae..755db0e65 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -71,6 +71,7 @@ Has these additional fields under the `pleroma` object: - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner. - `unread_notifications_count`: The count of unread notifications. Only returned to the account owner. - `notification_settings`: object, can be absent. See `/api/pleroma/notification_settings` for the parameters/keys returned. +- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user ### Source From 3ca9af1f9fef081830820b5bea90f789e460b83a Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 14:31:04 +0200 Subject: [PATCH 022/118] Account Schema: Add `accepts_chat_messages` --- lib/pleroma/web/api_spec/schemas/account.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 84f18f1b6..3a84a1593 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -102,7 +102,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do type: :object, description: "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`" - } + }, + accepts_chat_messages: %Schema{type: :boolean, nullable: true} } }, source: %Schema{ @@ -169,6 +170,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do "is_admin" => false, "is_moderator" => false, "skip_thread_containment" => false, + "accepts_chat_messages" => true, "chat_token" => "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc", "unread_conversation_count" => 0, From 4a7b89e37217af4d98746bb934b8264d7a8de51d Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 15:13:27 +0200 Subject: [PATCH 023/118] ChatMessageValidator: Additional validation. --- .../object_validators/chat_message_validator.ex | 6 ++++++ test/web/activity_pub/object_validator_test.exs | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index c481d79e0..91b475393 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -93,12 +93,14 @@ def validate_content_or_attachment(cng) do - If both users are in our system - If at least one of the users in this ChatMessage is a local user - If the recipient is not blocking the actor + - If the recipient is explicitly not accepting chat messages """ def validate_local_concern(cng) do with actor_ap <- get_field(cng, :actor), {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)}, {_, %User{} = recipient} <- {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())}, + {_, false} <- {:not_accepting_chats?, recipient.accepts_chat_messages == false}, {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)}, {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do cng @@ -107,6 +109,10 @@ def validate_local_concern(cng) do cng |> add_error(:actor, "actor is blocked by recipient") + {:not_accepting_chats?, true} -> + cng + |> add_error(:to, "recipient does not accept chat messages") + {:local?, false} -> cng |> add_error(:actor, "actor and recipient are both remote") diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index f38bf7e08..c1a872297 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -223,6 +223,17 @@ test "does not validate if the recipient is blocking the actor", %{ refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) end + test "does not validate if the recipient is not accepting chat messages", %{ + valid_chat_message: valid_chat_message, + recipient: recipient + } do + recipient + |> Ecto.Changeset.change(%{accepts_chat_messages: false}) + |> Pleroma.Repo.update!() + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + test "does not validate if the actor or the recipient is not in our system", %{ valid_chat_message: valid_chat_message } do From e3b5559780f798945eea59170afa9ef41bbf59b3 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 15:54:25 +0200 Subject: [PATCH 024/118] AccountController: Make setting accepts_chat_messages possible. --- lib/pleroma/user.ex | 3 ++- lib/pleroma/web/api_spec/operations/account_operation.ex | 9 +++++++-- .../web/mastodon_api/controllers/account_controller.ex | 3 ++- .../account_controller/update_credentials_test.exs | 7 +++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index a4130c89f..712bc3047 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -483,7 +483,8 @@ def update_changeset(struct, params \\ %{}) do :pleroma_settings_store, :discoverable, :actor_type, - :also_known_as + :also_known_as, + :accepts_chat_messages ] ) |> unique_constraint(:nickname) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 9bde8fc0d..3c05fa55f 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -61,7 +61,7 @@ def update_credentials_operation do description: "Update the user's display and preferences.", operationId: "AccountController.update_credentials", security: [%{"oAuth" => ["write:accounts"]}], - requestBody: request_body("Parameters", update_creadentials_request(), required: true), + requestBody: request_body("Parameters", update_credentials_request(), required: true), responses: %{ 200 => Operation.response("Account", "application/json", Account), 403 => Operation.response("Error", "application/json", ApiError) @@ -458,7 +458,7 @@ defp create_response do } end - defp update_creadentials_request do + defp update_credentials_request do %Schema{ title: "AccountUpdateCredentialsRequest", description: "POST body for creating an account", @@ -492,6 +492,11 @@ defp update_creadentials_request do nullable: true, description: "Whether manual approval of follow requests is required." }, + accepts_chat_messages: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Whether the user accepts receiving chat messages." + }, fields_attributes: %Schema{ nullable: true, oneOf: [ diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index b5008d69b..7ff767db6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -160,7 +160,8 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p :show_role, :skip_thread_containment, :allow_following_move, - :discoverable + :discoverable, + :accepts_chat_messages ] |> Enum.reduce(%{}, fn key, acc -> Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)}) 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 f67d294ba..37e33bc33 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 @@ -108,6 +108,13 @@ test "updates the user's locking status", %{conn: conn} do assert user_data["locked"] == true end + test "updates the user's chat acceptance status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{accepts_chat_messages: "false"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["accepts_chat_messages"] == false + end + test "updates the user's allow_following_move", %{user: user, conn: conn} do assert user.allow_following_move == true From 01695716c8d8916e8a9ddc3c07edfd45c7d5c8f2 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 15:55:18 +0200 Subject: [PATCH 025/118] Docs: Document `accepts_chat_messages` setting. --- docs/API/differences_in_mastoapi_responses.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 755db0e65..4514a7d59 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -186,6 +186,7 @@ Additional parameters can be added to the JSON body/Form data: - `pleroma_background_image` - sets the background image of the user. - `discoverable` - if true, discovery of this account in search results and other services is allowed. - `actor_type` - the type of this account. +- `accepts_chat_messages` - if false, this account will reject all chat messages. ### Pleroma Settings Store From ef4c16f6f19c0544ed22972c78195547b4cf3f5d Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 3 Jul 2020 15:59:42 +0200 Subject: [PATCH 026/118] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26f878a76..81265a7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added +- Chats: Added `accepts_chat_messages` field to user, exposed in APIs and federation. - Chats: Added support for federated chats. For details, see the docs. - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon. - Instance: Add `background_image` to configuration and `/api/v1/instance` From eaa59daa4c229bf47e30ac389563c82b11378e07 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 3 Jul 2020 17:06:20 -0500 Subject: [PATCH 027/118] Add Captcha endpoint to CSP headers when MediaProxy is enabled. Our CSP rules are lax when MediaProxy enabled, but lenient otherwise. This fixes broken captcha on instances not using MediaProxy. --- lib/pleroma/plugs/http_security_plug.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 1420a9611..f7192ebfc 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -125,11 +125,19 @@ defp get_proxy_and_attachment_sources do if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3, do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host + captcha_method = Config.get([Pleroma.Captcha, :method]) + + captcha_endpoint = + if Config.get([Pleroma.Captcha, :enabled]) && + captcha_method != "Pleroma.Captcha.Native", + do: Config.get([captcha_method, :endpoint]) + [] |> add_source(media_proxy_base_url) |> add_source(upload_base_url) |> add_source(s3_endpoint) |> add_source(media_proxy_whitelist) + |> add_source(captcha_endpoint) end defp add_source(iodata, nil), do: iodata From e9a28078ad969204faae600df3ddff8e75ed2f8a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 3 Jul 2020 17:18:22 -0500 Subject: [PATCH 028/118] Rename function and clarify that CSP is only strict with MediaProxy enabled --- lib/pleroma/plugs/http_security_plug.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index f7192ebfc..23a641faf 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -69,10 +69,11 @@ defp csp_string do img_src = "img-src 'self' data: blob:" media_src = "media-src 'self'" + # Strict multimedia CSP enforcement only when MediaProxy is enabled {img_src, media_src} = if Config.get([:media_proxy, :enabled]) && !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do - sources = get_proxy_and_attachment_sources() + sources = build_csp_multimedia_source_list() {[img_src, sources], [media_src, sources]} else {[img_src, " https:"], [media_src, " https:"]} @@ -107,7 +108,7 @@ defp csp_string do |> :erlang.iolist_to_binary() end - defp get_proxy_and_attachment_sources do + defp build_csp_multimedia_source_list do media_proxy_whitelist = Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc -> add_source(acc, host) From 991bd78ddad74641f8032c7b373771a5acb10da9 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 3 Jul 2020 17:19:43 -0500 Subject: [PATCH 029/118] Document the Captcha CSP fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85401809a..4b74d064c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Resolving Peertube accounts with Webfinger - `blob:` urls not being allowed by connect-src CSP - Mastodon API: fix `GET /api/v1/notifications` not returning the full result set +- Fix CSP policy generation to include remote Captcha services ## [Unreleased (patch)] From cf566556147975d45958d2d87a5ce23831eb91df Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 4 Jul 2020 17:11:37 +0200 Subject: [PATCH 030/118] Streamer: Don't filter out announce notifications. --- lib/pleroma/web/streamer/streamer.ex | 12 ++++++++---- test/web/streamer/streamer_test.exs | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 73ee3e1e1..d1d70e556 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -104,7 +104,9 @@ def stream(topics, items) do :ok end - def filtered_by_user?(%User{} = user, %Activity{} = item) do + def filtered_by_user?(user, item, streamed_type \\ :activity) + + def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) @@ -116,7 +118,9 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do true <- 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 <- !(item.data["type"] == "Announce" && parent.data["actor"] == user.ap_id), + true <- + !(streamed_type == :activity && item.data["type"] == "Announce" && + parent.data["actor"] == user.ap_id), 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), @@ -131,8 +135,8 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do end end - def filtered_by_user?(%User{} = user, %Notification{activity: activity}) do - filtered_by_user?(user, activity) + def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do + filtered_by_user?(user, activity, :notification) end defp do_stream("direct", item) do diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index dfe341b34..d56d74464 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -128,6 +128,23 @@ test "it does not stream announces of the user's own posts in the 'user' stream" assert Streamer.filtered_by_user?(user, announce) end + test "it does stream notifications announces of the user's own posts in the 'user' stream", %{ + user: user + } do + Streamer.get_topic_and_add_socket("user", user) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, announce} = CommonAPI.repeat(activity.id, other_user) + + notification = + Pleroma.Notification + |> Repo.get_by(%{user_id: user.id, activity_id: announce.id}) + |> Repo.preload(:activity) + + refute Streamer.filtered_by_user?(user, notification) + end + test "it streams boosts of mastodon user in the 'user' stream", %{user: user} do Streamer.get_topic_and_add_socket("user", user) From af612bd006a2792e27f9b995c0c86e010cc77e6c Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sun, 5 Jul 2020 10:11:43 -0500 Subject: [PATCH 031/118] Ensure all CSP parameters for remote hosts have a scheme --- lib/pleroma/plugs/http_security_plug.ex | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 23a641faf..3bf0b8ce7 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -116,22 +116,22 @@ defp build_csp_multimedia_source_list do media_proxy_base_url = if Config.get([:media_proxy, :base_url]), - do: URI.parse(Config.get([:media_proxy, :base_url])).host + do: build_csp_param(Config.get([:media_proxy, :base_url])) upload_base_url = if Config.get([Pleroma.Upload, :base_url]), - do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host + do: build_csp_param(Config.get([Pleroma.Upload, :base_url])) s3_endpoint = if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3, - do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host + do: build_csp_param(Config.get([Pleroma.Uploaders.S3, :public_endpoint])) captcha_method = Config.get([Pleroma.Captcha, :method]) captcha_endpoint = if Config.get([Pleroma.Captcha, :enabled]) && captcha_method != "Pleroma.Captcha.Native", - do: Config.get([captcha_method, :endpoint]) + do: build_csp_param(Config.get([captcha_method, :endpoint])) [] |> add_source(media_proxy_base_url) @@ -148,6 +148,14 @@ defp add_csp_param(csp_iodata, nil), do: csp_iodata defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] + defp build_csp_param(url) when is_binary(url) do + %{host: host, scheme: scheme} = URI.parse(url) + + if scheme do + scheme <> "://" <> host + end + end + def warn_if_disabled do unless Config.get([:http_security, :enabled]) do Logger.warn(" From fc1f34b85125b24a8094aaa963acb46acacd8eee Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Mon, 6 Jul 2020 00:01:25 +0300 Subject: [PATCH 032/118] Delete activity before sending response to client --- .../mastodon_api/controllers/status_controller.ex | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 3f4c53437..12be530c9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -201,15 +201,13 @@ def show(%{assigns: %{user: user}} = conn, %{id: id}) do @doc "DELETE /api/v1/statuses/:id" def delete(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), - render <- - try_render(conn, "show.json", - activity: activity, - for: user, - with_direct_conversation_id: true, - with_source: true - ), {:ok, %Activity{}} <- CommonAPI.delete(id, user) do - render + try_render(conn, "show.json", + activity: activity, + for: user, + with_direct_conversation_id: true, + with_source: true + ) else _e -> {:error, :not_found} end From 480dfafa831245976a5c21940adca6f2a73c1213 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 6 Jul 2020 08:48:20 +0300 Subject: [PATCH 033/118] don't save tesla settings into db --- lib/pleroma/config/loader.ex | 8 +++++++- test/config/holder_test.exs | 5 +---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index 0f3ecf1ed..64e7de6df 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do :swarm ] + @reject_groups [ + :postgrex, + :tesla + ] + if Code.ensure_loaded?(Config.Reader) do @reader Config.Reader @@ -47,7 +52,8 @@ defp filter(configs) do @spec filter_group(atom(), keyword()) :: keyword() def filter_group(group, configs) do Enum.reject(configs[group], fn {key, _v} -> - key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex + key in @reject_keys or group in @reject_groups or + (group == :phoenix and key == :serve_endpoints) end) end end diff --git a/test/config/holder_test.exs b/test/config/holder_test.exs index 15d48b5c7..abcaa27dd 100644 --- a/test/config/holder_test.exs +++ b/test/config/holder_test.exs @@ -10,7 +10,6 @@ defmodule Pleroma.Config.HolderTest do test "default_config/0" do config = Holder.default_config() assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads" - assert config[:tesla][:adapter] == Tesla.Mock refute config[:pleroma][Pleroma.Repo] refute config[:pleroma][Pleroma.Web.Endpoint] @@ -18,17 +17,15 @@ test "default_config/0" do refute config[:pleroma][:configurable_from_database] refute config[:pleroma][:database] refute config[:phoenix][:serve_endpoints] + refute config[:tesla][:adapter] end test "default_config/1" do pleroma_config = Holder.default_config(:pleroma) assert pleroma_config[Pleroma.Uploaders.Local][:uploads] == "test/uploads" - tesla_config = Holder.default_config(:tesla) - assert tesla_config[:adapter] == Tesla.Mock end test "default_config/2" do assert Holder.default_config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"] - assert Holder.default_config(:tesla, :adapter) == Tesla.Mock end end From 465ddcfd2090abbb18afd7f1f7f1a4ee30105668 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 6 Jul 2020 09:12:29 +0300 Subject: [PATCH 034/118] migration to delete migrated tesla setting --- .../20200706060258_remove_tesla_from_config.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 priv/repo/migrations/20200706060258_remove_tesla_from_config.exs diff --git a/priv/repo/migrations/20200706060258_remove_tesla_from_config.exs b/priv/repo/migrations/20200706060258_remove_tesla_from_config.exs new file mode 100644 index 000000000..798687f8a --- /dev/null +++ b/priv/repo/migrations/20200706060258_remove_tesla_from_config.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.RemoveTeslaFromConfig do + use Ecto.Migration + + def up do + execute("DELETE FROM config WHERE config.group = ':tesla'") + end + + def down do + end +end From 4a8c26654eb7ca7ce049dd4c485c16672b5837a6 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sat, 16 Nov 2019 22:54:13 +0100 Subject: [PATCH 035/118] Restrict statuses that contain user's irreversible filters --- lib/pleroma/filter.ex | 42 ++++++++++- lib/pleroma/web/activity_pub/activity_pub.ex | 22 ++++++ .../controllers/filter_controller.ex | 2 +- test/filter_test.exs | 2 +- test/support/factory.ex | 8 +++ test/web/activity_pub/activity_pub_test.exs | 69 +++++++++++++++++++ 6 files changed, 141 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 4d61b3650..91884c6b3 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -34,10 +34,18 @@ def get(id, %{id: user_id} = _user) do Repo.one(query) end - def get_filters(%User{id: user_id} = _user) do + def get_active(query) do + from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now()) + end + + def get_irreversible(query) do + from(f in query, where: f.hide) + end + + def get_by_user(query, %User{id: user_id} = _user) do query = from( - f in Pleroma.Filter, + f in query, where: f.user_id == ^user_id, order_by: [desc: :id] ) @@ -95,4 +103,34 @@ def update(%Pleroma.Filter{} = filter, params) do |> validate_required([:phrase, :context]) |> Repo.update() end + + def compose_regex(user_or_filters, format \\ :postgres) + + def compose_regex(%User{} = user, format) do + __MODULE__ + |> get_active() + |> get_irreversible() + |> get_by_user(user) + |> compose_regex(format) + end + + def compose_regex([_ | _] = filters, format) do + phrases = + filters + |> Enum.map(& &1.phrase) + |> Enum.join("|") + + case format do + :postgres -> + "\\y(#{phrases})\\y" + + :re -> + ~r/\b#{phrases}\b/i + + _ -> + nil + end + end + + def compose_regex(_, _), do: nil end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 94117202c..31353c866 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Constants alias Pleroma.Conversation alias Pleroma.Conversation.Participation + alias Pleroma.Filter alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object @@ -961,6 +962,26 @@ defp restrict_instance(query, %{instance: instance}) do defp restrict_instance(query, _), do: query + defp restrict_filtered(query, %{user: %User{} = user}) do + case Filter.compose_regex(user) do + nil -> + query + + regex -> + from([activity, object] in query, + where: + fragment("not(?->>'content' ~* ?)", object.data, ^regex) or + activity.actor == ^user.ap_id + ) + end + end + + defp restrict_filtered(query, %{blocking_user: %User{} = user}) do + restrict_filtered(query, %{user: user}) + end + + defp restrict_filtered(query, _), do: query + defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, _) do @@ -1099,6 +1120,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_instance(opts) |> restrict_announce_object_actor(opts) + |> restrict_filtered(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) |> exclude_chat_messages(opts) diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index abbf0ce02..db1ff3189 100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do @doc "GET /api/v1/filters" def index(%{assigns: %{user: user}} = conn, _) do - filters = Filter.get_filters(user) + filters = Filter.get_by_user(Filter, user) render(conn, "index.json", filters: filters) end diff --git a/test/filter_test.exs b/test/filter_test.exs index 63a30c736..061a95ad0 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -126,7 +126,7 @@ test "getting all filters by an user" do {:ok, filter_one} = Pleroma.Filter.create(query_one) {:ok, filter_two} = Pleroma.Filter.create(query_two) - filters = Pleroma.Filter.get_filters(user) + filters = Pleroma.Filter.get_by_user(Pleroma.Filter, user) assert filter_one in filters assert filter_two in filters end diff --git a/test/support/factory.ex b/test/support/factory.ex index af580021c..635d83650 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -428,4 +428,12 @@ def mfa_token_factory do user: build(:user) } end + + def filter_factory do + %Pleroma.Filter{ + user: build(:user), + filter_id: sequence(:filter_id, & &1), + phrase: "cofe" + } + end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 575e0c5db..4968403dc 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -785,6 +785,75 @@ test "excludes reblogs on request" do assert activity == expected_activity end + describe "irreversible filters" do + setup do + user = insert(:user) + user_two = insert(:user) + + insert(:filter, user: user_two, phrase: "cofe", hide: true) + insert(:filter, user: user_two, phrase: "ok boomer", hide: true) + insert(:filter, user: user_two, phrase: "test", hide: false) + + params = %{ + "type" => ["Create", "Announce"], + "user" => user_two + } + + {:ok, %{user: user, user_two: user_two, params: params}} + end + + test "it returns statuses if they don't contain exact filter words", %{ + user: user, + params: params + } do + {:ok, _} = CommonAPI.post(user, %{"status" => "hey"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "got cofefe?"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "I am not a boomer"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "ok boomers"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "ccofee is not a word"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "this is a test"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.count(activities) == 6 + end + + test "it does not filter user's own statuses", %{user_two: user_two, params: params} do + {:ok, _} = CommonAPI.post(user_two, %{"status" => "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user_two, %{"status" => "ok boomer"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.count(activities) == 2 + end + + test "it excludes statuses with filter words", %{user: user, params: params} do + {:ok, _} = CommonAPI.post(user, %{"status" => "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "ok boomer"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "is it a cOfE?"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "cofe is all I need"}) + {:ok, _} = CommonAPI.post(user, %{"status" => "— ok BOOMER\n"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.empty?(activities) + end + + test "it returns all statuses if user does not have any filters" do + another_user = insert(:user) + {:ok, _} = CommonAPI.post(another_user, %{"status" => "got cofe?"}) + {:ok, _} = CommonAPI.post(another_user, %{"status" => "test!"}) + + activities = + ActivityPub.fetch_activities([], %{ + "type" => ["Create", "Announce"], + "user" => another_user + }) + + assert Enum.count(activities) == 2 + end + end + describe "public fetch activities" do test "doesn't retrieve unlisted activities" do user = insert(:user) From 5af1bf443dfd21a6b0be9efc1f55a73e590f6ba3 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Fri, 22 Nov 2019 19:52:50 +0100 Subject: [PATCH 036/118] Skip notifications for statuses that contain an irreversible filtered word --- lib/pleroma/notification.ex | 36 ++++++++++++- test/notification_test.exs | 101 ++++++++++++++++++++++++++++++------ 2 files changed, 119 insertions(+), 18 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 2ef1a80c5..3f749cace 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -130,6 +130,7 @@ def for_user_query(user, opts \\ %{}) do |> preload([n, a, o], activity: {a, object: o}) |> exclude_notification_muted(user, exclude_notification_muted_opts) |> exclude_blocked(user, exclude_blocked_opts) + |> exclude_filtered(user) |> exclude_visibility(opts) end @@ -158,6 +159,20 @@ defp exclude_notification_muted(query, user, opts) do |> where([n, a, o, tm], is_nil(tm.user_id)) end + defp exclude_filtered(query, user) do + case Pleroma.Filter.compose_regex(user) do + nil -> + query + + regex -> + from([_n, a, o] in query, + where: + fragment("not(?->>'content' ~* ?)", o.data, ^regex) or + fragment("?->>'actor' = ?", o.data, ^user.ap_id) + ) + end + end + @valid_visibilities ~w[direct unlisted public private] defp exclude_visibility(query, %{exclude_visibilities: visibility}) @@ -555,7 +570,8 @@ def skip?(%Activity{} = activity, %User{} = user) do :follows, :non_followers, :non_follows, - :recently_followed + :recently_followed, + :filtered ] |> Enum.find(&skip?(&1, activity, user)) end @@ -624,6 +640,24 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, end) end + def skip?(:filtered, activity, user) do + object = Object.normalize(activity) + + cond do + is_nil(object) -> + false + + object.data["actor"] == user.ap_id -> + false + + not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) -> + Regex.match?(regex, object.data["content"]) + + true -> + false + end + end + def skip?(_, _, _), do: false def for_user_and_activity(user, activity) do diff --git a/test/notification_test.exs b/test/notification_test.exs index 6add3f7eb..9ac6925c3 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -324,6 +324,44 @@ test "it disables notifications from people who are invisible" do {:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"}) refute Notification.create_notification(status, user) end + + test "it doesn't create notifications if content matches with an irreversible filter" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + insert(:filter, user: subscriber, phrase: "cofe", hide: true) + + {:ok, status} = CommonAPI.post(user, %{"status" => "got cofe?"}) + + assert {:ok, [nil]} == Notification.create_notifications(status) + end + + test "it creates notifications if content matches with a not irreversible filter" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + insert(:filter, user: subscriber, phrase: "cofe", hide: false) + + {:ok, status} = CommonAPI.post(user, %{"status" => "got cofe?"}) + {:ok, [notification]} = Notification.create_notifications(status) + + assert notification + end + + test "it creates notifications when someone likes user's status with a filtered word" do + user = insert(:user) + other_user = insert(:user) + insert(:filter, user: user, phrase: "tesla", hide: true) + + {:ok, activity_one} = CommonAPI.post(user, %{"status" => "wow tesla"}) + {:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, other_user) + + {:ok, [notification]} = Notification.create_notifications(activity_two) + + assert notification + end end describe "follow / follow_request notifications" do @@ -990,8 +1028,13 @@ test "move activity generates a notification" do end describe "for_user" do - test "it returns notifications for muted user without notifications" do + setup do user = insert(:user) + + {:ok, %{user: user}} + end + + test "it returns notifications for muted user without notifications", %{user: user} do muted = insert(:user) {:ok, _user_relationships} = User.mute(user, muted, false) @@ -1002,8 +1045,7 @@ test "it returns notifications for muted user without notifications" do assert notification.activity.object end - test "it doesn't return notifications for muted user with notifications" do - user = insert(:user) + test "it doesn't return notifications for muted user with notifications", %{user: user} do muted = insert(:user) {:ok, _user_relationships} = User.mute(user, muted) @@ -1012,8 +1054,7 @@ test "it doesn't return notifications for muted user with notifications" do assert Notification.for_user(user) == [] end - test "it doesn't return notifications for blocked user" do - user = insert(:user) + test "it doesn't return notifications for blocked user", %{user: user} do blocked = insert(:user) {:ok, _user_relationship} = User.block(user, blocked) @@ -1022,8 +1063,7 @@ test "it doesn't return notifications for blocked user" do assert Notification.for_user(user) == [] end - test "it doesn't return notifications for domain-blocked non-followed user" do - user = insert(:user) + test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do blocked = insert(:user, ap_id: "http://some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com") @@ -1044,8 +1084,7 @@ test "it returns notifications for domain-blocked but followed user" do assert length(Notification.for_user(user)) == 1 end - test "it doesn't return notifications for muted thread" do - user = insert(:user) + test "it doesn't return notifications for muted thread", %{user: user} do another_user = insert(:user) {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) @@ -1054,8 +1093,7 @@ test "it doesn't return notifications for muted thread" do assert Notification.for_user(user) == [] end - test "it returns notifications from a muted user when with_muted is set" do - user = insert(:user) + test "it returns notifications from a muted user when with_muted is set", %{user: user} do muted = insert(:user) {:ok, _user_relationships} = User.mute(user, muted) @@ -1064,8 +1102,9 @@ test "it returns notifications from a muted user when with_muted is set" do assert length(Notification.for_user(user, %{with_muted: true})) == 1 end - test "it doesn't return notifications from a blocked user when with_muted is set" do - user = insert(:user) + test "it doesn't return notifications from a blocked user when with_muted is set", %{ + user: user + } do blocked = insert(:user) {:ok, _user_relationship} = User.block(user, blocked) @@ -1075,8 +1114,8 @@ test "it doesn't return notifications from a blocked user when with_muted is set end test "when with_muted is set, " <> - "it doesn't return notifications from a domain-blocked non-followed user" do - user = insert(:user) + "it doesn't return notifications from a domain-blocked non-followed user", + %{user: user} do blocked = insert(:user, ap_id: "http://some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com") @@ -1085,8 +1124,7 @@ test "when with_muted is set, " <> assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) end - test "it returns notifications from muted threads when with_muted is set" do - user = insert(:user) + test "it returns notifications from muted threads when with_muted is set", %{user: user} do another_user = insert(:user) {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) @@ -1094,5 +1132,34 @@ test "it returns notifications from muted threads when with_muted is set" do {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) assert length(Notification.for_user(user, %{with_muted: true})) == 1 end + + test "it doesn't return notifications about mentiones with filtered word", %{user: user} do + insert(:filter, user: user, phrase: "cofe", hide: true) + another_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(another_user, %{"status" => "@#{user.nickname} got cofe?"}) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "it returns notifications about mentiones with not hidden filtered word", %{user: user} do + insert(:filter, user: user, phrase: "test", hide: false) + another_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(another_user, %{"status" => "@#{user.nickname} test"}) + + assert length(Notification.for_user(user)) == 1 + end + + test "it returns notifications about favorites with filtered word", %{user: user} do + insert(:filter, user: user, phrase: "cofe", hide: true) + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Give me my cofe!"}) + {:ok, _, _} = CommonAPI.favorite(activity.id, another_user) + + assert length(Notification.for_user(user)) == 1 + end end end From 8277b29790dfd283d94b995539dcb28e51131150 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Mon, 25 Nov 2019 16:59:55 +0100 Subject: [PATCH 037/118] Restrict thread statuses that contain user's irreversible filters --- CHANGELOG.md | 7 ++--- lib/pleroma/web/activity_pub/activity_pub.ex | 2 ++ test/notification_test.exs | 2 +- test/web/activity_pub/activity_pub_test.exs | 28 ++++++++++++++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85401809a..0d31e7928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Streaming: Repeats of a user's posts will no longer be pushed to the user's stream. - Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance - Mastodon API: On deletion, returns the original post text. +- Mastodon API: Add `pleroma.unread_count` to the Marker entity.
@@ -58,8 +59,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Extended `/api/v1/instance`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. -- Mastodon API: Add support for filtering replies in public and home timelines -- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials` +- Mastodon API: Add support for filtering replies in public and home timelines. +- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`. +- Mastodon API: Support irreversible property for filters. - Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoint for status view. - OTP: Add command to reload emoji packs @@ -214,7 +216,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: `pleroma.thread_muted` to the Status entity - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload. -- Mastodon API: Add `pleroma.unread_count` to the Marker entity - Admin API: Render whole status in grouped reports - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise). - Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try. diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 31353c866..8abbef487 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -447,6 +447,7 @@ def fetch_activities_for_context_query(context, opts) do |> maybe_set_thread_muted_field(opts) |> restrict_blocked(opts) |> restrict_recipients(recipients, opts[:user]) + |> restrict_filtered(opts) |> where( [activity], fragment( @@ -1112,6 +1113,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_favorited_by(opts) |> restrict_blocked(restrict_blocked_opts) |> restrict_muted(restrict_muted_opts) + |> restrict_filtered(opts) |> restrict_media(opts) |> restrict_visibility(opts) |> restrict_thread_visibility(opts, config) diff --git a/test/notification_test.exs b/test/notification_test.exs index 9ac6925c3..abaafd60e 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -1147,7 +1147,7 @@ test "it returns notifications about mentiones with not hidden filtered word", % insert(:filter, user: user, phrase: "test", hide: false) another_user = insert(:user) - {:ok, _activity} = CommonAPI.post(another_user, %{"status" => "@#{user.nickname} test"}) + {:ok, _} = CommonAPI.post(another_user, %{"status" => "@#{user.nickname} test"}) assert length(Notification.for_user(user)) == 1 end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 4968403dc..2190ff808 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -507,6 +507,34 @@ test "retrieves activities that have a given context" do activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user}) assert activities == [activity_two, activity] end + + test "doesn't return activities with filtered words" do + user = insert(:user) + user_two = insert(:user) + insert(:filter, user: user, phrase: "test", hide: true) + + {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{"status" => "1"}) + + {:ok, %{id: id2}} = + CommonAPI.post(user_two, %{"status" => "2", "in_reply_to_status_id" => id1}) + + {:ok, %{id: id3} = user_activity} = + CommonAPI.post(user, %{"status" => "3 test?", "in_reply_to_status_id" => id2}) + + {:ok, %{id: id4} = filtered_activity} = + CommonAPI.post(user_two, %{"status" => "4 test!", "in_reply_to_status_id" => id3}) + + {:ok, _} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4}) + + activities = + context + |> ActivityPub.fetch_activities_for_context(%{"user" => user}) + |> Enum.map(& &1.id) + + assert length(activities) == 4 + assert user_activity.id in activities + refute filtered_activity.id in activities + end end test "doesn't return blocked activities" do From 6558f31cda07b8472ed99823ed0f46deffa584cc Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 7 Feb 2020 18:16:39 +0300 Subject: [PATCH 038/118] don't filter notifications for follow and move types --- lib/pleroma/notification.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 3f749cace..d439f51bc 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -640,6 +640,8 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, end) end + def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false + def skip?(:filtered, activity, user) do object = Object.normalize(activity) From 771748db1fa01a71c52c20b890e1b80bfcf1e230 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 26 Feb 2020 13:59:07 +0000 Subject: [PATCH 039/118] Apply suggestion to lib/pleroma/filter.ex --- lib/pleroma/filter.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 91884c6b3..98cb575a9 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -42,7 +42,7 @@ def get_irreversible(query) do from(f in query, where: f.hide) end - def get_by_user(query, %User{id: user_id} = _user) do + def get_filters(query \\ __MODULE__, %User{id: user_id}) do query = from( f in query, From 086a260c04185623065a97e0ba5277585d4fd49a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 26 Feb 2020 14:00:28 +0000 Subject: [PATCH 040/118] Apply suggestion to test/notification_test.exs --- test/notification_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/notification_test.exs b/test/notification_test.exs index abaafd60e..8679f52a5 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -1133,7 +1133,7 @@ test "it returns notifications from muted threads when with_muted is set", %{use assert length(Notification.for_user(user, %{with_muted: true})) == 1 end - test "it doesn't return notifications about mentiones with filtered word", %{user: user} do + test "it doesn't return notifications about mentions with filtered word", %{user: user} do insert(:filter, user: user, phrase: "cofe", hide: true) another_user = insert(:user) From 52ff75413a5a73f045c7b515a06ae40eb568dfa8 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 26 Feb 2020 14:00:38 +0000 Subject: [PATCH 041/118] Apply suggestion to test/notification_test.exs --- test/notification_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/notification_test.exs b/test/notification_test.exs index 8679f52a5..3279ea61e 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -1143,7 +1143,7 @@ test "it doesn't return notifications about mentions with filtered word", %{user assert Enum.empty?(Notification.for_user(user)) end - test "it returns notifications about mentiones with not hidden filtered word", %{user: user} do + test "it returns notifications about mentions with not hidden filtered word", %{user: user} do insert(:filter, user: user, phrase: "test", hide: false) another_user = insert(:user) From 20c27bef4083330a2415f1c0a04e4cad128b267a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 26 Feb 2020 17:50:56 +0300 Subject: [PATCH 042/118] renaming back and reject nil on create --- lib/pleroma/filter.ex | 2 +- lib/pleroma/notification.ex | 1 + .../controllers/filter_controller.ex | 2 +- test/filter_test.exs | 59 ++++++++++--------- test/notification_test.exs | 2 +- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 98cb575a9..5d6df9530 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -110,7 +110,7 @@ def compose_regex(%User{} = user, format) do __MODULE__ |> get_active() |> get_irreversible() - |> get_by_user(user) + |> get_filters(user) |> compose_regex(format) end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index d439f51bc..fcb2144ae 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -352,6 +352,7 @@ def dismiss(%{id: user_id} = _user, id) do end end + @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []} def create_notifications(activity, options \\ []) def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index db1ff3189..abbf0ce02 100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do @doc "GET /api/v1/filters" def index(%{assigns: %{user: user}} = conn, _) do - filters = Filter.get_by_user(Filter, user) + filters = Filter.get_filters(user) render(conn, "index.json", filters: filters) end diff --git a/test/filter_test.exs b/test/filter_test.exs index 061a95ad0..0a5c4426a 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -3,37 +3,39 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.FilterTest do - alias Pleroma.Repo use Pleroma.DataCase import Pleroma.Factory + alias Pleroma.Filter + alias Pleroma.Repo + describe "creating filters" do test "creating one filter" do user = insert(:user) - query = %Pleroma.Filter{ + query = %Filter{ user_id: user.id, filter_id: 42, phrase: "knights", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query) - result = Pleroma.Filter.get(filter.filter_id, user) + {:ok, %Filter{} = filter} = Filter.create(query) + result = Filter.get(filter.filter_id, user) assert query.phrase == result.phrase end test "creating one filter without a pre-defined filter_id" do user = insert(:user) - query = %Pleroma.Filter{ + query = %Filter{ user_id: user.id, phrase: "knights", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query) + {:ok, %Filter{} = filter} = Filter.create(query) # Should start at 1 assert filter.filter_id == 1 end @@ -41,23 +43,23 @@ test "creating one filter without a pre-defined filter_id" do test "creating additional filters uses previous highest filter_id + 1" do user = insert(:user) - query_one = %Pleroma.Filter{ + query_one = %Filter{ user_id: user.id, filter_id: 42, phrase: "knights", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter_one} = Pleroma.Filter.create(query_one) + {:ok, %Filter{} = filter_one} = Filter.create(query_one) - query_two = %Pleroma.Filter{ + query_two = %Filter{ user_id: user.id, # No filter_id phrase: "who", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter_two} = Pleroma.Filter.create(query_two) + {:ok, %Filter{} = filter_two} = Filter.create(query_two) assert filter_two.filter_id == filter_one.filter_id + 1 end @@ -65,29 +67,29 @@ test "filter_id is unique per user" do user_one = insert(:user) user_two = insert(:user) - query_one = %Pleroma.Filter{ + query_one = %Filter{ user_id: user_one.id, phrase: "knights", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter_one} = Pleroma.Filter.create(query_one) + {:ok, %Filter{} = filter_one} = Filter.create(query_one) - query_two = %Pleroma.Filter{ + query_two = %Filter{ user_id: user_two.id, phrase: "who", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter_two} = Pleroma.Filter.create(query_two) + {:ok, %Filter{} = filter_two} = Filter.create(query_two) assert filter_one.filter_id == 1 assert filter_two.filter_id == 1 - result_one = Pleroma.Filter.get(filter_one.filter_id, user_one) + result_one = Filter.get(filter_one.filter_id, user_one) assert result_one.phrase == filter_one.phrase - result_two = Pleroma.Filter.get(filter_two.filter_id, user_two) + result_two = Filter.get(filter_two.filter_id, user_two) assert result_two.phrase == filter_two.phrase end end @@ -95,38 +97,38 @@ test "filter_id is unique per user" do test "deleting a filter" do user = insert(:user) - query = %Pleroma.Filter{ + query = %Filter{ user_id: user.id, filter_id: 0, phrase: "knights", context: ["home"] } - {:ok, _filter} = Pleroma.Filter.create(query) - {:ok, filter} = Pleroma.Filter.delete(query) - assert is_nil(Repo.get(Pleroma.Filter, filter.filter_id)) + {:ok, _filter} = Filter.create(query) + {:ok, filter} = Filter.delete(query) + assert is_nil(Repo.get(Filter, filter.filter_id)) end test "getting all filters by an user" do user = insert(:user) - query_one = %Pleroma.Filter{ + query_one = %Filter{ user_id: user.id, filter_id: 1, phrase: "knights", context: ["home"] } - query_two = %Pleroma.Filter{ + query_two = %Filter{ user_id: user.id, filter_id: 2, phrase: "who", context: ["home"] } - {:ok, filter_one} = Pleroma.Filter.create(query_one) - {:ok, filter_two} = Pleroma.Filter.create(query_two) - filters = Pleroma.Filter.get_by_user(Pleroma.Filter, user) + {:ok, filter_one} = Filter.create(query_one) + {:ok, filter_two} = Filter.create(query_two) + filters = Filter.get_filters(user) assert filter_one in filters assert filter_two in filters end @@ -134,7 +136,7 @@ test "getting all filters by an user" do test "updating a filter" do user = insert(:user) - query_one = %Pleroma.Filter{ + query_one = %Filter{ user_id: user.id, filter_id: 1, phrase: "knights", @@ -146,8 +148,9 @@ test "updating a filter" do context: ["home", "timeline"] } - {:ok, filter_one} = Pleroma.Filter.create(query_one) - {:ok, filter_two} = Pleroma.Filter.update(filter_one, changes) + {:ok, filter_one} = Filter.create(query_one) + {:ok, filter_two} = Filter.update(filter_one, changes) + assert filter_one != filter_two assert filter_two.phrase == changes.phrase assert filter_two.context == changes.context diff --git a/test/notification_test.exs b/test/notification_test.exs index 3279ea61e..898c804cb 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -334,7 +334,7 @@ test "it doesn't create notifications if content matches with an irreversible fi {:ok, status} = CommonAPI.post(user, %{"status" => "got cofe?"}) - assert {:ok, [nil]} == Notification.create_notifications(status) + assert {:ok, []} == Notification.create_notifications(status) end test "it creates notifications if content matches with a not irreversible filter" do From da509487b21bbb627e5fdac6815ad9b3e4e4728b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 31 Mar 2020 16:53:11 +0300 Subject: [PATCH 043/118] adding benchmarks in new format --- benchmarks/load_testing/activities.ex | 10 ++++++++ benchmarks/load_testing/fetcher.ex | 34 +++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex index 074ded457..f5c7bfce8 100644 --- a/benchmarks/load_testing/activities.ex +++ b/benchmarks/load_testing/activities.ex @@ -24,6 +24,7 @@ defmodule Pleroma.LoadTesting.Activities do @visibility ~w(public private direct unlisted) @types [ :simple, + :simple_filtered, :emoji, :mentions, :hell_thread, @@ -242,6 +243,15 @@ defp insert_activity(:simple, visibility, group, users, _opts) do insert_local_activity(visibility, group, users, "Simple status") end + defp insert_activity(:simple_filtered, visibility, group, users, _opts) + when group in @remote_groups do + insert_remote_activity(visibility, group, users, "Remote status which must be filtered") + end + + defp insert_activity(:simple_filtered, visibility, group, users, _opts) do + insert_local_activity(visibility, group, users, "Simple status which must be filtered") + end + defp insert_activity(:emoji, visibility, group, users, _opts) when group in @remote_groups do insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:") diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index 15fd06c3d..dfbd916be 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -32,10 +32,22 @@ defp fetch_user(user) do ) end + defp create_filter(user) do + Pleroma.Filter.create(%Pleroma.Filter{ + user_id: user.id, + phrase: "must be filtered", + hide: true + }) + end + + defp delete_filter(filter), do: Repo.delete(filter) + defp fetch_timelines(user) do fetch_home_timeline(user) + fetch_home_timeline_with_filter(user) fetch_direct_timeline(user) fetch_public_timeline(user) + fetch_public_timeline_with_filter(user) fetch_public_timeline(user, :with_blocks) fetch_public_timeline(user, :local) fetch_public_timeline(user, :tag) @@ -61,7 +73,7 @@ defp opts_for_home_timeline(user) do } end - defp fetch_home_timeline(user) do + defp fetch_home_timeline(user, title_end \\ "") do opts = opts_for_home_timeline(user) recipients = [user.ap_id | User.following(user)] @@ -84,9 +96,11 @@ defp fetch_home_timeline(user) do |> Enum.reverse() |> List.last() + title = "home timeline " <> title_end + Benchee.run( %{ - "home timeline" => fn opts -> ActivityPub.fetch_activities(recipients, opts) end + title => fn opts -> ActivityPub.fetch_activities(recipients, opts) end }, inputs: %{ "1 page" => opts, @@ -108,6 +122,14 @@ defp fetch_home_timeline(user) do ) end + defp fetch_home_timeline_with_filter(user) do + {:ok, filter} = create_filter(user) + + fetch_home_timeline(user, "with filters") + + delete_filter(filter) + end + defp opts_for_direct_timeline(user) do %{ visibility: "direct", @@ -210,6 +232,14 @@ defp fetch_public_timeline(user) do fetch_public_timeline(opts, "public timeline") end + defp fetch_public_timeline_with_filter(user) do + {:ok, filter} = create_filter(user) + opts = opts_for_public_timeline(user) + + fetch_public_timeline(opts, "public timeline with filters") + delete_filter(filter) + end + defp fetch_public_timeline(user, :local) do opts = opts_for_public_timeline(user, :local) From 028a241b7dc45e31161e29ca24a34be8740a4656 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 1 May 2020 09:20:54 +0300 Subject: [PATCH 044/118] tests fixes --- test/notification_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/notification_test.exs b/test/notification_test.exs index 898c804cb..366dc176c 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -356,7 +356,7 @@ test "it creates notifications when someone likes user's status with a filtered insert(:filter, user: user, phrase: "tesla", hide: true) {:ok, activity_one} = CommonAPI.post(user, %{"status" => "wow tesla"}) - {:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, other_user) + {:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id) {:ok, [notification]} = Notification.create_notifications(activity_two) @@ -1157,7 +1157,7 @@ test "it returns notifications about favorites with filtered word", %{user: user another_user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "Give me my cofe!"}) - {:ok, _, _} = CommonAPI.favorite(activity.id, another_user) + {:ok, _} = CommonAPI.favorite(another_user, activity.id) assert length(Notification.for_user(user)) == 1 end From 818f3c2393fb428997f783e599b0d629dcd5a842 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 17 Jun 2020 12:34:27 +0300 Subject: [PATCH 045/118] test fixes --- test/notification_test.exs | 13 +++--- test/web/activity_pub/activity_pub_test.exs | 51 ++++++++++----------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/test/notification_test.exs b/test/notification_test.exs index 366dc176c..13e82ab2a 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -332,7 +332,7 @@ test "it doesn't create notifications if content matches with an irreversible fi User.subscribe(subscriber, user) insert(:filter, user: subscriber, phrase: "cofe", hide: true) - {:ok, status} = CommonAPI.post(user, %{"status" => "got cofe?"}) + {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) assert {:ok, []} == Notification.create_notifications(status) end @@ -344,7 +344,7 @@ test "it creates notifications if content matches with a not irreversible filter User.subscribe(subscriber, user) insert(:filter, user: subscriber, phrase: "cofe", hide: false) - {:ok, status} = CommonAPI.post(user, %{"status" => "got cofe?"}) + {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) {:ok, [notification]} = Notification.create_notifications(status) assert notification @@ -355,7 +355,7 @@ test "it creates notifications when someone likes user's status with a filtered other_user = insert(:user) insert(:filter, user: user, phrase: "tesla", hide: true) - {:ok, activity_one} = CommonAPI.post(user, %{"status" => "wow tesla"}) + {:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"}) {:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id) {:ok, [notification]} = Notification.create_notifications(activity_two) @@ -1137,8 +1137,7 @@ test "it doesn't return notifications about mentions with filtered word", %{user insert(:filter, user: user, phrase: "cofe", hide: true) another_user = insert(:user) - {:ok, _activity} = - CommonAPI.post(another_user, %{"status" => "@#{user.nickname} got cofe?"}) + {:ok, _activity} = CommonAPI.post(another_user, %{status: "@#{user.nickname} got cofe?"}) assert Enum.empty?(Notification.for_user(user)) end @@ -1147,7 +1146,7 @@ test "it returns notifications about mentions with not hidden filtered word", %{ insert(:filter, user: user, phrase: "test", hide: false) another_user = insert(:user) - {:ok, _} = CommonAPI.post(another_user, %{"status" => "@#{user.nickname} test"}) + {:ok, _} = CommonAPI.post(another_user, %{status: "@#{user.nickname} test"}) assert length(Notification.for_user(user)) == 1 end @@ -1156,7 +1155,7 @@ test "it returns notifications about favorites with filtered word", %{user: user insert(:filter, user: user, phrase: "cofe", hide: true) another_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "Give me my cofe!"}) + {:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"}) {:ok, _} = CommonAPI.favorite(another_user, activity.id) assert length(Notification.for_user(user)) == 1 diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 2190ff808..17e12a1a7 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -513,22 +513,21 @@ test "doesn't return activities with filtered words" do user_two = insert(:user) insert(:filter, user: user, phrase: "test", hide: true) - {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{"status" => "1"}) + {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{status: "1"}) - {:ok, %{id: id2}} = - CommonAPI.post(user_two, %{"status" => "2", "in_reply_to_status_id" => id1}) + {:ok, %{id: id2}} = CommonAPI.post(user_two, %{status: "2", in_reply_to_status_id: id1}) {:ok, %{id: id3} = user_activity} = - CommonAPI.post(user, %{"status" => "3 test?", "in_reply_to_status_id" => id2}) + CommonAPI.post(user, %{status: "3 test?", in_reply_to_status_id: id2}) {:ok, %{id: id4} = filtered_activity} = - CommonAPI.post(user_two, %{"status" => "4 test!", "in_reply_to_status_id" => id3}) + CommonAPI.post(user_two, %{status: "4 test!", in_reply_to_status_id: id3}) - {:ok, _} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4}) + {:ok, _} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) activities = context - |> ActivityPub.fetch_activities_for_context(%{"user" => user}) + |> ActivityPub.fetch_activities_for_context(%{user: user}) |> Enum.map(& &1.id) assert length(activities) == 4 @@ -823,8 +822,8 @@ test "excludes reblogs on request" do insert(:filter, user: user_two, phrase: "test", hide: false) params = %{ - "type" => ["Create", "Announce"], - "user" => user_two + type: ["Create", "Announce"], + user: user_two } {:ok, %{user: user, user_two: user_two, params: params}} @@ -834,12 +833,12 @@ test "it returns statuses if they don't contain exact filter words", %{ user: user, params: params } do - {:ok, _} = CommonAPI.post(user, %{"status" => "hey"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "got cofefe?"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "I am not a boomer"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "ok boomers"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "ccofee is not a word"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "this is a test"}) + {:ok, _} = CommonAPI.post(user, %{status: "hey"}) + {:ok, _} = CommonAPI.post(user, %{status: "got cofefe?"}) + {:ok, _} = CommonAPI.post(user, %{status: "I am not a boomer"}) + {:ok, _} = CommonAPI.post(user, %{status: "ok boomers"}) + {:ok, _} = CommonAPI.post(user, %{status: "ccofee is not a word"}) + {:ok, _} = CommonAPI.post(user, %{status: "this is a test"}) activities = ActivityPub.fetch_activities([], params) @@ -847,8 +846,8 @@ test "it returns statuses if they don't contain exact filter words", %{ end test "it does not filter user's own statuses", %{user_two: user_two, params: params} do - {:ok, _} = CommonAPI.post(user_two, %{"status" => "Give me some cofe!"}) - {:ok, _} = CommonAPI.post(user_two, %{"status" => "ok boomer"}) + {:ok, _} = CommonAPI.post(user_two, %{status: "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user_two, %{status: "ok boomer"}) activities = ActivityPub.fetch_activities([], params) @@ -856,11 +855,11 @@ test "it does not filter user's own statuses", %{user_two: user_two, params: par end test "it excludes statuses with filter words", %{user: user, params: params} do - {:ok, _} = CommonAPI.post(user, %{"status" => "Give me some cofe!"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "ok boomer"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "is it a cOfE?"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "cofe is all I need"}) - {:ok, _} = CommonAPI.post(user, %{"status" => "— ok BOOMER\n"}) + {:ok, _} = CommonAPI.post(user, %{status: "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user, %{status: "ok boomer"}) + {:ok, _} = CommonAPI.post(user, %{status: "is it a cOfE?"}) + {:ok, _} = CommonAPI.post(user, %{status: "cofe is all I need"}) + {:ok, _} = CommonAPI.post(user, %{status: "— ok BOOMER\n"}) activities = ActivityPub.fetch_activities([], params) @@ -869,13 +868,13 @@ test "it excludes statuses with filter words", %{user: user, params: params} do test "it returns all statuses if user does not have any filters" do another_user = insert(:user) - {:ok, _} = CommonAPI.post(another_user, %{"status" => "got cofe?"}) - {:ok, _} = CommonAPI.post(another_user, %{"status" => "test!"}) + {:ok, _} = CommonAPI.post(another_user, %{status: "got cofe?"}) + {:ok, _} = CommonAPI.post(another_user, %{status: "test!"}) activities = ActivityPub.fetch_activities([], %{ - "type" => ["Create", "Announce"], - "user" => another_user + type: ["Create", "Announce"], + user: another_user }) assert Enum.count(activities) == 2 From af7720237b448341932a4a0b53d94b006114e915 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 6 Jul 2020 11:08:13 +0200 Subject: [PATCH 046/118] Upload: Restrict description length --- config/config.exs | 1 + lib/pleroma/upload.ex | 9 ++++++++- test/upload_test.exs | 13 +++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 9b550920c..d28a359b2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -188,6 +188,7 @@ background_image: "/images/city.jpg", instance_thumbnail: "/instance/thumbnail.jpeg", limit: 5_000, + description_limit: 5_000, chat_limit: 5_000, remote_limit: 100_000, upload_limit: 16_000_000, diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 797555bff..0fa6b89dc 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -63,6 +63,10 @@ def store(upload, opts \\ []) do with {:ok, upload} <- prepare_upload(upload, opts), upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), + description = Map.get(opts, :description) || upload.name, + {_, true} <- + {:description_limit, + String.length(description) <= Pleroma.Config.get([:instance, :description_limit])}, {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do {:ok, %{ @@ -75,9 +79,12 @@ def store(upload, opts \\ []) do "href" => url_from_spec(upload, opts.base_url, url_spec) } ], - "name" => Map.get(opts, :description) || upload.name + "name" => description }} else + {:description_limit, _} -> + {:error, :description_too_long} + {:error, error} -> Logger.error( "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" diff --git a/test/upload_test.exs b/test/upload_test.exs index 2abf0edec..b06b54487 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -107,6 +107,19 @@ test "it returns error" do describe "Storing a file with the Local uploader" do setup [:ensure_local_uploader] + test "does not allow descriptions longer than the post limit" do + clear_config([:instance, :description_limit], 2) + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:error, :description_too_long} = Upload.store(file, description: "123") + end + test "returns a media url" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") From 2e21ae1b6df807d6937d9d2c49f15242ef268903 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 6 Jul 2020 11:08:53 +0200 Subject: [PATCH 047/118] Docs: Add description limits to cheat sheet --- docs/configuration/cheatsheet.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 6759d5e93..6b640cebc 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -18,6 +18,7 @@ To add configuration to your config file, you can copy it from the base config. * `notify_email`: Email used for notifications. * `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``. * `limit`: Posts character limit (CW/Subject included in the counter). +* `discription_limit`: The character limit for image descriptions. * `chat_limit`: Character limit of the instance chat messages. * `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `upload_limit`: File size limit of uploads (except for avatar, background, banner). From cc8b4e48d966211fdad43121850ac1ecfbb73c74 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 6 Jul 2020 11:12:37 +0200 Subject: [PATCH 048/118] InstanceView: Add chat limit, description limit --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 2 ++ .../web/mastodon_api/controllers/instance_controller_test.exs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 89e48fba5..5deb0d7ed 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -34,6 +34,8 @@ def render("show.json", _) do background_upload_limit: Keyword.get(instance, :background_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit), background_image: Keyword.get(instance, :background_image), + chat_limit: Keyword.get(instance, :chat_limit), + description_limit: Keyword.get(instance, :description_limit), pleroma: %{ metadata: %{ account_activation_required: Keyword.get(instance, :account_activation_required), diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs index 95ee26416..cc880d82c 100644 --- a/test/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/web/mastodon_api/controllers/instance_controller_test.exs @@ -32,7 +32,9 @@ test "get instance information", %{conn: conn} do "avatar_upload_limit" => _, "background_upload_limit" => _, "banner_upload_limit" => _, - "background_image" => _ + "background_image" => _, + "chat_limit" => _, + "description_limit" => _ } = result assert result["pleroma"]["metadata"]["account_activation_required"] != nil From 729506c56a176c725edbbadf0c42b1ac648a37dd Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 6 Jul 2020 11:16:58 +0200 Subject: [PATCH 049/118] Docs: document instance differences --- docs/API/differences_in_mastoapi_responses.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 72b5984ae..d2455d5d7 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -220,6 +220,8 @@ Has theses additional parameters (which are the same as in Pleroma-API): `GET /api/v1/instance` has additional fields - `max_toot_chars`: The maximum characters per post +- `chat_limit`: The maximum characters per chat message +- `description_limit`: The maximum characters per image description - `poll_limits`: The limits of polls - `upload_limit`: The maximum upload file size - `avatar_upload_limit`: The same for avatars From 58da575935f19b86c614717f4fe0d4b8508f395d Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 6 Jul 2020 11:18:01 +0200 Subject: [PATCH 050/118] Changelog: Document description limits. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85401809a..c4077c85d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
API Changes +- **Breaking:** Image description length is limited now. - **Breaking:** Emoji API: changed methods and renamed routes. - Streaming: Repeats of a user's posts will no longer be pushed to the user's stream. - Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance From 208baf157ad0c8be470566d5d51d0214c229e6a5 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 6 Jul 2020 11:38:40 +0200 Subject: [PATCH 051/118] ActivityPub: Add new 'capabilities' to user. --- lib/pleroma/web/activity_pub/activity_pub.ex | 3 ++- lib/pleroma/web/activity_pub/views/user_view.ex | 6 +++--- priv/static/schemas/litepub-0.1.jsonld | 2 +- .../tesla_mock/admin@mastdon.example.org.json | 4 +++- test/web/activity_pub/views/user_view_test.exs | 13 ++++++++++--- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 86428b861..17c9d8f21 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1224,7 +1224,8 @@ defp object_to_user_data(data) do end) locked = data["manuallyApprovesFollowers"] || false - accepts_chat_messages = data["acceptsChatMessages"] + capabilities = data["capabilities"] || %{} + accepts_chat_messages = capabilities["acceptsChatMessages"] data = Transmogrifier.maybe_fix_user_object(data) discoverable = data["discoverable"] || false invisible = data["invisible"] || false diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index d062d6230..3a4564912 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -81,7 +81,7 @@ def render("user.json", %{user: user}) do fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) - chat_message_acceptance = + capabilities = if is_boolean(user.accepts_chat_messages) do %{ "acceptsChatMessages" => user.accepts_chat_messages @@ -110,9 +110,9 @@ def render("user.json", %{user: user}) do "endpoints" => endpoints, "attachment" => fields, "tag" => emoji_tags, - "discoverable" => user.discoverable + "discoverable" => user.discoverable, + "capabilities" => capabilities } - |> Map.merge(chat_message_acceptance) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(Utils.make_json_ld_header()) diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index c1bcad0f8..e7722cf72 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -13,7 +13,7 @@ }, "discoverable": "toot:discoverable", "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "acceptsChatMessages": "litepub:acceptsChatMessages", + "capabilities": "litepub:capabilities", "ostatus": "http://ostatus.org#", "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json index f5cf174be..a911b979a 100644 --- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json +++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json @@ -26,7 +26,9 @@ "summary": "\u003cp\u003e\u003c/p\u003e", "url": "http://mastodon.example.org/@admin", "manuallyApprovesFollowers": false, - "acceptsChatMessages": true, + "capabilities": { + "acceptsChatMessages": true + }, "publicKey": { "id": "http://mastodon.example.org/users/admin#main-key", "owner": "http://mastodon.example.org/users/admin", diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 3b4a1bcde..98c7c9d09 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -165,9 +165,16 @@ test "it returns this value if it is set" do false_user = insert(:user, accepts_chat_messages: false) nil_user = insert(:user, accepts_chat_messages: nil) - assert %{"acceptsChatMessages" => true} = UserView.render("user.json", user: true_user) - assert %{"acceptsChatMessages" => false} = UserView.render("user.json", user: false_user) - refute Map.has_key?(UserView.render("user.json", user: nil_user), "acceptsChatMessages") + assert %{"capabilities" => %{"acceptsChatMessages" => true}} = + UserView.render("user.json", user: true_user) + + assert %{"capabilities" => %{"acceptsChatMessages" => false}} = + UserView.render("user.json", user: false_user) + + refute Map.has_key?( + UserView.render("user.json", user: nil_user)["capabilities"], + "acceptsChatMessages" + ) end end end From 158c26d7ddb3c77dc99a6298114929faf6a2915a Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 6 Jul 2020 12:11:10 +0200 Subject: [PATCH 052/118] StaticFE Plug: Use phoenix helper to get the requested format. --- lib/pleroma/plugs/static_fe_plug.ex | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index 7c69b2dac..143665c71 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -21,12 +21,6 @@ def call(conn, _) do defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) defp requires_html?(conn) do - case get_req_header(conn, "accept") do - [accept | _] -> - !String.contains?(accept, "json") && String.contains?(accept, "text/html") - - _ -> - false - end + Phoenix.Controller.get_format(conn) == "html" end end From 30d0df8e2f1340583b1413154dc4ad76d165b234 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 6 Jul 2020 12:17:08 +0200 Subject: [PATCH 053/118] Update frontend --- priv/static/index.html | 2 +- priv/static/static/config.json | 2 +- .../static/css/app.613cef07981cd95ccceb.css | 5 - .../css/app.613cef07981cd95ccceb.css.map | 1 - .../css/vendors~app.18fea621d430000acc27.css | 306 ------------------ .../vendors~app.18fea621d430000acc27.css.map | 1 - .../static/font/fontello.1589385935077.eot | Bin 22976 -> 0 bytes .../static/font/fontello.1589385935077.svg | 124 ------- .../static/font/fontello.1589385935077.ttf | Bin 22808 -> 0 bytes .../static/font/fontello.1589385935077.woff | Bin 13988 -> 0 bytes .../static/font/fontello.1589385935077.woff2 | Bin 11796 -> 0 bytes priv/static/static/fontello.json | 24 ++ priv/static/static/img/nsfw.74818f9.png | Bin 35104 -> 0 bytes .../static/js/2.18e4adec273c4ce867a8.js | 2 - .../static/js/2.18e4adec273c4ce867a8.js.map | 1 - .../static/js/app.838ffa9aecf210c7d744.js | 2 - .../static/js/app.838ffa9aecf210c7d744.js.map | 1 - .../js/vendors~app.561a1c605d1dfb0e6f74.js | 69 ---- .../vendors~app.561a1c605d1dfb0e6f74.js.map | 1 - priv/static/static/terms-of-service.html | 7 +- priv/static/static/themes/redmond-xx-se.json | 4 +- priv/static/static/themes/redmond-xx.json | 4 +- priv/static/static/themes/redmond-xxi.json | 4 +- priv/static/sw-pleroma-workbox.js | 28 ++ priv/static/sw-pleroma-workbox.js.map | 1 + priv/static/sw-pleroma.js | 11 +- priv/static/sw-pleroma.js.map | 2 +- priv/static/sw.js | 13 +- 28 files changed, 83 insertions(+), 532 deletions(-) delete mode 100644 priv/static/static/css/app.613cef07981cd95ccceb.css delete mode 100644 priv/static/static/css/app.613cef07981cd95ccceb.css.map delete mode 100644 priv/static/static/css/vendors~app.18fea621d430000acc27.css delete mode 100644 priv/static/static/css/vendors~app.18fea621d430000acc27.css.map delete mode 100644 priv/static/static/font/fontello.1589385935077.eot delete mode 100644 priv/static/static/font/fontello.1589385935077.svg delete mode 100644 priv/static/static/font/fontello.1589385935077.ttf delete mode 100644 priv/static/static/font/fontello.1589385935077.woff delete mode 100644 priv/static/static/font/fontello.1589385935077.woff2 delete mode 100644 priv/static/static/img/nsfw.74818f9.png delete mode 100644 priv/static/static/js/2.18e4adec273c4ce867a8.js delete mode 100644 priv/static/static/js/2.18e4adec273c4ce867a8.js.map delete mode 100644 priv/static/static/js/app.838ffa9aecf210c7d744.js delete mode 100644 priv/static/static/js/app.838ffa9aecf210c7d744.js.map delete mode 100644 priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js delete mode 100644 priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js.map create mode 100644 priv/static/sw-pleroma-workbox.js create mode 100644 priv/static/sw-pleroma-workbox.js.map diff --git a/priv/static/index.html b/priv/static/index.html index ddd4ec4eb..279deb8b6 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/config.json b/priv/static/static/config.json index 727dde73b..0030f78f1 100644 --- a/priv/static/static/config.json +++ b/priv/static/static/config.json @@ -14,7 +14,6 @@ "logoMargin": ".1em", "logoMask": true, "minimalScopesMode": false, - "noAttachmentLinks": false, "nsfwCensorImage": "", "postContentType": "text/plain", "redirectRootLogin": "/main/friends", @@ -22,6 +21,7 @@ "scopeCopy": true, "showFeaturesPanel": true, "showInstanceSpecificPanel": false, + "sidebarRight": false, "subjectLineBehavior": "email", "theme": "pleroma-dark", "webPushNotifications": false diff --git a/priv/static/static/css/app.613cef07981cd95ccceb.css b/priv/static/static/css/app.613cef07981cd95ccceb.css deleted file mode 100644 index c1d5f8188..000000000 --- a/priv/static/static/css/app.613cef07981cd95ccceb.css +++ /dev/null @@ -1,5 +0,0 @@ -.with-load-more-footer{padding:10px;text-align:center;border-top:1px solid;border-top-color:#222;border-top-color:var(--border, #222)}.with-load-more-footer .error{font-size:14px} -.tab-switcher{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.tab-switcher .contents{-ms-flex:1 0 auto;flex:1 0 auto;min-height:0px}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents.scrollable-tabs{-ms-flex-preferred-size:0;flex-basis:0;overflow-y:auto}.tab-switcher .tabs{display:-ms-flexbox;display:flex;position:relative;width:100%;overflow-y:hidden;overflow-x:auto;padding-top:5px;box-sizing:border-box}.tab-switcher .tabs::after,.tab-switcher .tabs::before{display:block;content:"";-ms-flex:1 1 auto;flex:1 1 auto;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}.tab-switcher .tabs .tab-wrapper{height:28px;position:relative;display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto}.tab-switcher .tabs .tab-wrapper .tab{width:100%;min-width:1px;position:relative;border-bottom-left-radius:0;border-bottom-right-radius:0;padding:6px 1em;padding-bottom:99px;margin-bottom:-93px;white-space:nowrap;color:#b9b9ba;color:var(--tabText, #b9b9ba);background-color:#182230;background-color:var(--tab, #182230)}.tab-switcher .tabs .tab-wrapper .tab:not(.active){z-index:4}.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover{z-index:6}.tab-switcher .tabs .tab-wrapper .tab.active{background:transparent;z-index:5;color:#b9b9ba;color:var(--tabActiveText, #b9b9ba)}.tab-switcher .tabs .tab-wrapper .tab img{max-height:26px;vertical-align:top;margin-top:-5px}.tab-switcher .tabs .tab-wrapper:not(.active)::after{content:"";position:absolute;left:0;right:0;bottom:0;z-index:7;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)} -.with-subscription-loading{padding:10px;text-align:center}.with-subscription-loading .error{font-size:14px} - -/*# sourceMappingURL=app.613cef07981cd95ccceb.css.map*/ \ No newline at end of file diff --git a/priv/static/static/css/app.613cef07981cd95ccceb.css.map b/priv/static/static/css/app.613cef07981cd95ccceb.css.map deleted file mode 100644 index 556e0bb0b..000000000 --- a/priv/static/static/css/app.613cef07981cd95ccceb.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA,uBAAuB,aAAa,kBAAkB,qBAAqB,sBAAsB,qCAAqC,8BAA8B,e;ACApK,cAAc,oBAAoB,aAAa,0BAA0B,sBAAsB,wBAAwB,kBAAkB,cAAc,eAAe,gCAAgC,aAAa,wCAAwC,0BAA0B,aAAa,gBAAgB,oBAAoB,oBAAoB,aAAa,kBAAkB,WAAW,kBAAkB,gBAAgB,gBAAgB,sBAAsB,uDAAuD,cAAc,WAAW,kBAAkB,cAAc,wBAAwB,yBAAyB,wCAAwC,iCAAiC,YAAY,kBAAkB,oBAAoB,aAAa,kBAAkB,cAAc,sCAAsC,WAAW,cAAc,kBAAkB,4BAA4B,6BAA6B,gBAAgB,oBAAoB,oBAAoB,mBAAmB,cAAc,8BAA8B,yBAAyB,qCAAqC,mDAAmD,UAAU,yDAAyD,UAAU,6CAA6C,uBAAuB,UAAU,cAAc,oCAAoC,0CAA0C,gBAAgB,mBAAmB,gBAAgB,qDAAqD,WAAW,kBAAkB,OAAO,QAAQ,SAAS,UAAU,wBAAwB,yBAAyB,wC;ACAtlD,2BAA2B,aAAa,kBAAkB,kCAAkC,e","file":"static/css/app.613cef07981cd95ccceb.css","sourcesContent":[".with-load-more-footer{padding:10px;text-align:center;border-top:1px solid;border-top-color:#222;border-top-color:var(--border, #222)}.with-load-more-footer .error{font-size:14px}",".tab-switcher{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.tab-switcher .contents{-ms-flex:1 0 auto;flex:1 0 auto;min-height:0px}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents.scrollable-tabs{-ms-flex-preferred-size:0;flex-basis:0;overflow-y:auto}.tab-switcher .tabs{display:-ms-flexbox;display:flex;position:relative;width:100%;overflow-y:hidden;overflow-x:auto;padding-top:5px;box-sizing:border-box}.tab-switcher .tabs::after,.tab-switcher .tabs::before{display:block;content:\"\";-ms-flex:1 1 auto;flex:1 1 auto;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}.tab-switcher .tabs .tab-wrapper{height:28px;position:relative;display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto}.tab-switcher .tabs .tab-wrapper .tab{width:100%;min-width:1px;position:relative;border-bottom-left-radius:0;border-bottom-right-radius:0;padding:6px 1em;padding-bottom:99px;margin-bottom:-93px;white-space:nowrap;color:#b9b9ba;color:var(--tabText, #b9b9ba);background-color:#182230;background-color:var(--tab, #182230)}.tab-switcher .tabs .tab-wrapper .tab:not(.active){z-index:4}.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover{z-index:6}.tab-switcher .tabs .tab-wrapper .tab.active{background:transparent;z-index:5;color:#b9b9ba;color:var(--tabActiveText, #b9b9ba)}.tab-switcher .tabs .tab-wrapper .tab img{max-height:26px;vertical-align:top;margin-top:-5px}.tab-switcher .tabs .tab-wrapper:not(.active)::after{content:\"\";position:absolute;left:0;right:0;bottom:0;z-index:7;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}",".with-subscription-loading{padding:10px;text-align:center}.with-subscription-loading .error{font-size:14px}"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/vendors~app.18fea621d430000acc27.css b/priv/static/static/css/vendors~app.18fea621d430000acc27.css deleted file mode 100644 index ef783cbb3..000000000 --- a/priv/static/static/css/vendors~app.18fea621d430000acc27.css +++ /dev/null @@ -1,306 +0,0 @@ -/*! - * Cropper.js v1.5.6 - * https://fengyuanchen.github.io/cropperjs - * - * Copyright 2015-present Chen Fengyuan - * Released under the MIT license - * - * Date: 2019-10-04T04:33:44.164Z - */ - -.cropper-container { - direction: ltr; - font-size: 0; - line-height: 0; - position: relative; - -ms-touch-action: none; - touch-action: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.cropper-container img { - display: block; - height: 100%; - image-orientation: 0deg; - max-height: none !important; - max-width: none !important; - min-height: 0 !important; - min-width: 0 !important; - width: 100%; -} - -.cropper-wrap-box, -.cropper-canvas, -.cropper-drag-box, -.cropper-crop-box, -.cropper-modal { - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; -} - -.cropper-wrap-box, -.cropper-canvas { - overflow: hidden; -} - -.cropper-drag-box { - background-color: #fff; - opacity: 0; -} - -.cropper-modal { - background-color: #000; - opacity: 0.5; -} - -.cropper-view-box { - display: block; - height: 100%; - outline: 1px solid #39f; - outline-color: rgba(51, 153, 255, 0.75); - overflow: hidden; - width: 100%; -} - -.cropper-dashed { - border: 0 dashed #eee; - display: block; - opacity: 0.5; - position: absolute; -} - -.cropper-dashed.dashed-h { - border-bottom-width: 1px; - border-top-width: 1px; - height: calc(100% / 3); - left: 0; - top: calc(100% / 3); - width: 100%; -} - -.cropper-dashed.dashed-v { - border-left-width: 1px; - border-right-width: 1px; - height: 100%; - left: calc(100% / 3); - top: 0; - width: calc(100% / 3); -} - -.cropper-center { - display: block; - height: 0; - left: 50%; - opacity: 0.75; - position: absolute; - top: 50%; - width: 0; -} - -.cropper-center::before, -.cropper-center::after { - background-color: #eee; - content: ' '; - display: block; - position: absolute; -} - -.cropper-center::before { - height: 1px; - left: -3px; - top: 0; - width: 7px; -} - -.cropper-center::after { - height: 7px; - left: 0; - top: -3px; - width: 1px; -} - -.cropper-face, -.cropper-line, -.cropper-point { - display: block; - height: 100%; - opacity: 0.1; - position: absolute; - width: 100%; -} - -.cropper-face { - background-color: #fff; - left: 0; - top: 0; -} - -.cropper-line { - background-color: #39f; -} - -.cropper-line.line-e { - cursor: ew-resize; - right: -3px; - top: 0; - width: 5px; -} - -.cropper-line.line-n { - cursor: ns-resize; - height: 5px; - left: 0; - top: -3px; -} - -.cropper-line.line-w { - cursor: ew-resize; - left: -3px; - top: 0; - width: 5px; -} - -.cropper-line.line-s { - bottom: -3px; - cursor: ns-resize; - height: 5px; - left: 0; -} - -.cropper-point { - background-color: #39f; - height: 5px; - opacity: 0.75; - width: 5px; -} - -.cropper-point.point-e { - cursor: ew-resize; - margin-top: -3px; - right: -3px; - top: 50%; -} - -.cropper-point.point-n { - cursor: ns-resize; - left: 50%; - margin-left: -3px; - top: -3px; -} - -.cropper-point.point-w { - cursor: ew-resize; - left: -3px; - margin-top: -3px; - top: 50%; -} - -.cropper-point.point-s { - bottom: -3px; - cursor: s-resize; - left: 50%; - margin-left: -3px; -} - -.cropper-point.point-ne { - cursor: nesw-resize; - right: -3px; - top: -3px; -} - -.cropper-point.point-nw { - cursor: nwse-resize; - left: -3px; - top: -3px; -} - -.cropper-point.point-sw { - bottom: -3px; - cursor: nesw-resize; - left: -3px; -} - -.cropper-point.point-se { - bottom: -3px; - cursor: nwse-resize; - height: 20px; - opacity: 1; - right: -3px; - width: 20px; -} - -@media (min-width: 768px) { - .cropper-point.point-se { - height: 15px; - width: 15px; - } -} - -@media (min-width: 992px) { - .cropper-point.point-se { - height: 10px; - width: 10px; - } -} - -@media (min-width: 1200px) { - .cropper-point.point-se { - height: 5px; - opacity: 0.75; - width: 5px; - } -} - -.cropper-point.point-se::before { - background-color: #39f; - bottom: -50%; - content: ' '; - display: block; - height: 200%; - opacity: 0; - position: absolute; - right: -50%; - width: 200%; -} - -.cropper-invisible { - opacity: 0; -} - -.cropper-bg { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'); -} - -.cropper-hide { - display: block; - height: 0; - position: absolute; - width: 0; -} - -.cropper-hidden { - display: none !important; -} - -.cropper-move { - cursor: move; -} - -.cropper-crop { - cursor: crosshair; -} - -.cropper-disabled .cropper-drag-box, -.cropper-disabled .cropper-face, -.cropper-disabled .cropper-line, -.cropper-disabled .cropper-point { - cursor: not-allowed; -} - - -/*# sourceMappingURL=vendors~app.18fea621d430000acc27.css.map*/ \ No newline at end of file diff --git a/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map b/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map deleted file mode 100644 index 057d67d6a..000000000 --- a/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///./node_modules/cropperjs/dist/cropper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,wCAAwC;AACxC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"static/css/vendors~app.18fea621d430000acc27.css","sourcesContent":["/*!\n * Cropper.js v1.5.6\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2019-10-04T04:33:44.164Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 0.75);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n.cropper-center::after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/font/fontello.1589385935077.eot b/priv/static/static/font/fontello.1589385935077.eot deleted file mode 100644 index e5f37013a88506e2a6a91b2c0758388f4f1a2885..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22976 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 - - -Copyright (C) 2020 by original authors @ fontello.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/priv/static/static/font/fontello.1589385935077.ttf b/priv/static/static/font/fontello.1589385935077.ttf deleted file mode 100644 index 0fde96cea20a6b7bfc8f046945623605824290d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22808 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 a!Q5K^ORo#4^tBiT1SSxiE9nCe(Ek9)LA=iZ diff --git a/priv/static/static/font/fontello.1589385935077.woff2 b/priv/static/static/font/fontello.1589385935077.woff2 deleted file mode 100644 index 012eb93051fe38d464bffb06f4dfd053c338ba6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11796 zcmV+vF6+^EPew8T0RR9104@{&4*&oF09hCS04=%z0RR9100000000000000000000 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 C9?j?g diff --git a/priv/static/static/fontello.json b/priv/static/static/fontello.json index 7f0e7cdd5..6083c0bfa 100755 --- a/priv/static/static/fontello.json +++ b/priv/static/static/fontello.json @@ -363,6 +363,30 @@ "css": "ok", "code": 59431, "src": "fontawesome" + }, + { + "uid": "4109c474ff99cad28fd5a2c38af2ec6f", + "css": "filter", + "code": 61616, + "src": "fontawesome" + }, + { + "uid": "9a76bc135eac17d2c8b8ad4a5774fc87", + "css": "download", + "code": 59429, + "src": "fontawesome" + }, + { + "uid": "f04a5d24e9e659145b966739c4fde82a", + "css": "bookmark", + "code": 59430, + "src": "fontawesome" + }, + { + "uid": "2f5ef6f6b7aaebc56458ab4e865beff5", + "css": "bookmark-empty", + "code": 61591, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/priv/static/static/img/nsfw.74818f9.png b/priv/static/static/img/nsfw.74818f9.png deleted file mode 100644 index d251377676855aa1c25428a1175c1372b10940cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35104 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 diff --git a/priv/static/static/js/2.18e4adec273c4ce867a8.js b/priv/static/static/js/2.18e4adec273c4ce867a8.js deleted file mode 100644 index d191aa852..000000000 --- a/priv/static/static/js/2.18e4adec273c4ce867a8.js +++ /dev/null @@ -1,2 +0,0 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[2],{587:function(t,e,i){var c=i(588);"string"==typeof c&&(c=[[t.i,c,""]]),c.locals&&(t.exports=c.locals);(0,i(3).default)("2eec4758",c,!0,{})},588:function(t,e,i){(t.exports=i(2)(!1)).push([t.i,".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(--accent,#d8a070))}",""])},589:function(t,e,i){"use strict";i.r(e);var c=i(91),n={components:{TabSwitcher:i(53).a},data:function(){return{meta:{stickers:[]},path:""}},computed:{pack:function(){return this.$store.state.instance.stickers||[]}},methods:{clear:function(){this.meta={stickers:[]}},pick:function(t,e){var i=this,n=this.$store;fetch(t).then((function(t){t.blob().then((function(t){var a=new File([t],e,{mimetype:"image/png"}),r=new FormData;r.append("file",a),c.a.uploadMedia({store:n,formData:r}).then((function(t){i.$emit("uploaded",t),i.clear()}),(function(t){console.warn("Can't attach sticker"),console.warn(t),i.$emit("upload-failed","default")}))}))}))}}},a=i(0);var r=function(t){i(587)},s=Object(a.a)(n,(function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"sticker-picker"},[i("tab-switcher",{staticClass:"tab-switcher",attrs:{"render-only-focused":!0,"scrollable-tabs":""}},t._l(t.pack,(function(e){return i("div",{key:e.path,staticClass:"sticker-picker-content",attrs:{"image-tooltip":e.meta.title,image:e.path+e.meta.tabIcon}},t._l(e.meta.stickers,(function(c){return i("div",{key:c,staticClass:"sticker",on:{click:function(i){return i.stopPropagation(),i.preventDefault(),t.pick(e.path+c,e.meta.title)}}},[i("img",{attrs:{src:e.path+c}})])})),0)})),0)],1)}),[],!1,r,null,null);e.default=s.exports}}]); -//# sourceMappingURL=2.18e4adec273c4ce867a8.js.map \ No newline at end of file diff --git a/priv/static/static/js/2.18e4adec273c4ce867a8.js.map b/priv/static/static/js/2.18e4adec273c4ce867a8.js.map deleted file mode 100644 index a7f98bfef..000000000 --- a/priv/static/static/js/2.18e4adec273c4ce867a8.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///./src/components/sticker_picker/sticker_picker.vue?e513","webpack:///./src/components/sticker_picker/sticker_picker.vue?1909","webpack:///./src/components/sticker_picker/sticker_picker.js","webpack:///./src/components/sticker_picker/sticker_picker.vue","webpack:///./src/components/sticker_picker/sticker_picker.vue?7504"],"names":["content","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","store","fetch","then","res","blob","file","File","mimetype","formData","FormData","append","statusPosterService","uploadMedia","fileData","$emit","error","console","warn","__vue_styles__","context","Component","_vm","_h","$createElement","_c","_self","staticClass","attrs","_l","stickerpack","key","title","tabIcon","on","$event","stopPropagation","preventDefault"],"mappings":"6EAGA,IAAIA,EAAU,EAAQ,KACA,iBAAZA,IAAsBA,EAAU,CAAC,CAACC,EAAOC,EAAIF,EAAS,MAC7DA,EAAQG,SAAQF,EAAOG,QAAUJ,EAAQG,SAG/BE,EADH,EAAQ,GAAkEC,SACnE,WAAYN,GAAS,EAAM,K,qBCRlCC,EAAOG,QAAU,EAAQ,EAAR,EAA4D,IAK/EG,KAAK,CAACN,EAAOC,EAAI,4iBAA6iB,M,oDC8CvjBM,EA/CO,CACpBC,WAAY,CACVC,Y,MAAAA,GAEFC,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,WACbC,EAAQT,KAAKC,OAEnBS,MAAMH,GACHI,MAAK,SAACC,GACLA,EAAIC,OAAOF,MAAK,SAACE,GACf,IAAIC,EAAO,IAAIC,KAAK,CAACF,GAAOL,EAAM,CAAEQ,SAAU,cAC1CC,EAAW,IAAIC,SACnBD,EAASE,OAAO,OAAQL,GACxBM,IAAoBC,YAAY,CAAEZ,QAAOQ,aACtCN,MAAK,SAACW,GACL,EAAKC,MAAM,WAAYD,GACvB,EAAKjB,WACJ,SAACmB,GACFC,QAAQC,KAAK,wBACbD,QAAQC,KAAKF,GACb,EAAKD,MAAM,gBAAiB,uB,OCnC5C,IAEII,EAVJ,SAAsBC,GACpB,EAAQ,MAeNC,EAAY,YACd,GCjBW,WAAa,IAAIC,EAAI9B,KAAS+B,EAAGD,EAAIE,eAAmBC,EAAGH,EAAII,MAAMD,IAAIF,EAAG,OAAOE,EAAG,MAAM,CAACE,YAAY,kBAAkB,CAACF,EAAG,eAAe,CAACE,YAAY,eAAeC,MAAM,CAAC,uBAAsB,EAAK,kBAAkB,KAAKN,EAAIO,GAAIP,EAAQ,MAAE,SAASQ,GAAa,OAAOL,EAAG,MAAM,CAACM,IAAID,EAAYzC,KAAKsC,YAAY,yBAAyBC,MAAM,CAAC,gBAAgBE,EAAY3C,KAAK6C,MAAM,MAAQF,EAAYzC,KAAOyC,EAAY3C,KAAK8C,UAAUX,EAAIO,GAAIC,EAAY3C,KAAa,UAAE,SAASY,GAAS,OAAO0B,EAAG,MAAM,CAACM,IAAIhC,EAAQ4B,YAAY,UAAUO,GAAG,CAAC,MAAQ,SAASC,GAAyD,OAAjDA,EAAOC,kBAAkBD,EAAOE,iBAAwBf,EAAIxB,KAAKgC,EAAYzC,KAAOU,EAAS+B,EAAY3C,KAAK6C,UAAU,CAACP,EAAG,MAAM,CAACG,MAAM,CAAC,IAAME,EAAYzC,KAAOU,UAAe,MAAK,IAAI,KACjvB,IDOY,EAahCoB,EAToB,KAEU,MAYjB,UAAAE,EAAiB","file":"static/js/2.18e4adec273c4ce867a8.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-01a5cae8\\\",\\\"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){return _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 }","import { filter, sortBy, includes } from 'lodash'\n\nexport const notificationsFromStore = store => store.state.statuses.notifications.data\n\nexport const visibleTypes = store => ([\n store.state.config.notificationVisibility.likes && 'like',\n store.state.config.notificationVisibility.mentions && 'mention',\n store.state.config.notificationVisibility.repeats && 'repeat',\n store.state.config.notificationVisibility.follows && 'follow',\n store.state.config.notificationVisibility.followRequest && 'follow_request',\n store.state.config.notificationVisibility.moves && 'move',\n store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'\n].filter(_ => _))\n\nconst statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']\n\nexport const isStatusNotification = (type) => includes(statusNotifications, type)\n\nconst sortById = (a, b) => {\n const seqA = Number(a.id)\n const seqB = Number(b.id)\n const isSeqA = !Number.isNaN(seqA)\n const isSeqB = !Number.isNaN(seqB)\n if (isSeqA && isSeqB) {\n return seqA > seqB ? -1 : 1\n } else if (isSeqA && !isSeqB) {\n return 1\n } else if (!isSeqA && isSeqB) {\n return -1\n } else {\n return a.id > b.id ? -1 : 1\n }\n}\n\nexport const filteredNotificationsFromStore = (store, types) => {\n // map is just to clone the array since sort mutates it and it causes some issues\n let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)\n sortedNotifications = sortBy(sortedNotifications, 'seen')\n return sortedNotifications.filter(\n (notification) => (types || visibleTypes(store)).includes(notification.type)\n )\n}\n\nexport const unseenNotificationsFromStore = store =>\n filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)\n","import { includes } from 'lodash'\n\nconst generateProfileLink = (id, screenName, restrictedNicknames) => {\n const complicated = !screenName || (isExternal(screenName) || includes(restrictedNicknames, screenName))\n return {\n name: (complicated ? 'external-user-profile' : 'user-profile'),\n params: (complicated ? { id } : { name: screenName })\n }\n}\n\nconst isExternal = screenName => screenName && screenName.includes('@')\n\nexport default generateProfileLink\n","// 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-70b9d662\\\",\\\"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();return _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'\nimport Popover from '../popover/popover.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 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 toggled: false\n }\n },\n components: {\n DialogModal,\n Popover\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 setToggled (value) {\n this.toggled = value\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-168f1ca6\\\",\\\"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('Popover',{staticClass:\"moderation-tools-popover\",attrs:{\"trigger\":\"click\",\"placement\":\"bottom\",\"offset\":{ y: 5 }},on:{\"show\":function($event){return _vm.setToggled(true)},\"close\":function($event){return _vm.setToggled(false)}}},[_c('div',{attrs:{\"slot\":\"content\"},slot:\"content\"},[_c('div',{staticClass:\"dropdown-menu\"},[(_vm.user.is_local)?_c('span',[_c('button',{staticClass:\"dropdown-item\",on:{\"click\":function($event){return _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){return _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){return _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){return _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){return _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){return _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){return _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){return _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){return _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){return _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){return _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:{ toggled: _vm.toggled },attrs:{\"slot\":\"trigger\"},slot:\"trigger\"},[_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){return _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){return _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'\nimport Popover from '../popover/popover.vue'\n\nconst AccountActions = {\n props: [\n 'user', 'relationship'\n ],\n data () {\n return { }\n },\n components: {\n ProgressButton,\n Popover\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-bf5e6e30\\\",\\\"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('Popover',{attrs:{\"trigger\":\"click\",\"placement\":\"bottom\"}},[_c('div',{staticClass:\"account-tools-popover\",attrs:{\"slot\":\"content\"},slot:\"content\"},[_c('div',{staticClass:\"dropdown-menu\"},[(_vm.relationship.following)?[(_vm.relationship.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.relationship.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.relationship.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\",attrs:{\"slot\":\"trigger\"},slot:\"trigger\"},[_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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'\nimport { mapGetters } from 'vuex'\n\nexport default {\n props: [\n 'userId', '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 user () {\n return this.$store.getters.findUser(this.userId)\n },\n relationship () {\n return this.$store.getters.relationship(this.userId)\n },\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 return {\n backgroundImage: [\n `linear-gradient(to bottom, var(--profileTint), var(--profileTint))`,\n `url(${this.user.cover_photo})`\n ].join(', ')\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-4d895630\\\",\\\"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,\"relationship\":_vm.relationship}}):_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.relationship.followed_by && _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:{\"relationship\":_vm.relationship}}),_vm._v(\" \"),(_vm.relationship.following)?[(!_vm.relationship.subscribing)?_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 toggled\",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.relationship.muting)?_c('button',{staticClass:\"btn btn-default btn-block toggled\",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();return _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();return _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();return _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'\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 }","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();return _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 Popover from '../popover/popover.vue'\nimport { mapGetters } from 'vuex'\n\nconst ReactButton = {\n props: ['status'],\n data () {\n return {\n filterWord: ''\n }\n },\n components: {\n Popover\n },\n methods: {\n addReaction (event, emoji, close) {\n const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)\n if (existingReaction && existingReaction.me) {\n this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })\n } else {\n this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })\n }\n close()\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-185f65eb\\\",\\\"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('Popover',{staticClass:\"react-button-popover\",attrs:{\"trigger\":\"click\",\"placement\":\"top\",\"offset\":{ y: 5 }},scopedSlots:_vm._u([{key:\"content\",fn:function(ref){\nvar close = ref.close;\nreturn _c('div',{},[_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){return _vm.addReaction($event, emoji, close)}}},[_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){return _vm.addReaction($event, emoji.replacement, close)}}},[_vm._v(\"\\n \"+_vm._s(emoji.replacement)+\"\\n \")])}),_vm._v(\" \"),_c('div',{staticClass:\"reaction-bottom-fader\"})],2)])}}])},[_vm._v(\" \"),_c('i',{staticClass:\"icon-smile button-icon add-reaction-button\",attrs:{\"slot\":\"trigger\",\"title\":_vm.$t('tool_tip.add_reaction')},slot:\"trigger\"})])}\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();return _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 Popover from '../popover/popover.vue'\n\nconst ExtraButtons = {\n props: [ 'status' ],\n components: { Popover },\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 copyLink () {\n navigator.clipboard.writeText(this.statusLink)\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 statusLink () {\n return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`\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-b30b8de6\\\",\\\"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 _c('Popover',{staticClass:\"extra-button-popover\",attrs:{\"trigger\":\"click\",\"placement\":\"top\"},scopedSlots:_vm._u([{key:\"content\",fn:function(ref){\nvar close = ref.close;\nreturn _c('div',{},[_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',{staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":[function($event){$event.preventDefault();return _vm.pinStatus($event)},close]}},[_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',{staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":[function($event){$event.preventDefault();return _vm.unpinStatus($event)},close]}},[_c('i',{staticClass:\"icon-pin\"}),_c('span',[_vm._v(_vm._s(_vm.$t(\"status.unpin\")))])]):_vm._e(),_vm._v(\" \"),(_vm.canDelete)?_c('button',{staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":[function($event){$event.preventDefault();return _vm.deleteStatus($event)},close]}},[_c('i',{staticClass:\"icon-cancel\"}),_c('span',[_vm._v(_vm._s(_vm.$t(\"status.delete\")))])]):_vm._e(),_vm._v(\" \"),_c('button',{staticClass:\"dropdown-item dropdown-item-icon\",on:{\"click\":[function($event){$event.preventDefault();return _vm.copyLink($event)},close]}},[_c('i',{staticClass:\"icon-share\"}),_c('span',[_vm._v(_vm._s(_vm.$t(\"status.copy_link\")))])])])])}}])},[_vm._v(\" \"),_c('i',{staticClass:\"icon-ellipsis button-icon\",attrs:{\"slot\":\"trigger\"},slot:\"trigger\"})])}\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 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-61e0eb0c\\\",\\\"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 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-db51c57e\\\",\\\"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){return _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 }","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 Attachment from '../attachment/attachment.vue'\nimport Poll from '../poll/poll.vue'\nimport Gallery from '../gallery/gallery.vue'\nimport LinkPreview from '../link-preview/link-preview.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 { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'\nimport { mapGetters, mapState } from 'vuex'\n\nconst StatusContent = {\n name: 'StatusContent',\n props: [\n 'status',\n 'focused',\n 'noHeading',\n 'fullContent'\n ],\n data () {\n return {\n showingTall: this.inConversation && this.focused,\n showingLongSubject: false,\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 hideAttachments () {\n return (this.mergedConfig.hideAttachments && !this.inConversation) ||\n (this.mergedConfig.hideAttachmentsInConv && this.inConversation)\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 // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.\n mightHideBecauseSubject () {\n return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)\n },\n mightHideBecauseTall () {\n return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)\n },\n hideSubjectStatus () {\n return this.mightHideBecauseSubject && !this.expandingSubject\n },\n hideTallStatus () {\n return this.mightHideBecauseTall && !this.showingTall\n },\n showingMore () {\n return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && 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 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 hasImageAttachments () {\n return this.status.attachments.some(\n file => fileType.fileType(file.mimetype) === 'image'\n )\n },\n hasVideoAttachments () {\n return this.status.attachments.some(\n file => fileType.fileType(file.mimetype) === 'video'\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 ...mapGetters(['mergedConfig']),\n ...mapState({\n betterShadow: state => state.interface.browserSupport.cssFilter,\n currentUser: state => state.users.currentUser\n })\n },\n components: {\n Attachment,\n Poll,\n Gallery,\n LinkPreview\n },\n methods: {\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 toggleShowMore () {\n if (this.mightHideBecauseTall) {\n this.showingTall = !this.showingTall\n } else if (this.mightHideBecauseSubject) {\n this.expandingSubject = !this.expandingSubject\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}\n\nexport default StatusContent\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_content.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./status_content.js\"\nimport __vue_script__ from \"!!babel-loader!./status_content.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-43c5cfd4\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./status_content.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:\"status-body\"},[_vm._t(\"header\"),_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.focused },attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();_vm.showingLongSubject=true}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t(\"general.show_more\"))+\"\\n \"),(_vm.hasImageAttachments)?_c('span',{staticClass:\"icon-picture\"}):_vm._e(),_vm._v(\" \"),(_vm.hasVideoAttachments)?_c('span',{staticClass:\"icon-video\"}):_vm._e(),_vm._v(\" \"),(_vm.status.card)?_c('span',{staticClass:\"icon-link\"}):_vm._e()]):_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.focused },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.length !== 0 && (!_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(\" \"),_vm._t(\"footer\")],2)}\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 error: false\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 Popover: () => import('../popover/popover.vue')\n },\n methods: {\n enter () {\n if (!this.status) {\n this.$store.dispatch('fetchStatus', this.statusId)\n .then(data => (this.error = false))\n .catch(e => (this.error = true))\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-3b873076\\\",\\\"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('Popover',{attrs:{\"trigger\":\"hover\",\"popover-class\":\"status-popover\",\"bound-to\":{ x: 'container' }},on:{\"show\":_vm.enter}},[_c('template',{slot:\"trigger\"},[_vm._t(\"default\")],2),_vm._v(\" \"),_c('div',{attrs:{\"slot\":\"content\"},slot:\"content\"},[(_vm.status)?_c('Status',{attrs:{\"is-preview\":true,\"statusoid\":_vm.status,\"compact\":true}}):(_vm.error)?_c('div',{staticClass:\"status-preview-no-content faint\"},[_vm._v(\"\\n \"+_vm._s(_vm.$t('status.status_unavailable'))+\"\\n \")]):_c('div',{staticClass:\"status-preview-no-content\"},[_c('i',{staticClass:\"icon-spin4 animate-spin\"})])],1)],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import UserAvatar from '../user_avatar/user_avatar.vue'\nimport Popover from '../popover/popover.vue'\n\nconst EMOJI_REACTION_COUNT_CUTOFF = 12\n\nconst EmojiReactions = {\n name: 'EmojiReactions',\n components: {\n UserAvatar,\n Popover\n },\n props: ['status'],\n data: () => ({\n showAll: false\n }),\n computed: {\n tooManyReactions () {\n return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF\n },\n emojiReactions () {\n return this.showAll\n ? this.status.emoji_reactions\n : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)\n },\n showMoreString () {\n return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`\n },\n accountsForEmoji () {\n return this.status.emoji_reactions.reduce((acc, reaction) => {\n acc[reaction.name] = reaction.accounts || []\n return acc\n }, {})\n },\n loggedIn () {\n return !!this.$store.state.users.currentUser\n }\n },\n methods: {\n toggleShowAll () {\n this.showAll = !this.showAll\n },\n reactedWith (emoji) {\n return this.status.emoji_reactions.find(r => r.name === emoji).me\n },\n fetchEmojiReactionsByIfMissing () {\n const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)\n if (hasNoAccounts) {\n this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)\n }\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.loggedIn) return\n\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-09ec7fb6\\\",\\\"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('Popover',{key:reaction.name,attrs:{\"trigger\":\"hover\",\"placement\":\"top\",\"offset\":{ y: 5 }}},[_c('div',{staticClass:\"reacted-users\",attrs:{\"slot\":\"content\"},slot:\"content\"},[(_vm.accountsForEmoji[reaction.name].length)?_c('div',_vm._l((_vm.accountsForEmoji[reaction.name]),function(account){return _c('div',{key:account.id,staticClass:\"reacted-user\"},[_c('UserAvatar',{staticClass:\"avatar-small\",attrs:{\"user\":account,\"compact\":true}}),_vm._v(\" \"),_c('div',{staticClass:\"reacted-user-names\"},[_c('span',{staticClass:\"reacted-user-name\",domProps:{\"innerHTML\":_vm._s(account.name_html)}}),_vm._v(\" \"),_c('span',{staticClass:\"reacted-user-screen-name\"},[_vm._v(_vm._s(account.screen_name))])])],1)}),0):_c('div',[_c('i',{staticClass:\"icon-spin4 animate-spin\"})])]),_vm._v(\" \"),_c('button',{staticClass:\"emoji-reaction btn btn-default\",class:{ 'picked-reaction': _vm.reactedWith(reaction.name), 'not-clickable': !_vm.loggedIn },attrs:{\"slot\":\"trigger\"},on:{\"click\":function($event){return _vm.emojiOnClick(reaction.name, $event)},\"mouseenter\":function($event){return _vm.fetchEmojiReactionsByIfMissing()}},slot:\"trigger\"},[_c('span',{staticClass:\"reaction-emoji\"},[_vm._v(_vm._s(reaction.name))]),_vm._v(\" \"),_c('span',[_vm._v(_vm._s(reaction.count))])])])}),_vm._v(\" \"),(_vm.tooManyReactions)?_c('a',{staticClass:\"emoji-reaction-expand faint\",attrs:{\"href\":\"javascript:void(0)\"},on:{\"click\":_vm.toggleShowAll}},[_vm._v(\"\\n \"+_vm._s(_vm.showAll ? _vm.$t('general.show_less') : _vm.showMoreString)+\"\\n \")]):_vm._e()],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import FavoriteButton from '../favorite_button/favorite_button.vue'\nimport ReactButton from '../react_button/react_button.vue'\nimport RetweetButton from '../retweet_button/retweet_button.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 AvatarList from '../avatar_list/avatar_list.vue'\nimport Timeago from '../timeago/timeago.vue'\nimport StatusContent from '../status_content/status_content.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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.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 error: null\n }\n },\n computed: {\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 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 () {\n const relationship = this.$store.getters.relationship(this.status.user.id)\n return !this.unmuted && (\n (!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) ||\n (!this.inConversation && this.status.thread_muted) ||\n this.muteWordHits.length > 0)\n },\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 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 // There's zero guarantee of this working. If we happen to have that user and their\n // relationship in store then it will work, but there's kinda little chance of having\n // them for people you're not following.\n const relationship = this.$store.state.users.relationships[this.status.attentions[i].id]\n if (checkFollowing && relationship && relationship.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 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 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 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 FavoriteButton,\n ReactButton,\n RetweetButton,\n ExtraButtons,\n PostStatusForm,\n UserCard,\n UserAvatar,\n AvatarList,\n Timeago,\n StatusPopover,\n EmojiReactions,\n StatusContent\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 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 generateUserProfileLink (id, name) {\n return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)\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","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-2d68efa0\\\",\\\"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-id\":_vm.status.user.id,\"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',{staticClass:\"reply-to-popover\",staticStyle:{\"min-width\":\"0\"},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();return _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();return _vm.gotoOriginal(reply.id)}}},[_vm._v(_vm._s(reply.name))])])})],2):_vm._e()])]):_vm._e(),_vm._v(\" \"),_c('StatusContent',{attrs:{\"status\":_vm.status,\"no-heading\":_vm.noHeading,\"highlight\":_vm.highlight,\"focused\":_vm.isFocused}}),_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(\" \"),((_vm.mergedConfig.emojiReactionsOnTimeline || _vm.isFocused) && (!_vm.noHeading && !_vm.isPreview))?_c('EmojiReactions',{attrs:{\"status\":_vm.status}}):_vm._e(),_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(\" \"),(_vm.loggedIn)?_c('ReactButton',{attrs:{\"status\":_vm.status}}):_vm._e(),_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 }","\nconst Popover = {\n name: 'Popover',\n props: {\n // Action to trigger popover: either 'hover' or 'click'\n trigger: String,\n // Either 'top' or 'bottom'\n placement: String,\n // Takes object with properties 'x' and 'y', values of these can be\n // 'container' for using offsetParent as boundaries for either axis\n // or 'viewport'\n boundTo: Object,\n // Takes a top/bottom/left/right object, how much space to leave\n // between boundary and popover element\n margin: Object,\n // Takes a x/y object and tells how many pixels to offset from\n // anchor point on either axis\n offset: Object,\n // Additional styles you may want for the popover container\n popoverClass: String\n },\n data () {\n return {\n hidden: true,\n styles: { opacity: 0 },\n oldSize: { width: 0, height: 0 }\n }\n },\n methods: {\n updateStyles () {\n if (this.hidden) {\n this.styles = {\n opacity: 0\n }\n return\n }\n\n // Popover will be anchored around this element, trigger ref is the container, so\n // its children are what are inside the slot. Expect only one slot=\"trigger\".\n const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el\n const screenBox = anchorEl.getBoundingClientRect()\n // Screen position of the origin point for popover\n const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }\n const content = this.$refs.content\n // Minor optimization, don't call a slow reflow call if we don't have to\n const parentBounds = this.boundTo &&\n (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&\n this.$el.offsetParent.getBoundingClientRect()\n const margin = this.margin || {}\n\n // What are the screen bounds for the popover? Viewport vs container\n // when using viewport, using default margin values to dodge the navbar\n const xBounds = this.boundTo && this.boundTo.x === 'container' ? {\n min: parentBounds.left + (margin.left || 0),\n max: parentBounds.right - (margin.right || 0)\n } : {\n min: 0 + (margin.left || 10),\n max: window.innerWidth - (margin.right || 10)\n }\n\n const yBounds = this.boundTo && this.boundTo.y === 'container' ? {\n min: parentBounds.top + (margin.top || 0),\n max: parentBounds.bottom - (margin.bottom || 0)\n } : {\n min: 0 + (margin.top || 50),\n max: window.innerHeight - (margin.bottom || 5)\n }\n\n let horizOffset = 0\n\n // If overflowing from left, move it so that it doesn't\n if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {\n horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min\n }\n\n // If overflowing from right, move it so that it doesn't\n if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {\n horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max\n }\n\n // Default to whatever user wished with placement prop\n let usingTop = this.placement !== 'bottom'\n\n // Handle special cases, first force to displaying on top if there's not space on bottom,\n // regardless of what placement value was. Then check if there's not space on top, and\n // force to bottom, again regardless of what placement value was.\n if (origin.y + content.offsetHeight > yBounds.max) usingTop = true\n if (origin.y - content.offsetHeight < yBounds.min) usingTop = false\n\n const yOffset = (this.offset && this.offset.y) || 0\n const translateY = usingTop\n ? -anchorEl.offsetHeight - yOffset - content.offsetHeight\n : yOffset\n\n const xOffset = (this.offset && this.offset.x) || 0\n const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset\n\n // Note, separate translateX and translateY avoids blurry text on chromium,\n // single translate or translate3d resulted in blurry text.\n this.styles = {\n opacity: 1,\n transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`\n }\n },\n showPopover () {\n if (this.hidden) this.$emit('show')\n this.hidden = false\n this.$nextTick(this.updateStyles)\n },\n hidePopover () {\n if (!this.hidden) this.$emit('close')\n this.hidden = true\n this.styles = { opacity: 0 }\n },\n onMouseenter (e) {\n if (this.trigger === 'hover') this.showPopover()\n },\n onMouseleave (e) {\n if (this.trigger === 'hover') this.hidePopover()\n },\n onClick (e) {\n if (this.trigger === 'click') {\n if (this.hidden) {\n this.showPopover()\n } else {\n this.hidePopover()\n }\n }\n },\n onClickOutside (e) {\n if (this.hidden) return\n if (this.$el.contains(e.target)) return\n this.hidePopover()\n }\n },\n updated () {\n // Monitor changes to content size, update styles only when content sizes have changed,\n // that should be the only time we need to move the popover box if we don't care about scroll\n // or resize\n const content = this.$refs.content\n if (!content) return\n if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {\n this.updateStyles()\n this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }\n }\n },\n created () {\n document.addEventListener('click', this.onClickOutside)\n },\n destroyed () {\n document.removeEventListener('click', this.onClickOutside)\n this.hidePopover()\n }\n}\n\nexport default Popover\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!./popover.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./popover.js\"\nimport __vue_script__ from \"!!babel-loader!./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-10f1984d\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./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('div',{on:{\"mouseenter\":_vm.onMouseenter,\"mouseleave\":_vm.onMouseleave}},[_c('div',{ref:\"trigger\",on:{\"click\":_vm.onClick}},[_vm._t(\"trigger\")],2),_vm._v(\" \"),(!_vm.hidden)?_c('div',{ref:\"content\",staticClass:\"popover\",class:_vm.popoverClass,style:(_vm.styles)},[_vm._t(\"content\",null,{\"close\":_vm.hidePopover})],2):_vm._e()])}\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(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }$event.stopPropagation();$event.preventDefault();return _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){return _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: (query) => this.$store.dispatch('searchUsers', { query })\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();return _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();return _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();return _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();return _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(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }if(!$event.metaKey){ return null; }return _vm.postStatus(_vm.newStatus)},\"keyup\":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }if(!$event.ctrlKey){ return null; }return _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){return _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().match(noPrefix))\n .sort((a, b) => {\n let aScore = 0\n let bScore = 0\n\n // An exact match always wins\n aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0\n bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0\n\n // Prioritize custom emoji a lot\n aScore += a.imageUrl ? 100 : 0\n bScore += b.imageUrl ? 100 : 0\n\n // Prioritize prefix matches somewhat\n aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0\n bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0\n\n // Sort by length\n aScore -= a.displayText.length\n bScore -= b.displayText.length\n\n // Break ties alphabetically\n const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5\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();return _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();return _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