-v -1 `
7. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any.
8. Restart the Pleroma service.
-
+9. After you've restarted Pleroma, you will notice that postgres will take up more cpu resources than usual. A lot in fact. To fix this you must do a VACUUM ANLAYZE. This can also be done while the instance is still running like so:
+ $ sudo -u postgres psql pleroma_database_name
+ pleroma=# VACUUM ANALYZE;
[^1]: Prefix with `MIX_ENV=prod` to run it using the production config file.
## Remove
diff --git a/docs/configuration/howto_theming_your_instance.md b/docs/configuration/howto_theming_your_instance.md
new file mode 100644
index 000000000..d0daf5b25
--- /dev/null
+++ b/docs/configuration/howto_theming_your_instance.md
@@ -0,0 +1,74 @@
+# Theming your instance
+
+To add a custom theme to your instance, you'll first need to get a custom theme, upload it to the server, make it available to the instance and eventually you can set it as default.
+
+## Getting a custom theme
+
+### Create your own theme
+
+* You can create your own theme using the Pleroma FE by going to settings (gear on the top right) and choose the Theme tab. Here you have the options to create a personal theme.
+* To download your theme, you can do Save preset
+* If you want to upload a theme to customise it further, you can upload it using Load preset
+
+This will only save the theme for you personally. To make it available to the whole instance, you'll need to upload it to the server.
+
+### Get an existing theme
+
+* You can download a theme from another instance by going to that instance, go to settings and make sure you have the theme selected that you want. Then you can do Save preset to download it.
+* You can also find and download custom themes at
+
+## Adding the custom theme to the instance
+
+### Upload the theme to the server
+
+Themes can be found in the [static directory](static_dir.md). Create `STATIC-DIR/static/themes/` if needed and copy your theme there. Next you need to add an entry for your theme to `STATIC-DIR/static/styles.json`. If you use a from source installation, you'll first need to copy the file from `priv/static/static/styles.json`.
+
+Example of `styles.json` where we add our own `my-awesome-theme.json`
+```json
+{
+ "pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
+ "ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
+ "monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
+
+ "redmond-xx": "/static/themes/redmond-xx.json",
+ "redmond-xx-se": "/static/themes/redmond-xx-se.json",
+ "redmond-xxi": "/static/themes/redmond-xxi.json",
+ "breezy-dark": "/static/themes/breezy-dark.json",
+ "breezy-light": "/static/themes/breezy-light.json",
+ "mammal": "/static/themes/mammal.json",
+ "my-awesome-theme": "/static/themes/my-awesome-theme.json"
+}
+```
+
+Now you'll already be able to select the theme in Pleroma FE from the drop-down. You don't need to restart Pleroma because we only changed static served files. You may need to refresh the page in your browser. You'll notice however that the theme doesn't have a name, it's just an empty entry in the drop-down.
+
+### Give the theme a name
+
+When you open one of the themes that ship with Pleroma, you'll notice that the json has a `"name"` key. Add a key-value pair to your theme where the key name is `"name"` and the value the name you want to give your theme. After this you can refresh te page in your browser and the name should be visible in the drop-down.
+
+Example of `my-awesome-theme.json` where we add the name "My Awesome Theme"
+```json
+{
+ "_pleroma_theme_version": 2,
+ "name": "My Awesome Theme",
+ "theme": {}
+}
+```
+
+### Set as default theme
+
+Now we can set the new theme as default in the [Pleroma FE configuration](General-tips-for-customizing-Pleroma-FE.md).
+
+Example of adding the new theme in the back-end config files
+```elixir
+config :pleroma, :frontend_configurations,
+ pleroma_fe: %{
+ theme: "my-awesome-theme"
+ }
+```
+
+If you added it in the back-end configuration file, you'll need to restart your instance for the changes to take effect. If you don't see the changes, it's probably because the browser has cached the previous theme. In that case you'll want to clear browser caches. Alternatively you can use a private/incognito window just to see the changes.
+
diff --git a/docs/introduction.md b/docs/introduction.md
index 823e14f53..a915c143c 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -41,7 +41,7 @@ On the top right you will also see a wrench icon. This opens your personal setti
This is where the interesting stuff happens!
Depending on the timeline you will see different statuses, but each status has a standard structure:
-- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status). Clicking on the profile pic will uncollapse the user's profile.
+- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the reply-to status). Clicking on the profile pic will uncollapse the user's profile.
- A `+` button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
- An arrow icon allows you to open the status on the instance where it's originating from.
- The text of the status, including mentions and attachements. If you click on a mention, it will automatically open the profile page of that person.
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index b4e8d3a0b..adac47c86 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -185,11 +185,7 @@ def run(["gen-pack", src]) do
tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
- {:ok, _} =
- :zip.unzip(
- binary_archive,
- cwd: tmp_pack_dir
- )
+ {:ok, _} = :zip.unzip(binary_archive, cwd: String.to_charlist(tmp_pack_dir))
emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 9af6cda30..54d34e42f 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -6,6 +6,8 @@ defmodule Mix.Tasks.Pleroma.Instance do
use Mix.Task
import Mix.Pleroma
+ alias Pleroma.Config
+
@shortdoc "Manages Pleroma instance"
@moduledoc File.read!("docs/administration/CLI_tasks/instance.md")
@@ -63,7 +65,8 @@ def run(["gen" | rest]) do
get_option(
options,
:instance_name,
- "What is the name of your instance? (e.g. Pleroma/Soykaf)"
+ "What is the name of your instance? (e.g. The Corndog Emporium)",
+ domain
)
email = get_option(options, :admin_email, "What is your admin email address?")
@@ -153,6 +156,8 @@ def run(["gen" | rest]) do
Pleroma.Config.get([:instance, :static_dir])
)
+ Config.put([:instance, :static_dir], static_dir)
+
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
@@ -202,8 +207,14 @@ def run(["gen" | rest]) do
write_robots_txt(indexable, template_dir)
shell_info(
- "\n All files successfully written! Refer to the installation instructions for your platform for next steps"
+ "\n All files successfully written! Refer to the installation instructions for your platform for next steps."
)
+
+ if db_configurable? do
+ shell_info(
+ " Please transfer your config to the database after running database migrations. Refer to \"Transfering the config to/from the database\" section of the docs for more information."
+ )
+ end
else
shell_error(
"The task would have overwritten the following files:\n" <>
diff --git a/lib/mix/tasks/pleroma/refresh_counter_cache.ex b/lib/mix/tasks/pleroma/refresh_counter_cache.ex
new file mode 100644
index 000000000..bc2571efd
--- /dev/null
+++ b/lib/mix/tasks/pleroma/refresh_counter_cache.ex
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.RefreshCounterCache do
+ @shortdoc "Refreshes counter cache"
+
+ use Mix.Task
+
+ alias Pleroma.Activity
+ alias Pleroma.CounterCache
+ alias Pleroma.Repo
+
+ require Logger
+ import Ecto.Query
+
+ def run([]) do
+ Mix.Pleroma.start_pleroma()
+
+ ["public", "unlisted", "private", "direct"]
+ |> Enum.each(fn visibility ->
+ count = status_visibility_count_query(visibility)
+ name = "status_visibility_#{visibility}"
+ CounterCache.set(name, count)
+ Mix.Pleroma.shell_info("Set #{name} to #{count}")
+ end)
+
+ Mix.Pleroma.shell_info("Done")
+ end
+
+ defp status_visibility_count_query(visibility) do
+ Activity
+ |> where(
+ [a],
+ fragment(
+ "activity_visibility(?, ?, ?) = ?",
+ a.actor,
+ a.recipients,
+ a.data,
+ ^visibility
+ )
+ )
+ |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
+ |> Repo.aggregate(:count, :id, timeout: :timer.minutes(30))
+ end
+end
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 85c9e4954..ba10a705a 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -100,8 +100,7 @@ def run(["rm", nickname]) do
User.perform(:delete, user)
shell_info("User #{nickname} deleted.")
else
- _ ->
- shell_error("No local user #{nickname}")
+ _ -> shell_error("No local user #{nickname}")
end
end
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 72e2256ea..7fb1f913b 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -310,7 +310,7 @@ def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
def restrict_deactivated_users(query) do
deactivated_users =
- from(u in User.Query.build(deactivated: true), select: u.ap_id)
+ from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
|> Repo.all()
Activity.Queries.exclude_authors(query, deactivated_users)
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
index 79f305201..8d08d81ca 100644
--- a/lib/pleroma/activity/queries.ex
+++ b/lib/pleroma/activity/queries.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Activity.Queries do
Contains queries for Activity.
"""
- import Ecto.Query, only: [from: 2]
+ import Ecto.Query, only: [from: 2, where: 3]
@type query :: Ecto.Queryable.t() | Activity.t()
@@ -30,7 +30,7 @@ def by_actor(query \\ Activity, actor) do
)
end
- @spec by_author(query, String.t()) :: query
+ @spec by_author(query, User.t()) :: query
def by_author(query \\ Activity, %User{ap_id: ap_id}) do
from(a in query, where: a.actor == ^ap_id)
end
@@ -63,6 +63,22 @@ def by_object_id(query, object_id) when is_binary(object_id) do
)
end
+ @spec by_object_in_reply_to_id(query, String.t(), keyword()) :: query
+ def by_object_in_reply_to_id(query, in_reply_to_id, opts \\ []) do
+ query =
+ if opts[:skip_preloading] do
+ Activity.with_joined_object(query)
+ else
+ Activity.with_preloaded_object(query)
+ end
+
+ where(
+ query,
+ [activity, object: o],
+ fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(in_reply_to_id))
+ )
+ end
+
@spec by_type(query, String.t()) :: query
def by_type(query \\ Activity, activity_type) do
from(
diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex
index c2765a5b8..cf75c3adc 100644
--- a/lib/pleroma/captcha/captcha.ex
+++ b/lib/pleroma/captcha/captcha.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors
+# Copyright © 2017-2020 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do
@@ -50,7 +50,7 @@ def handle_call(:new, _from, state) do
token = new_captcha[:token]
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
- # Basicallty copy what Phoenix.Token does here, add the time to
+ # Basically copy what Phoenix.Token does here, add the time to
# the actual data and make it a binary to then encrypt it
encrypted_captcha_answer =
%{
@@ -62,7 +62,7 @@ def handle_call(:new, _from, state) do
{
:reply,
- # Repalce the answer with the encrypted answer
+ # Replace the answer with the encrypted answer
%{new_captcha | answer_data: encrypted_captcha_answer},
state
}
@@ -82,7 +82,8 @@ def handle_call({:validate, token, captcha, answer_data}, _from, state) do
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid)
result =
- with {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
+ with false <- is_nil(answer_data),
+ {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
try do
if DateTime.before?(at, valid_if_after),
diff --git a/lib/pleroma/captcha/native.ex b/lib/pleroma/captcha/native.ex
index 5306fe1aa..2c8db2c30 100644
--- a/lib/pleroma/captcha/native.ex
+++ b/lib/pleroma/captcha/native.ex
@@ -10,8 +10,8 @@ defmodule Pleroma.Captcha.Native do
@impl Service
def new do
case Captcha.get() do
- {:timeout} ->
- %{error: dgettext("errors", "Captcha timeout")}
+ :error ->
+ %{error: dgettext("errors", "Captcha error")}
{:ok, answer_data, img_binary} ->
%{
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index e5d28ebff..d4b255537 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -133,10 +133,8 @@ def restrict_recipients(query, user, %{"recipients" => user_ids}) do
[user.id | user_ids]
|> Enum.uniq()
|> Enum.reduce([], fn user_id, acc ->
- case FlakeId.Ecto.CompatType.dump(user_id) do
- {:ok, user_id} -> [user_id | acc]
- _ -> acc
- end
+ {:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
+ [user_id | acc]
end)
conversation_subquery =
diff --git a/lib/pleroma/counter_cache.ex b/lib/pleroma/counter_cache.ex
new file mode 100644
index 000000000..8e868e956
--- /dev/null
+++ b/lib/pleroma/counter_cache.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.CounterCache do
+ alias Pleroma.CounterCache
+ alias Pleroma.Repo
+ use Ecto.Schema
+ import Ecto.Changeset
+ import Ecto.Query
+
+ schema "counter_cache" do
+ field(:name, :string)
+ field(:count, :integer)
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [:name, :count])
+ |> validate_required([:name])
+ |> unique_constraint(:name)
+ end
+
+ def get_as_map(names) when is_list(names) do
+ CounterCache
+ |> where([cc], cc.name in ^names)
+ |> Repo.all()
+ |> Enum.group_by(& &1.name, & &1.count)
+ |> Map.new(fn {k, v} -> {k, hd(v)} end)
+ end
+
+ def set(name, count) do
+ %CounterCache{}
+ |> changeset(%{"name" => name, "count" => count})
+ |> Repo.insert(
+ on_conflict: [set: [count: count]],
+ returning: true,
+ conflict_target: :name
+ )
+ end
+end
diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex
index 36771533f..08f96f7bf 100644
--- a/lib/pleroma/mime.ex
+++ b/lib/pleroma/mime.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.MIME do
@default "application/octet-stream"
@read_bytes 35
- @spec file_mime_type(String.t()) ::
+ @spec file_mime_type(String.t(), String.t()) ::
{:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error
def file_mime_type(path, filename) do
with {:ok, content_type} <- file_mime_type(path),
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 52556bf31..f316f8b36 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -301,4 +301,26 @@ def update_data(%Object{data: data} = object, attrs \\ %{}) do
def local?(%Object{data: %{"id" => id}}) do
String.starts_with?(id, Pleroma.Web.base_url() <> "/")
end
+
+ def replies(object, opts \\ []) do
+ object = Object.normalize(object)
+
+ query =
+ Object
+ |> where(
+ [o],
+ fragment("(?)->>'inReplyTo' = ?", o.data, ^object.data["id"])
+ )
+ |> order_by([o], asc: o.id)
+
+ if opts[:self_only] do
+ actor = object.data["actor"]
+ where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
+ else
+ query
+ end
+ end
+
+ def self_replies(object, opts \\ []),
+ do: replies(object, Keyword.put(opts, :self_only, true))
end
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index 25aa32f60..9efa50edb 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -39,15 +39,8 @@ def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor)
defp compare_uris(_, %URI{scheme: "tag"}), do: :ok
end
- defp compare_uris(%URI{} = id_uri, %URI{} = other_uri) do
- if id_uri.host == other_uri.host do
- :ok
- else
- :error
- end
- end
-
- defp compare_uris(_, _), do: :error
+ defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
+ defp compare_uris(_id_uri, _other_uri), do: :error
@doc """
Checks that an imported AP object's actor matches the domain it came from.
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 5e9bf1574..d7fd35f1d 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.Federator
require Logger
require Pleroma.Constants
@@ -59,20 +60,23 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do
end
end
- # TODO:
- # This will create a Create activity, which we need internally at the moment.
+ # Note: will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do
- with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
- {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
- {:normalize, nil} <- {:normalize, Object.normalize(data, false)},
+ with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
+ {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
+ {_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
+ {_, nil} <- {:normalize, Object.normalize(data, false)},
params <- prepare_activity_params(data),
- {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
- {:transmogrifier, {:ok, activity}} <-
+ {_, :ok} <- {:containment, Containment.contain_origin(id, params)},
+ {_, {:ok, activity}} <-
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
- {:object, _data, %Object{} = object} <-
+ {_, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object}
else
+ {:allowed_depth, false} ->
+ {:error, "Max thread distance exceeded."}
+
{:containment, _} ->
{:error, "Object containment failed."}
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index cf590fb01..771a06e32 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Stats do
import Ecto.Query
+ alias Pleroma.CounterCache
alias Pleroma.Repo
alias Pleroma.User
@@ -96,4 +97,21 @@ defp get_stat_data do
}
}
end
+
+ def get_status_visibility_count do
+ counter_cache =
+ CounterCache.get_as_map([
+ "status_visibility_public",
+ "status_visibility_private",
+ "status_visibility_unlisted",
+ "status_visibility_direct"
+ ])
+
+ %{
+ public: counter_cache["status_visibility_public"] || 0,
+ unlisted: counter_cache["status_visibility_unlisted"] || 0,
+ private: counter_cache["status_visibility_private"] || 0,
+ direct: counter_cache["status_visibility_direct"] || 0
+ }
+ end
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 5ea36fea3..56e599ecc 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -853,14 +853,14 @@ def get_followers_query(user, page) do
@spec get_followers_query(User.t()) :: Ecto.Query.t()
def get_followers_query(user), do: get_followers_query(user, nil)
- @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
+ @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do
user
|> get_followers_query(page)
|> Repo.all()
end
- @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
+ @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do
user
|> get_followers_query(page)
@@ -1304,7 +1304,6 @@ def perform(:delete, %User{} = user) do
Repo.delete(user)
end
- @spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:fetch_initial_posts, %User{} = user) do
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
@@ -1336,7 +1335,6 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
)
end
- @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:follow_import, %User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index 364bc1c89..4358907cb 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -48,7 +48,7 @@ defmodule Pleroma.User.Query do
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
- nickname: [String.t()],
+ nickname: [String.t()] | String.t(),
ap_id: [String.t()],
order_by: term(),
select: term(),
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index 6b55df483..1cfecef83 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -33,9 +33,15 @@ defp format_query(query_string) do
# Strip the beginning @ off if there is a query
query_string = String.trim_leading(query_string, "@")
- with [name, domain] <- String.split(query_string, "@"),
- formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "") do
- name <> "@" <> to_string(:idna.encode(formatted_domain))
+ with [name, domain] <- String.split(query_string, "@") do
+ encoded_domain =
+ domain
+ |> String.replace(~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "")
+ |> String.to_charlist()
+ |> :idna.encode()
+ |> to_string()
+
+ name <> "@" <> encoded_domain
else
_ -> query_string
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 5c436941a..12695b3f9 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -770,13 +770,18 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do
|> Enum.reverse()
end
- def fetch_instance_activities(params) do
+ def fetch_statuses(reading_user, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
- |> Map.put("instance", params["instance"])
- fetch_activities([Pleroma.Constants.as_public()], params, :offset)
+ recipients =
+ user_activities_recipients(%{
+ "godmode" => params["godmode"],
+ "reading_user" => reading_user
+ })
+
+ fetch_activities(recipients, params, :offset)
|> Enum.reverse()
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index a72d8430f..3afc82345 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -156,10 +156,11 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
+ depth = (options[:depth] || 0) + 1
- if Federator.allowed_incoming_reply_depth?(options[:depth]) do
+ if Federator.allowed_thread_distance?(depth) do
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
- %Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
+ %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
@@ -312,7 +313,7 @@ def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
- with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
+ with true <- Federator.allowed_thread_distance?(options[:depth]),
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer")
else
@@ -406,8 +407,7 @@ def handle_incoming(
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
- options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
- object = fix_object(data["object"], options)
+ object = fix_object(object, options)
params = %{
to: data["to"],
@@ -424,7 +424,20 @@ def handle_incoming(
])
}
- ActivityPub.create(params)
+ with {:ok, created_activity} <- ActivityPub.create(params) do
+ reply_depth = (options[:depth] || 0) + 1
+
+ if Federator.allowed_thread_distance?(reply_depth) do
+ for reply_id <- replies(object) do
+ Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
+ "id" => reply_id,
+ "depth" => reply_depth
+ })
+ end
+ end
+
+ {:ok, created_activity}
+ end
else
%Activity{} = activity -> {:ok, activity}
_e -> :error
@@ -442,7 +455,8 @@ def handle_incoming(
|> fix_addressing
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
- options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
+ reply_depth = (options[:depth] || 0) + 1
+ options = Keyword.put(options, :depth, reply_depth)
object = fix_object(object, options)
params = %{
@@ -903,6 +917,50 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
def set_reply_to_uri(obj), do: obj
+ @doc """
+ Serialized Mastodon-compatible `replies` collection containing _self-replies_.
+ Based on Mastodon's ActivityPub::NoteSerializer#replies.
+ """
+ def set_replies(obj_data) do
+ replies_uris =
+ with limit when limit > 0 <-
+ Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
+ %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
+ object
+ |> Object.self_replies()
+ |> select([o], fragment("?->>'id'", o.data))
+ |> limit(^limit)
+ |> Repo.all()
+ else
+ _ -> []
+ end
+
+ set_replies(obj_data, replies_uris)
+ end
+
+ defp set_replies(obj, []) do
+ obj
+ end
+
+ defp set_replies(obj, replies_uris) do
+ replies_collection = %{
+ "type" => "Collection",
+ "items" => replies_uris
+ }
+
+ Map.merge(obj, %{"replies" => replies_collection})
+ end
+
+ def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
+ items
+ end
+
+ def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
+ items
+ end
+
+ def replies(_), do: []
+
# Prepares the object of an outgoing create activity.
def prepare_object(object) do
object
@@ -914,6 +972,7 @@ def prepare_object(object) do
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
+ |> set_replies
|> strip_internal_fields
|> strip_internal_tags
|> set_type
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 10ce5eee8..50e076ca4 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -45,8 +45,8 @@ def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
- @spec determine_explicit_mentions(map()) :: map()
- def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do
+ @spec determine_explicit_mentions(map()) :: [any]
+ def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
Enum.flat_map(tag, fn
%{"type" => "Mention", "href" => href} -> [href]
_ -> []
@@ -427,7 +427,7 @@ defp fetch_likes(object) do
@doc """
Updates a follow activity's state (for locked accounts).
"""
- @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
+ @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 67222ebae..8804343b9 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
+ alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -98,7 +99,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
- when action in [:config_show, :list_log]
+ when action in [:config_show, :list_log, :stats]
)
plug(
@@ -243,13 +244,15 @@ def user_show(conn, %{"nickname" => nickname}) do
end
def list_instance_statuses(conn, %{"instance" => instance} = params) do
+ with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params)
activities =
- ActivityPub.fetch_instance_activities(%{
+ ActivityPub.fetch_statuses(nil, %{
"instance" => instance,
"limit" => page_size,
- "offset" => (page - 1) * page_size
+ "offset" => (page - 1) * page_size,
+ "exclude_reblogs" => !with_reblogs && "true"
})
conn
@@ -258,6 +261,7 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
+ with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
godmode = params["godmode"] == "true" || params["godmode"] == true
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
@@ -266,7 +270,8 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
activities =
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => page_size,
- "godmode" => godmode
+ "godmode" => godmode,
+ "exclude_reblogs" => !with_reblogs && "true"
})
conn
@@ -740,6 +745,24 @@ def report_notes_delete(%{assigns: %{user: user}} = conn, %{
end
end
+ def list_statuses(%{assigns: %{user: admin}} = conn, params) do
+ godmode = params["godmode"] == "true" || params["godmode"] == true
+ local_only = params["local_only"] == "true" || params["local_only"] == true
+ {page, page_size} = page_params(params)
+
+ activities =
+ ActivityPub.fetch_statuses(admin, %{
+ "godmode" => godmode,
+ "local_only" => local_only,
+ "limit" => page_size,
+ "offset" => (page - 1) * page_size
+ })
+
+ conn
+ |> put_view(Pleroma.Web.AdminAPI.StatusView)
+ |> render("index.json", %{activities: activities, as: :activity})
+ end
+
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
@@ -953,6 +976,13 @@ def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" =
conn |> json("")
end
+ def stats(conn, _) do
+ count = Stats.get_status_visibility_count()
+
+ conn
+ |> json(%{"status_visibility" => count})
+ end
+
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex
index ed919833e..778cf4c36 100644
--- a/lib/pleroma/web/admin_api/search.ex
+++ b/lib/pleroma/web/admin_api/search.ex
@@ -18,7 +18,11 @@ defmacro not_empty_string(string) do
@spec user(map()) :: {:ok, [User.t()], pos_integer()}
def user(params \\ %{}) do
- query = User.Query.build(params) |> order_by([u], u.nickname)
+ query =
+ params
+ |> Map.drop([:page, :page_size])
+ |> User.Query.build()
+ |> order_by([u], u.nickname)
paginated_query =
User.Query.paginate(query, params[:page] || 1, params[:page_size] || @page_size)
diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex
index 6f2b2b09c..8ae8a7afe 100644
--- a/lib/pleroma/web/admin_api/views/status_view.ex
+++ b/lib/pleroma/web/admin_api/views/status_view.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
alias Pleroma.User
def render("index.json", opts) do
- render_many(opts.activities, __MODULE__, "show.json", opts)
+ safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index f506a7d24..013fb5b70 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -15,13 +15,19 @@ defmodule Pleroma.Web.Federator do
require Logger
- @doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)"
+ @doc """
+ Returns `true` if the distance to target object does not exceed max configured value.
+ Serves to prevent fetching of very long threads, especially useful on smaller instances.
+ Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161).
+ Applies to fetching of both ancestor (reply-to) and child (reply) objects.
+ """
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
- def allowed_incoming_reply_depth?(depth) do
- max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
+ def allowed_thread_distance?(distance) do
+ max_distance = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
- if max_replies_depth do
- (depth || 1) <= max_replies_depth
+ if max_distance && max_distance >= 0 do
+ # Default depth is 0 (an object has zero distance from itself in its thread)
+ (distance || 0) <= max_distance
else
true
end
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 333012920..947edd8b7 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -92,9 +92,9 @@ def raw_nodeinfo do
openRegistrations: Config.get([:instance, :registrations_open]),
usage: %{
users: %{
- total: stats.user_count || 0
+ total: Map.get(stats, :user_count, 0)
},
- localPosts: stats.status_count || 0
+ localPosts: Map.get(stats, :status_count, 0)
},
metadata: %{
nodeName: Config.get([:instance, :name]),
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
index a2f6d2287..03e95e020 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
@@ -323,7 +323,7 @@ def delete(conn, %{"name" => name}) do
{:ok, _} ->
conn |> json("ok")
- {:error, _} ->
+ {:error, _, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Couldn't delete the pack #{name}"})
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 9bfe86704..103c638b4 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -192,6 +192,7 @@ defmodule Pleroma.Web.Router do
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
+ get("/statuses", AdminAPIController, :list_statuses)
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
@@ -201,6 +202,7 @@ defmodule Pleroma.Web.Router do
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
+ get("/stats", AdminAPIController, :stats)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex
new file mode 100644
index 000000000..e860ca869
--- /dev/null
+++ b/lib/pleroma/workers/remote_fetcher_worker.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.RemoteFetcherWorker do
+ alias Pleroma.Object.Fetcher
+
+ use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher"
+
+ @impl Oban.Worker
+ def perform(
+ %{
+ "op" => "fetch_remote",
+ "id" => id
+ } = args,
+ _job
+ ) do
+ {:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])
+ end
+end
diff --git a/priv/repo/migrations/20200109123126_add_counter_cache_table.exs b/priv/repo/migrations/20200109123126_add_counter_cache_table.exs
new file mode 100644
index 000000000..df9e21193
--- /dev/null
+++ b/priv/repo/migrations/20200109123126_add_counter_cache_table.exs
@@ -0,0 +1,55 @@
+defmodule Pleroma.Repo.Migrations.AddCounterCacheTable do
+ use Ecto.Migration
+
+ def up do
+ create_if_not_exists table(:counter_cache) do
+ add(:name, :string, null: false)
+ add(:count, :bigint, null: false, default: 0)
+ end
+
+ create_if_not_exists(unique_index(:counter_cache, [:name]))
+
+ """
+ CREATE OR REPLACE FUNCTION update_status_visibility_counter_cache()
+ RETURNS TRIGGER AS
+ $$
+ DECLARE
+ BEGIN
+ IF TG_OP = 'INSERT' THEN
+ IF NEW.data->>'type' = 'Create' THEN
+ EXECUTE 'INSERT INTO counter_cache (name, count) VALUES (''status_visibility_' || activity_visibility(NEW.actor, NEW.recipients, NEW.data) || ''', 1) ON CONFLICT (name) DO UPDATE SET count = counter_cache.count + 1';
+ END IF;
+ RETURN NEW;
+ ELSIF TG_OP = 'UPDATE' THEN
+ IF (NEW.data->>'type' = 'Create') and (OLD.data->>'type' = 'Create') and activity_visibility(NEW.actor, NEW.recipients, NEW.data) != activity_visibility(OLD.actor, OLD.recipients, OLD.data) THEN
+ EXECUTE 'INSERT INTO counter_cache (name, count) VALUES (''status_visibility_' || activity_visibility(NEW.actor, NEW.recipients, NEW.data) || ''', 1) ON CONFLICT (name) DO UPDATE SET count = counter_cache.count + 1';
+ EXECUTE 'update counter_cache SET count = counter_cache.count - 1 where count > 0 and name = ''status_visibility_' || activity_visibility(OLD.actor, OLD.recipients, OLD.data) || ''';';
+ END IF;
+ RETURN NEW;
+ ELSIF TG_OP = 'DELETE' THEN
+ IF OLD.data->>'type' = 'Create' THEN
+ EXECUTE 'update counter_cache SET count = counter_cache.count - 1 where count > 0 and name = ''status_visibility_' || activity_visibility(OLD.actor, OLD.recipients, OLD.data) || ''';';
+ END IF;
+ RETURN OLD;
+ END IF;
+ END;
+ $$
+ LANGUAGE 'plpgsql';
+ """
+ |> execute()
+
+ """
+ CREATE TRIGGER status_visibility_counter_cache_trigger BEFORE INSERT OR UPDATE of recipients, data OR DELETE ON activities
+ FOR EACH ROW
+ EXECUTE PROCEDURE update_status_visibility_counter_cache();
+ """
+ |> execute()
+ end
+
+ def down do
+ execute("drop trigger if exists status_visibility_counter_cache_trigger on activities")
+ execute("drop function if exists update_status_visibility_counter_cache()")
+ drop_if_exists(unique_index(:counter_cache, [:name]))
+ drop_if_exists(table(:counter_cache))
+ end
+end
diff --git a/priv/static/adminfe/chunk-03b0.49362218.css b/priv/static/adminfe/chunk-03b0.49362218.css
new file mode 100644
index 000000000..e43c776aa
Binary files /dev/null and b/priv/static/adminfe/chunk-03b0.49362218.css differ
diff --git a/priv/static/adminfe/chunk-06de.3abb5de7.css b/priv/static/adminfe/chunk-06de.3abb5de7.css
deleted file mode 100644
index 4fb60b648..000000000
Binary files a/priv/static/adminfe/chunk-06de.3abb5de7.css and /dev/null differ
diff --git a/priv/static/adminfe/chunk-15fa.86ad6a40.css b/priv/static/adminfe/chunk-15fa.dc3643e6.css
similarity index 100%
rename from priv/static/adminfe/chunk-15fa.86ad6a40.css
rename to priv/static/adminfe/chunk-15fa.dc3643e6.css
diff --git a/priv/static/adminfe/chunk-17a5.edcdbe30.css b/priv/static/adminfe/chunk-17a5.edcdbe30.css
new file mode 100644
index 000000000..0b2a3f669
Binary files /dev/null and b/priv/static/adminfe/chunk-17a5.edcdbe30.css differ
diff --git a/priv/static/adminfe/chunk-20e0.ee636d82.css b/priv/static/adminfe/chunk-20e0.ee636d82.css
deleted file mode 100644
index 567079fed..000000000
Binary files a/priv/static/adminfe/chunk-20e0.ee636d82.css and /dev/null differ
diff --git a/priv/static/adminfe/chunk-293a.a8b5ee5b.css b/priv/static/adminfe/chunk-293a.a8b5ee5b.css
new file mode 100644
index 000000000..924633a80
Binary files /dev/null and b/priv/static/adminfe/chunk-293a.a8b5ee5b.css differ
diff --git a/priv/static/adminfe/chunk-2b8b.0f1ee211.css b/priv/static/adminfe/chunk-2b8b.0f1ee211.css
new file mode 100644
index 000000000..2857a9d6e
Binary files /dev/null and b/priv/static/adminfe/chunk-2b8b.0f1ee211.css differ
diff --git a/priv/static/adminfe/chunk-453a.bbab87da.css b/priv/static/adminfe/chunk-453a.bbab87da.css
new file mode 100644
index 000000000..d6cc7d182
Binary files /dev/null and b/priv/static/adminfe/chunk-453a.bbab87da.css differ
diff --git a/priv/static/adminfe/chunk-46cf.6dd5bbb7.css b/priv/static/adminfe/chunk-46cf.6dd5bbb7.css
new file mode 100644
index 000000000..aa7160528
Binary files /dev/null and b/priv/static/adminfe/chunk-46cf.6dd5bbb7.css differ
diff --git a/priv/static/adminfe/chunk-48a4.1bb1db91.css b/priv/static/adminfe/chunk-48a4.1bb1db91.css
deleted file mode 100644
index 48784b9d2..000000000
Binary files a/priv/static/adminfe/chunk-48a4.1bb1db91.css and /dev/null differ
diff --git a/priv/static/adminfe/chunk-51b0.55057987.css b/priv/static/adminfe/chunk-4e46.ad5e9ff3.css
similarity index 100%
rename from priv/static/adminfe/chunk-51b0.55057987.css
rename to priv/static/adminfe/chunk-4e46.ad5e9ff3.css
diff --git a/priv/static/adminfe/chunk-560d.802cfba1.css b/priv/static/adminfe/chunk-560d.802cfba1.css
new file mode 100644
index 000000000..a74b42d14
Binary files /dev/null and b/priv/static/adminfe/chunk-560d.802cfba1.css differ
diff --git a/priv/static/adminfe/chunk-645e.8bb40e00.css b/priv/static/adminfe/chunk-645e.8bb40e00.css
deleted file mode 100644
index 0591e4930..000000000
Binary files a/priv/static/adminfe/chunk-645e.8bb40e00.css and /dev/null differ
diff --git a/priv/static/adminfe/chunk-6aa3.fb02ac69.css b/priv/static/adminfe/chunk-6dd6.85f319f7.css
similarity index 100%
rename from priv/static/adminfe/chunk-6aa3.fb02ac69.css
rename to priv/static/adminfe/chunk-6dd6.85f319f7.css
diff --git a/priv/static/adminfe/chunk-7f8e.f03bd164.css b/priv/static/adminfe/chunk-7f8e.f03bd164.css
deleted file mode 100644
index 6cd674a28..000000000
Binary files a/priv/static/adminfe/chunk-7f8e.f03bd164.css and /dev/null differ
diff --git a/priv/static/adminfe/chunk-b4ba.e55f897a.css b/priv/static/adminfe/chunk-b4ba.e55f897a.css
deleted file mode 100644
index dadc5cbda..000000000
Binary files a/priv/static/adminfe/chunk-b4ba.e55f897a.css and /dev/null differ
diff --git a/priv/static/adminfe/chunk-bb29.61e9e8f2.css b/priv/static/adminfe/chunk-bb29.61e9e8f2.css
deleted file mode 100644
index bbab1c2ac..000000000
Binary files a/priv/static/adminfe/chunk-bb29.61e9e8f2.css and /dev/null differ
diff --git a/priv/static/adminfe/chunk-cf58.6bdb954d.css b/priv/static/adminfe/chunk-cf58.80435fa1.css
similarity index 55%
rename from priv/static/adminfe/chunk-cf58.6bdb954d.css
rename to priv/static/adminfe/chunk-cf58.80435fa1.css
index 69fc0072a..8b0f21153 100644
Binary files a/priv/static/adminfe/chunk-cf58.6bdb954d.css and b/priv/static/adminfe/chunk-cf58.80435fa1.css differ
diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html
index 0b08c3290..b0bdb162d 100644
--- a/priv/static/adminfe/index.html
+++ b/priv/static/adminfe/index.html
@@ -1 +1 @@
-Admin FE
\ No newline at end of file
+Admin FE
\ No newline at end of file
diff --git a/priv/static/adminfe/static/js/app.30262183.js b/priv/static/adminfe/static/js/app.30262183.js
new file mode 100644
index 000000000..c872d448f
Binary files /dev/null and b/priv/static/adminfe/static/js/app.30262183.js differ
diff --git a/priv/static/adminfe/static/js/app.30262183.js.map b/priv/static/adminfe/static/js/app.30262183.js.map
new file mode 100644
index 000000000..3711b8a98
Binary files /dev/null and b/priv/static/adminfe/static/js/app.30262183.js.map differ
diff --git a/priv/static/adminfe/static/js/app.5f0094e3.js b/priv/static/adminfe/static/js/app.5f0094e3.js
deleted file mode 100644
index e65b2f09c..000000000
Binary files a/priv/static/adminfe/static/js/app.5f0094e3.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/app.5f0094e3.js.map b/priv/static/adminfe/static/js/app.5f0094e3.js.map
deleted file mode 100644
index edb94a554..000000000
Binary files a/priv/static/adminfe/static/js/app.5f0094e3.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-03b0.7a203856.js b/priv/static/adminfe/static/js/chunk-03b0.7a203856.js
new file mode 100644
index 000000000..43ca0e4e6
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-03b0.7a203856.js differ
diff --git a/priv/static/adminfe/static/js/chunk-03b0.7a203856.js.map b/priv/static/adminfe/static/js/chunk-03b0.7a203856.js.map
new file mode 100644
index 000000000..697a106ac
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-03b0.7a203856.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-06de.ff4586ab.js b/priv/static/adminfe/static/js/chunk-06de.ff4586ab.js
deleted file mode 100644
index c989bed18..000000000
Binary files a/priv/static/adminfe/static/js/chunk-06de.ff4586ab.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-06de.ff4586ab.js.map b/priv/static/adminfe/static/js/chunk-06de.ff4586ab.js.map
deleted file mode 100644
index 5652f3192..000000000
Binary files a/priv/static/adminfe/static/js/chunk-06de.ff4586ab.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-15fa.99004e49.js b/priv/static/adminfe/static/js/chunk-15fa.15303f3a.js
similarity index 99%
rename from priv/static/adminfe/static/js/chunk-15fa.99004e49.js
rename to priv/static/adminfe/static/js/chunk-15fa.15303f3a.js
index 5ab46f8b9..7d3e0c56e 100644
Binary files a/priv/static/adminfe/static/js/chunk-15fa.99004e49.js and b/priv/static/adminfe/static/js/chunk-15fa.15303f3a.js differ
diff --git a/priv/static/adminfe/static/js/chunk-15fa.99004e49.js.map b/priv/static/adminfe/static/js/chunk-15fa.15303f3a.js.map
similarity index 99%
rename from priv/static/adminfe/static/js/chunk-15fa.99004e49.js.map
rename to priv/static/adminfe/static/js/chunk-15fa.15303f3a.js.map
index f4b92260a..f08d1dbf9 100644
Binary files a/priv/static/adminfe/static/js/chunk-15fa.99004e49.js.map and b/priv/static/adminfe/static/js/chunk-15fa.15303f3a.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-17a5.13b13757.js b/priv/static/adminfe/static/js/chunk-17a5.13b13757.js
new file mode 100644
index 000000000..80e7a8ac7
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-17a5.13b13757.js differ
diff --git a/priv/static/adminfe/static/js/chunk-17a5.13b13757.js.map b/priv/static/adminfe/static/js/chunk-17a5.13b13757.js.map
new file mode 100644
index 000000000..7da1a0077
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-17a5.13b13757.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-20e0.dc3e8a45.js b/priv/static/adminfe/static/js/chunk-20e0.dc3e8a45.js
deleted file mode 100644
index 5fa5107f4..000000000
Binary files a/priv/static/adminfe/static/js/chunk-20e0.dc3e8a45.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-20e0.dc3e8a45.js.map b/priv/static/adminfe/static/js/chunk-20e0.dc3e8a45.js.map
deleted file mode 100644
index 2186ce225..000000000
Binary files a/priv/static/adminfe/static/js/chunk-20e0.dc3e8a45.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-293a.a728de01.js b/priv/static/adminfe/static/js/chunk-293a.a728de01.js
new file mode 100644
index 000000000..c856e21eb
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-293a.a728de01.js differ
diff --git a/priv/static/adminfe/static/js/chunk-293a.a728de01.js.map b/priv/static/adminfe/static/js/chunk-293a.a728de01.js.map
new file mode 100644
index 000000000..03f61abcb
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-293a.a728de01.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-2b8b.e3daf966.js b/priv/static/adminfe/static/js/chunk-2b8b.e3daf966.js
new file mode 100644
index 000000000..4b100db60
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-2b8b.e3daf966.js differ
diff --git a/priv/static/adminfe/static/js/chunk-2b8b.e3daf966.js.map b/priv/static/adminfe/static/js/chunk-2b8b.e3daf966.js.map
new file mode 100644
index 000000000..a7282eaf4
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-2b8b.e3daf966.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-48a4.e3d2c4b6.js b/priv/static/adminfe/static/js/chunk-453a.2fcd7192.js
similarity index 89%
rename from priv/static/adminfe/static/js/chunk-48a4.e3d2c4b6.js
rename to priv/static/adminfe/static/js/chunk-453a.2fcd7192.js
index 596384cab..b0ee1b6b0 100644
Binary files a/priv/static/adminfe/static/js/chunk-48a4.e3d2c4b6.js and b/priv/static/adminfe/static/js/chunk-453a.2fcd7192.js differ
diff --git a/priv/static/adminfe/static/js/chunk-453a.2fcd7192.js.map b/priv/static/adminfe/static/js/chunk-453a.2fcd7192.js.map
new file mode 100644
index 000000000..b43d2f571
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-453a.2fcd7192.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-46cf.104380a9.js b/priv/static/adminfe/static/js/chunk-46cf.104380a9.js
new file mode 100644
index 000000000..9e1e1520b
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-46cf.104380a9.js differ
diff --git a/priv/static/adminfe/static/js/chunk-46cf.104380a9.js.map b/priv/static/adminfe/static/js/chunk-46cf.104380a9.js.map
new file mode 100644
index 000000000..b9357ca8f
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-46cf.104380a9.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-48a4.e3d2c4b6.js.map b/priv/static/adminfe/static/js/chunk-48a4.e3d2c4b6.js.map
deleted file mode 100644
index f3d7fd870..000000000
Binary files a/priv/static/adminfe/static/js/chunk-48a4.e3d2c4b6.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-51b0.7d1554b1.js b/priv/static/adminfe/static/js/chunk-4e46.d257e435.js
similarity index 85%
rename from priv/static/adminfe/static/js/chunk-51b0.7d1554b1.js
rename to priv/static/adminfe/static/js/chunk-4e46.d257e435.js
index a770215ae..39c5dcc4e 100644
Binary files a/priv/static/adminfe/static/js/chunk-51b0.7d1554b1.js and b/priv/static/adminfe/static/js/chunk-4e46.d257e435.js differ
diff --git a/priv/static/adminfe/static/js/chunk-51b0.7d1554b1.js.map b/priv/static/adminfe/static/js/chunk-4e46.d257e435.js.map
similarity index 98%
rename from priv/static/adminfe/static/js/chunk-51b0.7d1554b1.js.map
rename to priv/static/adminfe/static/js/chunk-4e46.d257e435.js.map
index 9b8014486..75d3554ac 100644
Binary files a/priv/static/adminfe/static/js/chunk-51b0.7d1554b1.js.map and b/priv/static/adminfe/static/js/chunk-4e46.d257e435.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-560d.a8bb8682.js b/priv/static/adminfe/static/js/chunk-560d.a8bb8682.js
new file mode 100644
index 000000000..0b03305e9
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-560d.a8bb8682.js differ
diff --git a/priv/static/adminfe/static/js/chunk-560d.a8bb8682.js.map b/priv/static/adminfe/static/js/chunk-560d.a8bb8682.js.map
new file mode 100644
index 000000000..bfab1ade9
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-560d.a8bb8682.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-645e.ad5c2109.js b/priv/static/adminfe/static/js/chunk-645e.ad5c2109.js
deleted file mode 100644
index 1ffacd027..000000000
Binary files a/priv/static/adminfe/static/js/chunk-645e.ad5c2109.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-645e.ad5c2109.js.map b/priv/static/adminfe/static/js/chunk-645e.ad5c2109.js.map
deleted file mode 100644
index 47f9e1213..000000000
Binary files a/priv/static/adminfe/static/js/chunk-645e.ad5c2109.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-6aa3.95b2c0b4.js b/priv/static/adminfe/static/js/chunk-6dd6.6c139a9c.js
similarity index 97%
rename from priv/static/adminfe/static/js/chunk-6aa3.95b2c0b4.js
rename to priv/static/adminfe/static/js/chunk-6dd6.6c139a9c.js
index 1d44bee16..670016168 100644
Binary files a/priv/static/adminfe/static/js/chunk-6aa3.95b2c0b4.js and b/priv/static/adminfe/static/js/chunk-6dd6.6c139a9c.js differ
diff --git a/priv/static/adminfe/static/js/chunk-6aa3.95b2c0b4.js.map b/priv/static/adminfe/static/js/chunk-6dd6.6c139a9c.js.map
similarity index 99%
rename from priv/static/adminfe/static/js/chunk-6aa3.95b2c0b4.js.map
rename to priv/static/adminfe/static/js/chunk-6dd6.6c139a9c.js.map
index aa3fa8ee1..b1438722c 100644
Binary files a/priv/static/adminfe/static/js/chunk-6aa3.95b2c0b4.js.map and b/priv/static/adminfe/static/js/chunk-6dd6.6c139a9c.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-7f8e.a4876ede.js b/priv/static/adminfe/static/js/chunk-7f8e.a4876ede.js
deleted file mode 100644
index e3025b00f..000000000
Binary files a/priv/static/adminfe/static/js/chunk-7f8e.a4876ede.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-7f8e.a4876ede.js.map b/priv/static/adminfe/static/js/chunk-7f8e.a4876ede.js.map
deleted file mode 100644
index c34b1dc46..000000000
Binary files a/priv/static/adminfe/static/js/chunk-7f8e.a4876ede.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-b4ba.f717f3b2.js b/priv/static/adminfe/static/js/chunk-b4ba.f717f3b2.js
deleted file mode 100644
index b748c0739..000000000
Binary files a/priv/static/adminfe/static/js/chunk-b4ba.f717f3b2.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-b4ba.f717f3b2.js.map b/priv/static/adminfe/static/js/chunk-b4ba.f717f3b2.js.map
deleted file mode 100644
index 2871c747d..000000000
Binary files a/priv/static/adminfe/static/js/chunk-b4ba.f717f3b2.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-bb29.6468c7fe.js b/priv/static/adminfe/static/js/chunk-bb29.6468c7fe.js
deleted file mode 100644
index 866a07448..000000000
Binary files a/priv/static/adminfe/static/js/chunk-bb29.6468c7fe.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-bb29.6468c7fe.js.map b/priv/static/adminfe/static/js/chunk-bb29.6468c7fe.js.map
deleted file mode 100644
index 27274eaec..000000000
Binary files a/priv/static/adminfe/static/js/chunk-bb29.6468c7fe.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-cf58.438233c4.js.map b/priv/static/adminfe/static/js/chunk-cf58.438233c4.js.map
deleted file mode 100644
index dd70f756c..000000000
Binary files a/priv/static/adminfe/static/js/chunk-cf58.438233c4.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-cf58.438233c4.js b/priv/static/adminfe/static/js/chunk-cf58.e52693b3.js
similarity index 99%
rename from priv/static/adminfe/static/js/chunk-cf58.438233c4.js
rename to priv/static/adminfe/static/js/chunk-cf58.e52693b3.js
index 5c22b4ba4..b74c20373 100644
Binary files a/priv/static/adminfe/static/js/chunk-cf58.438233c4.js and b/priv/static/adminfe/static/js/chunk-cf58.e52693b3.js differ
diff --git a/priv/static/adminfe/static/js/chunk-cf58.e52693b3.js.map b/priv/static/adminfe/static/js/chunk-cf58.e52693b3.js.map
new file mode 100644
index 000000000..0f3f15299
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-cf58.e52693b3.js.map differ
diff --git a/priv/static/adminfe/static/js/runtime.dfdeb6eb.js b/priv/static/adminfe/static/js/runtime.ae93ea9f.js
similarity index 54%
rename from priv/static/adminfe/static/js/runtime.dfdeb6eb.js
rename to priv/static/adminfe/static/js/runtime.ae93ea9f.js
index 418b2c4de..ebda2acde 100644
Binary files a/priv/static/adminfe/static/js/runtime.dfdeb6eb.js and b/priv/static/adminfe/static/js/runtime.ae93ea9f.js differ
diff --git a/priv/static/adminfe/static/js/runtime.dfdeb6eb.js.map b/priv/static/adminfe/static/js/runtime.ae93ea9f.js.map
similarity index 89%
rename from priv/static/adminfe/static/js/runtime.dfdeb6eb.js.map
rename to priv/static/adminfe/static/js/runtime.ae93ea9f.js.map
index 6728ad670..6392c981a 100644
Binary files a/priv/static/adminfe/static/js/runtime.dfdeb6eb.js.map and b/priv/static/adminfe/static/js/runtime.ae93ea9f.js.map differ
diff --git a/priv/static/index.html b/priv/static/index.html
index 6e1f751ce..557ee5849 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/css/app.ae04505b31bb0ee2765e.css b/priv/static/static/css/app.1055039ce3f2fe4dd110.css
similarity index 90%
rename from priv/static/static/css/app.ae04505b31bb0ee2765e.css
rename to priv/static/static/css/app.1055039ce3f2fe4dd110.css
index 05e3f2087..1867ca81a 100644
Binary files a/priv/static/static/css/app.ae04505b31bb0ee2765e.css and b/priv/static/static/css/app.1055039ce3f2fe4dd110.css differ
diff --git a/priv/static/static/css/app.1055039ce3f2fe4dd110.css.map b/priv/static/static/css/app.1055039ce3f2fe4dd110.css.map
new file mode 100644
index 000000000..861ee8313
--- /dev/null
+++ b/priv/static/static/css/app.1055039ce3f2fe4dd110.css.map
@@ -0,0 +1 @@
+{"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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACxFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.1055039ce3f2fe4dd110.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/app.ae04505b31bb0ee2765e.css.map b/priv/static/static/css/app.ae04505b31bb0ee2765e.css.map
deleted file mode 100644
index 72f33316e..000000000
--- a/priv/static/static/css/app.ae04505b31bb0ee2765e.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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;AClFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.ae04505b31bb0ee2765e.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/base16-3024.css b/priv/static/static/css/base16-3024.css
deleted file mode 100644
index 91859e272..000000000
Binary files a/priv/static/static/css/base16-3024.css and /dev/null differ
diff --git a/priv/static/static/css/base16-apathy.css b/priv/static/static/css/base16-apathy.css
deleted file mode 100644
index 2e99ba1f0..000000000
Binary files a/priv/static/static/css/base16-apathy.css and /dev/null differ
diff --git a/priv/static/static/css/base16-ashes.css b/priv/static/static/css/base16-ashes.css
deleted file mode 100644
index d10e1918e..000000000
Binary files a/priv/static/static/css/base16-ashes.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-cave.css b/priv/static/static/css/base16-atelier-cave.css
deleted file mode 100644
index 5ac17f97e..000000000
Binary files a/priv/static/static/css/base16-atelier-cave.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-dune.css b/priv/static/static/css/base16-atelier-dune.css
deleted file mode 100644
index cfb2d9a1e..000000000
Binary files a/priv/static/static/css/base16-atelier-dune.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-estuary.css b/priv/static/static/css/base16-atelier-estuary.css
deleted file mode 100644
index 76d82c754..000000000
Binary files a/priv/static/static/css/base16-atelier-estuary.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-forest.css b/priv/static/static/css/base16-atelier-forest.css
deleted file mode 100644
index 8108ed8f6..000000000
Binary files a/priv/static/static/css/base16-atelier-forest.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-heath.css b/priv/static/static/css/base16-atelier-heath.css
deleted file mode 100644
index 8858cb807..000000000
Binary files a/priv/static/static/css/base16-atelier-heath.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-lakeside.css b/priv/static/static/css/base16-atelier-lakeside.css
deleted file mode 100644
index 77d44c5fa..000000000
Binary files a/priv/static/static/css/base16-atelier-lakeside.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-plateau.css b/priv/static/static/css/base16-atelier-plateau.css
deleted file mode 100644
index a7445030b..000000000
Binary files a/priv/static/static/css/base16-atelier-plateau.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-savanna.css b/priv/static/static/css/base16-atelier-savanna.css
deleted file mode 100644
index be728d07d..000000000
Binary files a/priv/static/static/css/base16-atelier-savanna.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-seaside.css b/priv/static/static/css/base16-atelier-seaside.css
deleted file mode 100644
index 8b3914669..000000000
Binary files a/priv/static/static/css/base16-atelier-seaside.css and /dev/null differ
diff --git a/priv/static/static/css/base16-atelier-sulphurpool.css b/priv/static/static/css/base16-atelier-sulphurpool.css
deleted file mode 100644
index fb44d6e0f..000000000
Binary files a/priv/static/static/css/base16-atelier-sulphurpool.css and /dev/null differ
diff --git a/priv/static/static/css/base16-bespin.css b/priv/static/static/css/base16-bespin.css
deleted file mode 100644
index 48a9dcf76..000000000
Binary files a/priv/static/static/css/base16-bespin.css and /dev/null differ
diff --git a/priv/static/static/css/base16-brewer.css b/priv/static/static/css/base16-brewer.css
deleted file mode 100644
index c88f219b7..000000000
Binary files a/priv/static/static/css/base16-brewer.css and /dev/null differ
diff --git a/priv/static/static/css/base16-bright.css b/priv/static/static/css/base16-bright.css
deleted file mode 100644
index c2333b8d1..000000000
Binary files a/priv/static/static/css/base16-bright.css and /dev/null differ
diff --git a/priv/static/static/css/base16-chalk.css b/priv/static/static/css/base16-chalk.css
deleted file mode 100644
index e3cb3c20c..000000000
Binary files a/priv/static/static/css/base16-chalk.css and /dev/null differ
diff --git a/priv/static/static/css/base16-codeschool.css b/priv/static/static/css/base16-codeschool.css
deleted file mode 100644
index 00194bbfc..000000000
Binary files a/priv/static/static/css/base16-codeschool.css and /dev/null differ
diff --git a/priv/static/static/css/base16-darktooth.css b/priv/static/static/css/base16-darktooth.css
deleted file mode 100644
index 534487064..000000000
Binary files a/priv/static/static/css/base16-darktooth.css and /dev/null differ
diff --git a/priv/static/static/css/base16-default-dark.css b/priv/static/static/css/base16-default-dark.css
deleted file mode 100644
index 3cd7e860c..000000000
Binary files a/priv/static/static/css/base16-default-dark.css and /dev/null differ
diff --git a/priv/static/static/css/base16-default-light.css b/priv/static/static/css/base16-default-light.css
deleted file mode 100644
index 7e660c302..000000000
Binary files a/priv/static/static/css/base16-default-light.css and /dev/null differ
diff --git a/priv/static/static/css/base16-eighties.css b/priv/static/static/css/base16-eighties.css
deleted file mode 100644
index 8ffcf04d9..000000000
Binary files a/priv/static/static/css/base16-eighties.css and /dev/null differ
diff --git a/priv/static/static/css/base16-embers.css b/priv/static/static/css/base16-embers.css
deleted file mode 100644
index 74e9b7693..000000000
Binary files a/priv/static/static/css/base16-embers.css and /dev/null differ
diff --git a/priv/static/static/css/base16-flat.css b/priv/static/static/css/base16-flat.css
deleted file mode 100644
index 72918a5df..000000000
Binary files a/priv/static/static/css/base16-flat.css and /dev/null differ
diff --git a/priv/static/static/css/base16-github.css b/priv/static/static/css/base16-github.css
deleted file mode 100644
index 080ed34ce..000000000
Binary files a/priv/static/static/css/base16-github.css and /dev/null differ
diff --git a/priv/static/static/css/base16-google-dark.css b/priv/static/static/css/base16-google-dark.css
deleted file mode 100644
index 988eac51d..000000000
Binary files a/priv/static/static/css/base16-google-dark.css and /dev/null differ
diff --git a/priv/static/static/css/base16-google-light.css b/priv/static/static/css/base16-google-light.css
deleted file mode 100644
index 2ee2a6069..000000000
Binary files a/priv/static/static/css/base16-google-light.css and /dev/null differ
diff --git a/priv/static/static/css/base16-grayscale-dark.css b/priv/static/static/css/base16-grayscale-dark.css
deleted file mode 100644
index dc0dd03a0..000000000
Binary files a/priv/static/static/css/base16-grayscale-dark.css and /dev/null differ
diff --git a/priv/static/static/css/base16-grayscale-light.css b/priv/static/static/css/base16-grayscale-light.css
deleted file mode 100644
index f9fd213ae..000000000
Binary files a/priv/static/static/css/base16-grayscale-light.css and /dev/null differ
diff --git a/priv/static/static/css/base16-green-screen.css b/priv/static/static/css/base16-green-screen.css
deleted file mode 100644
index 205efeaec..000000000
Binary files a/priv/static/static/css/base16-green-screen.css and /dev/null differ
diff --git a/priv/static/static/css/base16-harmonic16-dark.css b/priv/static/static/css/base16-harmonic16-dark.css
deleted file mode 100644
index 0c2c7ce42..000000000
Binary files a/priv/static/static/css/base16-harmonic16-dark.css and /dev/null differ
diff --git a/priv/static/static/css/base16-harmonic16-light.css b/priv/static/static/css/base16-harmonic16-light.css
deleted file mode 100644
index 37bb7679a..000000000
Binary files a/priv/static/static/css/base16-harmonic16-light.css and /dev/null differ
diff --git a/priv/static/static/css/base16-hopscotch.css b/priv/static/static/css/base16-hopscotch.css
deleted file mode 100644
index f2ad232c5..000000000
Binary files a/priv/static/static/css/base16-hopscotch.css and /dev/null differ
diff --git a/priv/static/static/css/base16-ir-black.css b/priv/static/static/css/base16-ir-black.css
deleted file mode 100644
index 8d14ab9b8..000000000
Binary files a/priv/static/static/css/base16-ir-black.css and /dev/null differ
diff --git a/priv/static/static/css/base16-isotope.css b/priv/static/static/css/base16-isotope.css
deleted file mode 100644
index f7a4a0b4b..000000000
Binary files a/priv/static/static/css/base16-isotope.css and /dev/null differ
diff --git a/priv/static/static/css/base16-london-tube.css b/priv/static/static/css/base16-london-tube.css
deleted file mode 100644
index 0537d1ad5..000000000
Binary files a/priv/static/static/css/base16-london-tube.css and /dev/null differ
diff --git a/priv/static/static/css/base16-macintosh.css b/priv/static/static/css/base16-macintosh.css
deleted file mode 100644
index d5969fec2..000000000
Binary files a/priv/static/static/css/base16-macintosh.css and /dev/null differ
diff --git a/priv/static/static/css/base16-marrakesh.css b/priv/static/static/css/base16-marrakesh.css
deleted file mode 100644
index 91f0471fc..000000000
Binary files a/priv/static/static/css/base16-marrakesh.css and /dev/null differ
diff --git a/priv/static/static/css/base16-materia.css b/priv/static/static/css/base16-materia.css
deleted file mode 100644
index 41d935dd1..000000000
Binary files a/priv/static/static/css/base16-materia.css and /dev/null differ
diff --git a/priv/static/static/css/base16-mexico-light.css b/priv/static/static/css/base16-mexico-light.css
deleted file mode 100644
index 1916c67bc..000000000
Binary files a/priv/static/static/css/base16-mexico-light.css and /dev/null differ
diff --git a/priv/static/static/css/base16-mocha.css b/priv/static/static/css/base16-mocha.css
deleted file mode 100644
index 6cb2fb580..000000000
Binary files a/priv/static/static/css/base16-mocha.css and /dev/null differ
diff --git a/priv/static/static/css/base16-monokai.css b/priv/static/static/css/base16-monokai.css
deleted file mode 100644
index fc7ccf471..000000000
Binary files a/priv/static/static/css/base16-monokai.css and /dev/null differ
diff --git a/priv/static/static/css/base16-ocean.css b/priv/static/static/css/base16-ocean.css
deleted file mode 100644
index 8622d17e0..000000000
Binary files a/priv/static/static/css/base16-ocean.css and /dev/null differ
diff --git a/priv/static/static/css/base16-oceanicnext.css b/priv/static/static/css/base16-oceanicnext.css
deleted file mode 100644
index df4d9ef5e..000000000
Binary files a/priv/static/static/css/base16-oceanicnext.css and /dev/null differ
diff --git a/priv/static/static/css/base16-paraiso.css b/priv/static/static/css/base16-paraiso.css
deleted file mode 100644
index b68c94071..000000000
Binary files a/priv/static/static/css/base16-paraiso.css and /dev/null differ
diff --git a/priv/static/static/css/base16-phd.css b/priv/static/static/css/base16-phd.css
deleted file mode 100644
index 54276ab11..000000000
Binary files a/priv/static/static/css/base16-phd.css and /dev/null differ
diff --git a/priv/static/static/css/base16-pico.css b/priv/static/static/css/base16-pico.css
deleted file mode 100644
index 86482b72d..000000000
Binary files a/priv/static/static/css/base16-pico.css and /dev/null differ
diff --git a/priv/static/static/css/base16-pleroma-dark.css b/priv/static/static/css/base16-pleroma-dark.css
deleted file mode 100644
index e1d46f927..000000000
Binary files a/priv/static/static/css/base16-pleroma-dark.css and /dev/null differ
diff --git a/priv/static/static/css/base16-pleroma-light.css b/priv/static/static/css/base16-pleroma-light.css
deleted file mode 100644
index 1a85689aa..000000000
Binary files a/priv/static/static/css/base16-pleroma-light.css and /dev/null differ
diff --git a/priv/static/static/css/base16-pop.css b/priv/static/static/css/base16-pop.css
deleted file mode 100644
index 14acac171..000000000
Binary files a/priv/static/static/css/base16-pop.css and /dev/null differ
diff --git a/priv/static/static/css/base16-railscasts.css b/priv/static/static/css/base16-railscasts.css
deleted file mode 100644
index 18f43bfd6..000000000
Binary files a/priv/static/static/css/base16-railscasts.css and /dev/null differ
diff --git a/priv/static/static/css/base16-seti-ui.css b/priv/static/static/css/base16-seti-ui.css
deleted file mode 100644
index bd4f9cc42..000000000
Binary files a/priv/static/static/css/base16-seti-ui.css and /dev/null differ
diff --git a/priv/static/static/css/base16-shapeshifter.css b/priv/static/static/css/base16-shapeshifter.css
deleted file mode 100644
index ded180691..000000000
Binary files a/priv/static/static/css/base16-shapeshifter.css and /dev/null differ
diff --git a/priv/static/static/css/base16-solar-flare.css b/priv/static/static/css/base16-solar-flare.css
deleted file mode 100644
index 7d1d38624..000000000
Binary files a/priv/static/static/css/base16-solar-flare.css and /dev/null differ
diff --git a/priv/static/static/css/base16-solarized-dark.css b/priv/static/static/css/base16-solarized-dark.css
deleted file mode 100644
index ac16f12c9..000000000
Binary files a/priv/static/static/css/base16-solarized-dark.css and /dev/null differ
diff --git a/priv/static/static/css/base16-solarized-light.css b/priv/static/static/css/base16-solarized-light.css
deleted file mode 100644
index 7164cb046..000000000
Binary files a/priv/static/static/css/base16-solarized-light.css and /dev/null differ
diff --git a/priv/static/static/css/base16-spacemacs.css b/priv/static/static/css/base16-spacemacs.css
deleted file mode 100644
index 487376500..000000000
Binary files a/priv/static/static/css/base16-spacemacs.css and /dev/null differ
diff --git a/priv/static/static/css/base16-summerfruit-dark.css b/priv/static/static/css/base16-summerfruit-dark.css
deleted file mode 100644
index 1c8f2332e..000000000
Binary files a/priv/static/static/css/base16-summerfruit-dark.css and /dev/null differ
diff --git a/priv/static/static/css/base16-summerfruit-light.css b/priv/static/static/css/base16-summerfruit-light.css
deleted file mode 100644
index cb54d4c54..000000000
Binary files a/priv/static/static/css/base16-summerfruit-light.css and /dev/null differ
diff --git a/priv/static/static/css/base16-tomorrow-night.css b/priv/static/static/css/base16-tomorrow-night.css
deleted file mode 100644
index 09ecf08ef..000000000
Binary files a/priv/static/static/css/base16-tomorrow-night.css and /dev/null differ
diff --git a/priv/static/static/css/base16-tomorrow.css b/priv/static/static/css/base16-tomorrow.css
deleted file mode 100644
index f14868230..000000000
Binary files a/priv/static/static/css/base16-tomorrow.css and /dev/null differ
diff --git a/priv/static/static/css/base16-twilight.css b/priv/static/static/css/base16-twilight.css
deleted file mode 100644
index c8dfda3f8..000000000
Binary files a/priv/static/static/css/base16-twilight.css and /dev/null differ
diff --git a/priv/static/static/css/base16-unikitty-dark.css b/priv/static/static/css/base16-unikitty-dark.css
deleted file mode 100644
index e6ef32e33..000000000
Binary files a/priv/static/static/css/base16-unikitty-dark.css and /dev/null differ
diff --git a/priv/static/static/css/base16-unikitty-light.css b/priv/static/static/css/base16-unikitty-light.css
deleted file mode 100644
index 7e4c51b7a..000000000
Binary files a/priv/static/static/css/base16-unikitty-light.css and /dev/null differ
diff --git a/priv/static/static/css/themes.json b/priv/static/static/css/themes.json
deleted file mode 100644
index ea8e5a0c4..000000000
--- a/priv/static/static/css/themes.json
+++ /dev/null
@@ -1,66 +0,0 @@
-[
-"base16-pleroma-dark.css",
-"base16-pleroma-light.css",
-"base16-3024.css",
-"base16-apathy.css",
-"base16-ashes.css",
-"base16-atelier-cave.css",
-"base16-atelier-dune.css",
-"base16-atelier-estuary.css",
-"base16-atelier-forest.css",
-"base16-atelier-heath.css",
-"base16-atelier-lakeside.css",
-"base16-atelier-plateau.css",
-"base16-atelier-savanna.css",
-"base16-atelier-seaside.css",
-"base16-atelier-sulphurpool.css",
-"base16-bespin.css",
-"base16-brewer.css",
-"base16-bright.css",
-"base16-chalk.css",
-"base16-codeschool.css",
-"base16-darktooth.css",
-"base16-default-dark.css",
-"base16-default-light.css",
-"base16-eighties.css",
-"base16-embers.css",
-"base16-flat.css",
-"base16-github.css",
-"base16-google-dark.css",
-"base16-google-light.css",
-"base16-grayscale-dark.css",
-"base16-grayscale-light.css",
-"base16-green-screen.css",
-"base16-harmonic16-dark.css",
-"base16-harmonic16-light.css",
-"base16-hopscotch.css",
-"base16-ir-black.css",
-"base16-isotope.css",
-"base16-london-tube.css",
-"base16-macintosh.css",
-"base16-marrakesh.css",
-"base16-materia.css",
-"base16-mexico-light.css",
-"base16-mocha.css",
-"base16-monokai.css",
-"base16-ocean.css",
-"base16-oceanicnext.css",
-"base16-paraiso.css",
-"base16-phd.css",
-"base16-pico.css",
-"base16-pop.css",
-"base16-railscasts.css",
-"base16-seti-ui.css",
-"base16-shapeshifter.css",
-"base16-solar-flare.css",
-"base16-solarized-dark.css",
-"base16-solarized-light.css",
-"base16-spacemacs.css",
-"base16-summerfruit-dark.css",
-"base16-summerfruit-light.css",
-"base16-tomorrow-night.css",
-"base16-tomorrow.css",
-"base16-twilight.css",
-"base16-unikitty-dark.css",
-"base16-unikitty-light.css"
-]
diff --git a/priv/static/static/font/fontello.1581425930672.woff2 b/priv/static/static/font/fontello.1581425930672.woff2
deleted file mode 100644
index 81a10daee..000000000
Binary files a/priv/static/static/font/fontello.1581425930672.woff2 and /dev/null differ
diff --git a/priv/static/static/font/fontello.1581425930672.eot b/priv/static/static/font/fontello.1582927362782.eot
similarity index 98%
rename from priv/static/static/font/fontello.1581425930672.eot
rename to priv/static/static/font/fontello.1582927362782.eot
index 0de06da29..bd848d613 100644
Binary files a/priv/static/static/font/fontello.1581425930672.eot and b/priv/static/static/font/fontello.1582927362782.eot differ
diff --git a/priv/static/static/font/fontello.1581425930672.svg b/priv/static/static/font/fontello.1582927362782.svg
similarity index 100%
rename from priv/static/static/font/fontello.1581425930672.svg
rename to priv/static/static/font/fontello.1582927362782.svg
diff --git a/priv/static/static/font/fontello.1581425930672.ttf b/priv/static/static/font/fontello.1582927362782.ttf
similarity index 99%
rename from priv/static/static/font/fontello.1581425930672.ttf
rename to priv/static/static/font/fontello.1582927362782.ttf
index 5a9cefeec..82691040e 100644
Binary files a/priv/static/static/font/fontello.1581425930672.ttf and b/priv/static/static/font/fontello.1582927362782.ttf differ
diff --git a/priv/static/static/font/fontello.1581425930672.woff b/priv/static/static/font/fontello.1582927362782.woff
similarity index 98%
rename from priv/static/static/font/fontello.1581425930672.woff
rename to priv/static/static/font/fontello.1582927362782.woff
index 28f4dda9d..8c8b4043d 100644
Binary files a/priv/static/static/font/fontello.1581425930672.woff and b/priv/static/static/font/fontello.1582927362782.woff differ
diff --git a/priv/static/static/font/fontello.1582927362782.woff2 b/priv/static/static/font/fontello.1582927362782.woff2
new file mode 100644
index 000000000..147707460
Binary files /dev/null and b/priv/static/static/font/fontello.1582927362782.woff2 differ
diff --git a/priv/static/static/fontello.1582927362782.css b/priv/static/static/fontello.1582927362782.css
new file mode 100644
index 000000000..1889379ff
Binary files /dev/null and b/priv/static/static/fontello.1582927362782.css differ
diff --git a/priv/static/static/js/2.9be9f9ec29f7536c73c3.js b/priv/static/static/js/2.9be9f9ec29f7536c73c3.js
deleted file mode 100644
index d464dbf74..000000000
Binary files a/priv/static/static/js/2.9be9f9ec29f7536c73c3.js and /dev/null differ
diff --git a/priv/static/static/js/2.f158cbd2b8770e467dfe.js b/priv/static/static/js/2.f158cbd2b8770e467dfe.js
new file mode 100644
index 000000000..24f80fe7b
Binary files /dev/null and b/priv/static/static/js/2.f158cbd2b8770e467dfe.js differ
diff --git a/priv/static/static/js/2.9be9f9ec29f7536c73c3.js.map b/priv/static/static/js/2.f158cbd2b8770e467dfe.js.map
similarity index 50%
rename from priv/static/static/js/2.9be9f9ec29f7536c73c3.js.map
rename to priv/static/static/js/2.f158cbd2b8770e467dfe.js.map
index 21efd7ec8..94ca6f090 100644
Binary files a/priv/static/static/js/2.9be9f9ec29f7536c73c3.js.map and b/priv/static/static/js/2.f158cbd2b8770e467dfe.js.map differ
diff --git a/priv/static/static/js/app.128bd8b808a3b5b6da6b.js b/priv/static/static/js/app.128bd8b808a3b5b6da6b.js
new file mode 100644
index 000000000..8d36b0c36
Binary files /dev/null and b/priv/static/static/js/app.128bd8b808a3b5b6da6b.js differ
diff --git a/priv/static/static/js/app.128bd8b808a3b5b6da6b.js.map b/priv/static/static/js/app.128bd8b808a3b5b6da6b.js.map
new file mode 100644
index 000000000..e53b74277
Binary files /dev/null and b/priv/static/static/js/app.128bd8b808a3b5b6da6b.js.map differ
diff --git a/priv/static/static/js/app.f8af8a9b83e330e80903.js b/priv/static/static/js/app.f8af8a9b83e330e80903.js
deleted file mode 100644
index f755c141a..000000000
Binary files a/priv/static/static/js/app.f8af8a9b83e330e80903.js and /dev/null differ
diff --git a/priv/static/static/js/app.f8af8a9b83e330e80903.js.map b/priv/static/static/js/app.f8af8a9b83e330e80903.js.map
deleted file mode 100644
index 106368819..000000000
Binary files a/priv/static/static/js/app.f8af8a9b83e330e80903.js.map and /dev/null differ
diff --git a/priv/static/static/js/vendors~app.52ac194cbc427f97f06e.js b/priv/static/static/js/vendors~app.52ac194cbc427f97f06e.js
deleted file mode 100644
index f59457df6..000000000
Binary files a/priv/static/static/js/vendors~app.52ac194cbc427f97f06e.js and /dev/null differ
diff --git a/priv/static/static/js/vendors~app.52ac194cbc427f97f06e.js.map b/priv/static/static/js/vendors~app.52ac194cbc427f97f06e.js.map
deleted file mode 100644
index b0ccd82fb..000000000
Binary files a/priv/static/static/js/vendors~app.52ac194cbc427f97f06e.js.map and /dev/null differ
diff --git a/priv/static/static/js/vendors~app.c5bbd3734647f0cc7eef.js b/priv/static/static/js/vendors~app.c5bbd3734647f0cc7eef.js
new file mode 100644
index 000000000..8964180cd
Binary files /dev/null and b/priv/static/static/js/vendors~app.c5bbd3734647f0cc7eef.js differ
diff --git a/priv/static/static/js/vendors~app.c5bbd3734647f0cc7eef.js.map b/priv/static/static/js/vendors~app.c5bbd3734647f0cc7eef.js.map
new file mode 100644
index 000000000..fab720d23
Binary files /dev/null and b/priv/static/static/js/vendors~app.c5bbd3734647f0cc7eef.js.map differ
diff --git a/priv/static/static/styles.json b/priv/static/static/styles.json
index 23508970d..23f57c65e 100644
--- a/priv/static/static/styles.json
+++ b/priv/static/static/styles.json
@@ -1,6 +1,6 @@
{
- "pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
- "pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "pleroma-dark": "/static/themes/pleroma-dark.json",
+ "pleroma-light": "/static/themes/pleroma-light.json",
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
@@ -12,5 +12,6 @@
"redmond-xxi": "/static/themes/redmond-xxi.json",
"breezy-dark": "/static/themes/breezy-dark.json",
"breezy-light": "/static/themes/breezy-light.json",
- "mammal": "/static/themes/mammal.json"
+ "mammal": "/static/themes/mammal.json",
+ "paper": "/static/themes/paper.json"
}
diff --git a/priv/static/static/terms-of-service.html b/priv/static/static/terms-of-service.html
index c02cb7198..a6da539e4 100644
--- a/priv/static/static/terms-of-service.html
+++ b/priv/static/static/terms-of-service.html
@@ -1,7 +1,4 @@
Terms of Service
-This is a placeholder ToS.
-
-Edit "/static/terms-of-service.html"
to make it fit the needs of your instance.
-
-
+This is a placeholder ToS. Edit "/static/terms-of-service.html"
to make it fit the needs of your instance.
+
diff --git a/priv/static/static/themes/breezy-dark.json b/priv/static/static/themes/breezy-dark.json
index 6119bf887..76b962c56 100644
--- a/priv/static/static/themes/breezy-dark.json
+++ b/priv/static/static/themes/breezy-dark.json
@@ -1,7 +1,9 @@
{
"_pleroma_theme_version": 2,
"name": "Breezy Dark (beta)",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
"shadows": {
"panel": [
{
@@ -19,7 +21,7 @@
"y": "0",
"blur": "0",
"spread": "1",
- "color": "#ffffff",
+ "color": "--btn,900",
"alpha": "0.15",
"inset": true
},
@@ -40,7 +42,7 @@
"blur": "40",
"spread": "-40",
"inset": true,
- "color": "#ffffff",
+ "color": "--panel,900",
"alpha": "0.1"
}
],
@@ -50,8 +52,8 @@
"y": "0",
"blur": 0,
"spread": "1",
- "color": "--link",
- "alpha": "0.3",
+ "color": "--accent",
+ "alpha": "1",
"inset": true
},
{
@@ -65,21 +67,12 @@
}
],
"buttonPressed": [
- {
- "x": 0,
- "y": 0,
- "blur": "0",
- "spread": "50",
- "color": "--faint",
- "alpha": 1,
- "inset": true
- },
{
"x": 0,
"y": "0",
"blur": 0,
"spread": "1",
- "color": "#ffffff",
+ "color": "--btn,900",
"alpha": 0.2,
"inset": true
},
@@ -99,31 +92,30 @@
"y": "0",
"blur": 0,
"spread": "1",
- "color": "#FFFFFF",
+ "color": "--input,900",
"alpha": "0.2",
"inset": true
}
]
},
- "fonts": {},
- "opacity": {
- "input": "1",
- "panel": "0"
- },
+ "opacity": {},
"colors": {
"bg": "#31363b",
"text": "#eff0f1",
"link": "#3daee9",
"fg": "#31363b",
- "panel": "#31363b",
- "input": "#232629",
- "topBarLink": "#eff0f1",
- "btn": "#31363b",
+ "panel": "transparent",
+ "input": "--bg,-6.47",
+ "topBarLink": "--topBarText",
+ "btn": "--bg",
"border": "#4c545b",
"cRed": "#da4453",
"cBlue": "#3daee9",
"cGreen": "#27ae60",
- "cOrange": "#f67400"
+ "cOrange": "#f67400",
+ "btnPressed": "--accent",
+ "selectedMenu": "--accent",
+ "selectedMenuPopover": "--accent"
},
"radii": {
"btn": "2",
diff --git a/priv/static/static/themes/breezy-light.json b/priv/static/static/themes/breezy-light.json
index becf704fc..0968fff0c 100644
--- a/priv/static/static/themes/breezy-light.json
+++ b/priv/static/static/themes/breezy-light.json
@@ -1,7 +1,9 @@
{
"_pleroma_theme_version": 2,
"name": "Breezy Light (beta)",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
"shadows": {
"panel": [
{
@@ -19,7 +21,7 @@
"y": "0",
"blur": "0",
"spread": "1",
- "color": "#000000",
+ "color": "--btn,900",
"alpha": "0.3",
"inset": true
},
@@ -40,7 +42,7 @@
"blur": "40",
"spread": "-40",
"inset": true,
- "color": "#ffffff",
+ "color": "--panel,900",
"alpha": "0.1"
}
],
@@ -50,8 +52,8 @@
"y": "0",
"blur": 0,
"spread": "1",
- "color": "--link",
- "alpha": "0.3",
+ "color": "--accent",
+ "alpha": "1",
"inset": true
},
{
@@ -65,21 +67,12 @@
}
],
"buttonPressed": [
- {
- "x": 0,
- "y": 0,
- "blur": "0",
- "spread": "50",
- "color": "--faint",
- "alpha": 1,
- "inset": true
- },
{
"x": 0,
"y": "0",
"blur": 0,
"spread": "1",
- "color": "#ffffff",
+ "color": "--btn,900",
"alpha": 0.2,
"inset": true
},
@@ -99,31 +92,30 @@
"y": "0",
"blur": 0,
"spread": "1",
- "color": "#000000",
+ "color": "--input,900",
"alpha": "0.2",
"inset": true
}
]
},
- "fonts": {},
"opacity": {
"input": "1"
},
"colors": {
"bg": "#eff0f1",
"text": "#232627",
- "link": "#2980b9",
- "fg": "#bcc2c7",
- "panel": "#475057",
- "panelText": "#fcfcfc",
- "input": "#fcfcfc",
- "topBar": "#475057",
- "topBarLink": "#eff0f1",
- "btn": "#eff0f1",
+ "fg": "#475057",
+ "accent": "#2980b9",
+ "input": "--bg,-6.47",
+ "topBarLink": "--topBarText",
+ "btn": "--bg",
"cRed": "#da4453",
"cBlue": "#2980b9",
"cGreen": "#27ae60",
- "cOrange": "#f67400"
+ "cOrange": "#f67400",
+ "btnPressed": "--accent",
+ "selectedMenu": "--accent",
+ "selectedMenuPopover": "--accent"
},
"radii": {
"btn": "2",
diff --git a/priv/static/static/themes/paper.json b/priv/static/static/themes/paper.json
new file mode 100644
index 000000000..a3b90a0a1
--- /dev/null
+++ b/priv/static/static/themes/paper.json
@@ -0,0 +1,172 @@
+{
+ "_pleroma_theme_version": 2,
+ "name": "Paper",
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
+ "shadows": {
+ "panel": [
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "9",
+ "spread": 0,
+ "inset": false,
+ "color": "#668bb2",
+ "alpha": "0.1"
+ },
+ {
+ "x": "0",
+ "y": "1",
+ "blur": "2",
+ "spread": "-1",
+ "inset": false,
+ "color": "#668bb2",
+ "alpha": "0.1"
+ }
+ ],
+ "topBar": [
+ {
+ "x": 0,
+ "y": "3",
+ "blur": "8",
+ "spread": 0,
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ },
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "4",
+ "spread": 0,
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ }
+ ],
+ "button": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": 0,
+ "color": "#463f78",
+ "alpha": "0.1",
+ "inset": false
+ }
+ ],
+ "input": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "2",
+ "spread": 0,
+ "inset": true,
+ "color": "#6277b7",
+ "alpha": "0.1"
+ }
+ ],
+ "buttonHover": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": 0,
+ "color": "#494949",
+ "alpha": "0.1"
+ },
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "0",
+ "spread": "20",
+ "color": "#ffffff",
+ "alpha": "1",
+ "inset": true
+ }
+ ],
+ "buttonPressed": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "4",
+ "spread": "0",
+ "color": "#494949",
+ "alpha": "0.8",
+ "inset": false
+ }
+ ],
+ "avatarStatus": [
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "4",
+ "spread": "0",
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ }
+ ],
+ "avatar": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": "0",
+ "color": "#3e618e",
+ "alpha": "0.9"
+ }
+ ],
+ "popup": [
+ {
+ "x": "0",
+ "y": "3",
+ "blur": "11",
+ "spread": 0,
+ "color": "#668bb2",
+ "alpha": "0.2"
+ },
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "3",
+ "spread": "-1",
+ "color": "#668bb2",
+ "alpha": "0.2"
+ }
+ ]
+ },
+ "opacity": {
+ "underlay": "1",
+ "border": "0"
+ },
+ "colors": {
+ "bg": "#ffffff",
+ "fg": "#f6f6f6",
+ "text": "#494949",
+ "underlay": "#ffffff",
+ "link": "#788ca1",
+ "accent": "#97a0aa",
+ "cBlue": "#788ca1",
+ "cRed": "#eed7ce",
+ "cGreen": "#788ca1",
+ "cOrange": "#788ca1",
+ "postLink": "#788ca1",
+ "border": "#ffffff",
+ "icon": "#b6c9c4",
+ "panel": "#ffffff",
+ "topBarText": "#4b4b4b"
+ },
+ "radii": {
+ "btn": "0",
+ "input": "0",
+ "checkbox": "0",
+ "panel": "0",
+ "avatar": "2",
+ "avatarAlt": "2",
+ "tooltip": "0",
+ "attachment": "0"
+ }
+ }
+}
diff --git a/priv/static/static/themes/pleroma-dark.json b/priv/static/static/themes/pleroma-dark.json
new file mode 100644
index 000000000..2703fba11
--- /dev/null
+++ b/priv/static/static/themes/pleroma-dark.json
@@ -0,0 +1,191 @@
+{
+ "_pleroma_theme_version": 2,
+ "name": "Pleroma Dark",
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
+ "shadows": {
+ "buttonHover": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "1",
+ "spread": "2",
+ "color": "#b9b9ba",
+ "alpha": "0.4",
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "buttonPressed": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": 4,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 1,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "2",
+ "spread": 0,
+ "inset": false,
+ "color": "#000000",
+ "alpha": 1
+ }
+ ],
+ "panelHeader": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "3",
+ "spread": 0,
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.4"
+ },
+ {
+ "x": "0",
+ "y": "1",
+ "blur": "0",
+ "spread": 0,
+ "inset": true,
+ "color": "#ffffff",
+ "alpha": "0.2"
+ }
+ ],
+ "panel": [
+ {
+ "x": "0",
+ "y": "0",
+ "blur": "3",
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.5"
+ },
+ {
+ "x": "0",
+ "y": "4",
+ "blur": "6",
+ "spread": "3",
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.3"
+ }
+ ],
+ "button": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": 2,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 1
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "topBar": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": 4,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.4"
+ },
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "7",
+ "spread": 0,
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.3"
+ }
+ ]
+ },
+ "opacity": {
+ "underlay": "0.6"
+ },
+ "colors": {
+ "bg": "#0f161e",
+ "fg": "#151e2b",
+ "text": "#b9b9ba",
+ "underlay": "#090e14",
+ "accent": "#e2b188",
+ "cBlue": "#81beea",
+ "cRed": "#d31014",
+ "cGreen": "#5dc94a",
+ "cOrange": "#ffc459",
+ "border": "--fg,3",
+ "topBarText": "--text,-9.75",
+ "topBarLink": "--topBarText",
+ "btnToggled": "--accent,-24.2",
+ "alertErrorText": "--text,21.2",
+ "badgeNotification": "#e15932",
+ "badgeNotificationText": "#ffffff"
+ },
+ "radii": {
+ "btn": "3",
+ "input": "3",
+ "panel": "3",
+ "avatar": "3",
+ "attachment": "3"
+ }
+ }
+}
diff --git a/priv/static/static/themes/pleroma-light.json b/priv/static/static/themes/pleroma-light.json
new file mode 100644
index 000000000..05fc300aa
--- /dev/null
+++ b/priv/static/static/themes/pleroma-light.json
@@ -0,0 +1,219 @@
+{
+ "_pleroma_theme_version": 2,
+ "name": "Pleroma Light",
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
+ "shadows": {
+ "button": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": 2,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.2"
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": "0.5",
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "buttonHover": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "2",
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.2"
+ },
+ {
+ "x": 0,
+ "y": "0",
+ "blur": "1",
+ "spread": "2",
+ "color": "#ffc39f",
+ "alpha": "1",
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "input": [
+ {
+ "x": 0,
+ "y": 1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "2",
+ "inset": true,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.15"
+ }
+ ],
+ "panel": [
+ {
+ "x": "0",
+ "y": 1,
+ "blur": "3",
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.5"
+ },
+ {
+ "x": "0",
+ "y": "3",
+ "blur": "6",
+ "spread": "1",
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.2"
+ }
+ ],
+ "panelHeader": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": 0,
+ "spread": 0,
+ "inset": true,
+ "color": "#ffffff",
+ "alpha": "0.5"
+ },
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "3",
+ "spread": 0,
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.3"
+ }
+ ],
+ "buttonPressed": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": 4,
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.2"
+ },
+ {
+ "x": 0,
+ "y": 1,
+ "blur": "1",
+ "spread": "2",
+ "color": "#000000",
+ "alpha": "0.3",
+ "inset": true
+ },
+ {
+ "x": 0,
+ "y": -1,
+ "blur": 0,
+ "spread": 0,
+ "color": "#FFFFFF",
+ "alpha": 0.2,
+ "inset": true
+ }
+ ],
+ "popup": [
+ {
+ "x": "1",
+ "y": "2",
+ "blur": "2",
+ "spread": 0,
+ "color": "#000000",
+ "alpha": "0.2"
+ },
+ {
+ "x": "1",
+ "y": "3",
+ "blur": "7",
+ "spread": "0",
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.2"
+ }
+ ],
+ "avatarStatus": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "4",
+ "spread": "0",
+ "inset": false,
+ "color": "#000000",
+ "alpha": "0.2"
+ }
+ ]
+ },
+ "opacity": {
+ "underlay": "0.4"
+ },
+ "colors": {
+ "bg": "#f2f6f9",
+ "fg": "#d6dfed",
+ "text": "#304055",
+ "underlay": "#5d6086",
+ "accent": "#f55b1b",
+ "cBlue": "#0095ff",
+ "cRed": "#d31014",
+ "cGreen": "#0fa00f",
+ "cOrange": "#ffa500",
+ "border": "#d8e6f9",
+ "topBarText": "#304055",
+ "topBarLink": "--topBarText",
+ "btnToggled": "--accent,-24.2",
+ "input": "#dee3ed",
+ "badgeNotification": "#e83802"
+ },
+ "radii": {
+ "btn": "3",
+ "input": "3",
+ "panel": "3",
+ "avatar": "3",
+ "attachment": "3"
+ }
+ }
+}
diff --git a/priv/static/static/themes/redmond-xx-se.json b/priv/static/static/themes/redmond-xx-se.json
index 70ee89d1b..7a4a29da3 100644
--- a/priv/static/static/themes/redmond-xx-se.json
+++ b/priv/static/static/themes/redmond-xx-se.json
@@ -1,7 +1,8 @@
{
"_pleroma_theme_version": 2,
"name": "Redmond XX SE",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
"shadows": {
"panel": [
{
@@ -268,6 +269,7 @@
"bg": "#c0c0c0",
"text": "#000000",
"link": "#0000ff",
+ "accent": "#000080",
"fg": "#c0c0c0",
"panel": "#000080",
"panelFaint": "#c0c0c0",
@@ -275,13 +277,16 @@
"topBar": "#000080",
"topBarLink": "#ffffff",
"btn": "#c0c0c0",
+ "btnToggled": "--btn",
"faint": "#3f3f3f",
"faintLink": "#404080",
"border": "#808080",
"cRed": "#FF0000",
"cBlue": "#008080",
"cGreen": "#008000",
- "cOrange": "#808000"
+ "cOrange": "#808000",
+ "highlight": "--accent",
+ "selectedPost": "--bg,-10"
},
"radii": {
"btn": "0",
diff --git a/priv/static/static/themes/redmond-xx.json b/priv/static/static/themes/redmond-xx.json
index 4fd6a3691..ff95b1e06 100644
--- a/priv/static/static/themes/redmond-xx.json
+++ b/priv/static/static/themes/redmond-xx.json
@@ -1,7 +1,8 @@
{
"_pleroma_theme_version": 2,
"name": "Redmond XX",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
"shadows": {
"panel": [
{
@@ -259,6 +260,7 @@
"bg": "#c0c0c0",
"text": "#000000",
"link": "#0000ff",
+ "accent": "#000080",
"fg": "#c0c0c0",
"panel": "#000080",
"panelFaint": "#c0c0c0",
@@ -266,13 +268,16 @@
"topBar": "#000080",
"topBarLink": "#ffffff",
"btn": "#c0c0c0",
+ "btnToggled": "--btn",
"faint": "#3f3f3f",
"faintLink": "#404080",
"border": "#808080",
"cRed": "#FF0000",
"cBlue": "#008080",
"cGreen": "#008000",
- "cOrange": "#808000"
+ "cOrange": "#808000",
+ "highlight": "--accent",
+ "selectedPost": "--bg,-10"
},
"radii": {
"btn": "0",
diff --git a/priv/static/static/themes/redmond-xxi.json b/priv/static/static/themes/redmond-xxi.json
index d10bf138f..f788bdb83 100644
--- a/priv/static/static/themes/redmond-xxi.json
+++ b/priv/static/static/themes/redmond-xxi.json
@@ -1,7 +1,8 @@
{
"_pleroma_theme_version": 2,
"name": "Redmond XXI",
- "theme": {
+ "source": {
+ "themeEngineVersion": 3,
"shadows": {
"panel": [
{
@@ -241,6 +242,7 @@
"bg": "#d6d6ce",
"text": "#000000",
"link": "#0000ff",
+ "accent": "#0a246a",
"fg": "#d6d6ce",
"panel": "#042967",
"panelFaint": "#FFFFFF",
@@ -248,13 +250,16 @@
"topBar": "#042967",
"topBarLink": "#ffffff",
"btn": "#d6d6ce",
+ "btnToggled": "--btn",
"faint": "#3f3f3f",
"faintLink": "#404080",
"border": "#808080",
"cRed": "#c42726",
"cBlue": "#6699cc",
"cGreen": "#669966",
- "cOrange": "#cc6633"
+ "cOrange": "#cc6633",
+ "highlight": "--accent",
+ "selectedPost": "--bg,-10"
},
"radii": {
"btn": "0",
diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js
index 42ef52ad4..87455aa6a 100644
Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ
diff --git a/test/captcha_test.exs b/test/captcha_test.exs
index 393c8219e..5e29b48b0 100644
--- a/test/captcha_test.exs
+++ b/test/captcha_test.exs
@@ -1,17 +1,20 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors
+# Copyright © 2017-2020 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.CaptchaTest do
- use ExUnit.Case
+ use Pleroma.DataCase
import Tesla.Mock
+ alias Pleroma.Captcha
alias Pleroma.Captcha.Kocaptcha
alias Pleroma.Captcha.Native
@ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
+ clear_config([Pleroma.Captcha, :enabled])
+
describe "Kocaptcha" do
setup do
ets_name = Kocaptcha.Ets
@@ -31,17 +34,18 @@ defmodule Pleroma.CaptchaTest do
test "new and validate" do
new = Kocaptcha.new()
- assert new[:type] == :kocaptcha
- assert new[:token] == "afa1815e14e29355e6c8f6b143a39fa2"
- assert new[:url] ==
- "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"
+ token = "afa1815e14e29355e6c8f6b143a39fa2"
+ url = "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"
- assert Kocaptcha.validate(
- new[:token],
- "7oEy8c",
- new[:answer_data]
- ) == :ok
+ assert %{
+ answer_data: answer,
+ token: ^token,
+ url: ^url,
+ type: :kocaptcha
+ } = new
+
+ assert Kocaptcha.validate(token, "7oEy8c", answer) == :ok
end
end
@@ -61,4 +65,52 @@ test "new and validate" do
assert {:error, "Invalid CAPTCHA"} == Native.validate(token, answer, answer <> "foobar")
end
end
+
+ describe "Captcha Wrapper" do
+ test "validate" do
+ Pleroma.Config.put([Pleroma.Captcha, :enabled], true)
+
+ new = Captcha.new()
+
+ assert %{
+ answer_data: answer,
+ token: token
+ } = new
+
+ assert is_binary(answer)
+ assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer)
+ end
+
+ test "doesn't validate invalid answer" do
+ Pleroma.Config.put([Pleroma.Captcha, :enabled], true)
+
+ new = Captcha.new()
+
+ assert %{
+ answer_data: answer,
+ token: token
+ } = new
+
+ assert is_binary(answer)
+
+ assert {:error, "Invalid answer data"} =
+ Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar")
+ end
+
+ test "nil answer_data" do
+ Pleroma.Config.put([Pleroma.Captcha, :enabled], true)
+
+ new = Captcha.new()
+
+ assert %{
+ answer_data: answer,
+ token: token
+ } = new
+
+ assert is_binary(answer)
+
+ assert {:error, "Invalid answer data"} =
+ Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil)
+ end
+ end
end
diff --git a/test/fixtures/mastodon-post-activity.json b/test/fixtures/mastodon-post-activity.json
index b91263431..5c3d22722 100644
--- a/test/fixtures/mastodon-post-activity.json
+++ b/test/fixtures/mastodon-post-activity.json
@@ -35,6 +35,19 @@
"inReplyTo": null,
"inReplyToAtomUri": null,
"published": "2018-02-12T14:08:20Z",
+ "replies": {
+ "id": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true",
+ "partOf": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies",
+ "items": [
+ "http://mastodon.example.org/users/admin/statuses/99512778738411823",
+ "http://mastodon.example.org/users/admin/statuses/99512778738411824"
+ ]
+ }
+ },
"sensitive": true,
"summary": "cw",
"tag": [
diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs
index 2aad7a588..3afd35648 100644
--- a/test/object/fetcher_test.exs
+++ b/test/object/fetcher_test.exs
@@ -26,6 +26,31 @@ defmodule Pleroma.Object.FetcherTest do
:ok
end
+ describe "max thread distance restriction" do
+ @ap_id "http://mastodon.example.org/@admin/99541947525187367"
+
+ clear_config([:instance, :federation_incoming_replies_max_depth])
+
+ test "it returns thread depth exceeded error if thread depth is exceeded" do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+ assert {:error, "Max thread distance exceeded."} =
+ Fetcher.fetch_object_from_id(@ap_id, depth: 1)
+ end
+
+ test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+ assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id)
+ end
+
+ test "it fetches object if requested depth does not exceed max thread depth" do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10)
+
+ assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10)
+ end
+ end
+
describe "actor origin containment" do
test "it rejects objects with a bogus origin" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
diff --git a/test/stat_test.exs b/test/stat_test.exs
new file mode 100644
index 000000000..1f0c6199a
--- /dev/null
+++ b/test/stat_test.exs
@@ -0,0 +1,70 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.StateTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+ alias Pleroma.Web.CommonAPI
+
+ describe "status visibility count" do
+ test "on new status" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
+
+ Enum.each(0..1, fn _ ->
+ CommonAPI.post(user, %{
+ "visibility" => "unlisted",
+ "status" => "hey"
+ })
+ end)
+
+ Enum.each(0..2, fn _ ->
+ CommonAPI.post(user, %{
+ "visibility" => "direct",
+ "status" => "hey @#{other_user.nickname}"
+ })
+ end)
+
+ Enum.each(0..3, fn _ ->
+ CommonAPI.post(user, %{
+ "visibility" => "private",
+ "status" => "hey"
+ })
+ end)
+
+ assert %{direct: 3, private: 4, public: 1, unlisted: 2} =
+ Pleroma.Stats.get_status_visibility_count()
+ end
+
+ test "on status delete" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
+ assert %{public: 1} = Pleroma.Stats.get_status_visibility_count()
+ CommonAPI.delete(activity.id, user)
+ assert %{public: 0} = Pleroma.Stats.get_status_visibility_count()
+ end
+
+ test "on status visibility update" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
+ assert %{public: 1, private: 0} = Pleroma.Stats.get_status_visibility_count()
+ {:ok, _} = CommonAPI.update_activity_scope(activity.id, %{"visibility" => "private"})
+ assert %{public: 0, private: 1} = Pleroma.Stats.get_status_visibility_count()
+ end
+
+ test "doesn't count unrelated activities" do
+ user = insert(:user)
+ other_user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
+ _ = CommonAPI.follow(user, other_user)
+ CommonAPI.favorite(activity.id, other_user)
+ CommonAPI.repeat(activity.id, other_user)
+
+ assert %{direct: 0, private: 0, public: 1, unlisted: 0} =
+ Pleroma.Stats.get_status_visibility_count()
+ end
+ end
+end
diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex
index 65ca6b3bd..6dae94edf 100644
--- a/test/support/captcha_mock.ex
+++ b/test/support/captcha_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors
+# Copyright © 2017-2020 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Mock do
@@ -7,8 +7,17 @@ defmodule Pleroma.Captcha.Mock do
@behaviour Service
@impl Service
- def new, do: %{type: :mock}
+ def new,
+ do: %{
+ type: :mock,
+ token: "afa1815e14e29355e6c8f6b143a39fa2",
+ answer_data: "63615261b77f5354fb8c4e4986477555",
+ url: "https://example.org/captcha.png"
+ }
@impl Service
- def validate(_token, _captcha, _data), do: :ok
+ def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
+
+ def validate(_token, captcha, answer),
+ do: {:error, "Invalid CAPTCHA captcha: #{inspect(captcha)} ; answer: #{inspect(answer)}"}
end
diff --git a/test/support/oban_helpers.ex b/test/support/oban_helpers.ex
index 72792c064..0e3b654df 100644
--- a/test/support/oban_helpers.ex
+++ b/test/support/oban_helpers.ex
@@ -9,6 +9,10 @@ defmodule Pleroma.Tests.ObanHelpers do
alias Pleroma.Repo
+ def wipe_all do
+ Repo.delete_all(Oban.Job)
+ end
+
def perform_all do
Oban.Job
|> Repo.all()
diff --git a/test/tasks/instance_test.exs b/test/tasks/instance_test.exs
index d69275726..a0cc5d7c7 100644
--- a/test/tasks/instance_test.exs
+++ b/test/tasks/instance_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.InstanceTest do
- use ExUnit.Case, async: true
+ use ExUnit.Case
setup do
File.mkdir_p!(tmp_path())
@@ -15,6 +15,8 @@ defmodule Pleroma.InstanceTest do
if File.exists?(static_dir) do
File.rm_rf(Path.join(static_dir, "robots.txt"))
end
+
+ Pleroma.Config.put([:instance, :static_dir], static_dir)
end)
:ok
diff --git a/test/tasks/refresh_counter_cache_test.exs b/test/tasks/refresh_counter_cache_test.exs
new file mode 100644
index 000000000..47367af94
--- /dev/null
+++ b/test/tasks/refresh_counter_cache_test.exs
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.RefreshCounterCacheTest do
+ use Pleroma.DataCase
+ alias Pleroma.Web.CommonAPI
+ import ExUnit.CaptureIO, only: [capture_io: 1]
+ import Pleroma.Factory
+
+ test "counts statuses" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
+
+ Enum.each(0..1, fn _ ->
+ CommonAPI.post(user, %{
+ "visibility" => "unlisted",
+ "status" => "hey"
+ })
+ end)
+
+ Enum.each(0..2, fn _ ->
+ CommonAPI.post(user, %{
+ "visibility" => "direct",
+ "status" => "hey @#{other_user.nickname}"
+ })
+ end)
+
+ Enum.each(0..3, fn _ ->
+ CommonAPI.post(user, %{
+ "visibility" => "private",
+ "status" => "hey"
+ })
+ end)
+
+ assert capture_io(fn -> Mix.Tasks.Pleroma.RefreshCounterCache.run([]) end) =~ "Done\n"
+
+ assert %{direct: 3, private: 4, public: 1, unlisted: 2} =
+ Pleroma.Stats.get_status_visibility_count()
+ end
+end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 1b12ee3a9..937f78cbe 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -3,7 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
+ use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
+
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
@@ -40,7 +42,7 @@ test "it ignores an incoming notice if we already have it" do
end
@tag capture_log: true
- test "it fetches replied-to activities if we don't have them" do
+ test "it fetches reply-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
@@ -61,7 +63,7 @@ test "it fetches replied-to activities if we don't have them" do
assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
end
- test "it does not fetch replied-to activities beyond max_replies_depth" do
+ test "it does not fetch reply-to activities beyond max replies depth limit" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
@@ -73,7 +75,7 @@ test "it does not fetch replied-to activities beyond max_replies_depth" do
data = Map.put(data, "object", object)
with_mock Pleroma.Web.Federator,
- allowed_incoming_reply_depth?: fn _ -> false end do
+ allowed_thread_distance?: fn _ -> false end do
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
returned_object = Object.normalize(returned_activity, false)
@@ -1348,6 +1350,101 @@ test "it accepts Move activities" do
end
end
+ describe "`handle_incoming/2`, Mastodon format `replies` handling" do
+ clear_config([:activitypub, :note_replies_output_limit]) do
+ Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
+ end
+
+ clear_config([:instance, :federation_incoming_replies_max_depth])
+
+ setup do
+ data =
+ "test/fixtures/mastodon-post-activity.json"
+ |> File.read!()
+ |> Poison.decode!()
+
+ items = get_in(data, ["object", "replies", "first", "items"])
+ assert length(items) > 0
+
+ %{data: data, items: items}
+ end
+
+ test "schedules background fetching of `replies` items if max thread depth limit allows", %{
+ data: data,
+ items: items
+ } do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10)
+
+ {:ok, _activity} = Transmogrifier.handle_incoming(data)
+
+ for id <- items do
+ job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
+ assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
+ end
+ end
+
+ test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
+ %{data: data} do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+ {:ok, _activity} = Transmogrifier.handle_incoming(data)
+
+ assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
+ end
+ end
+
+ describe "`handle_incoming/2`, Pleroma format `replies` handling" do
+ clear_config([:activitypub, :note_replies_output_limit]) do
+ Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
+ end
+
+ clear_config([:instance, :federation_incoming_replies_max_depth])
+
+ setup do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "post1"})
+
+ {:ok, reply1} =
+ CommonAPI.post(user, %{"status" => "reply1", "in_reply_to_status_id" => activity.id})
+
+ {:ok, reply2} =
+ CommonAPI.post(user, %{"status" => "reply2", "in_reply_to_status_id" => activity.id})
+
+ replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end)
+
+ {:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data)
+
+ Repo.delete(activity.object)
+ Repo.delete(activity)
+
+ %{federation_output: federation_output, replies_uris: replies_uris}
+ end
+
+ test "schedules background fetching of `replies` items if max thread depth limit allows", %{
+ federation_output: federation_output,
+ replies_uris: replies_uris
+ } do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1)
+
+ {:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
+
+ for id <- replies_uris do
+ job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
+ assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
+ end
+ end
+
+ test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
+ %{federation_output: federation_output} do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+ {:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
+
+ assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
+ end
+ end
+
describe "prepare outgoing" do
test "it inlines private announced objects" do
user = insert(:user)
@@ -2046,4 +2143,49 @@ test "returns object with emoji when object contains map tag" do
}
end
end
+
+ describe "set_replies/1" do
+ clear_config([:activitypub, :note_replies_output_limit]) do
+ Pleroma.Config.put([:activitypub, :note_replies_output_limit], 2)
+ end
+
+ test "returns unmodified object if activity doesn't have self-replies" do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ assert Transmogrifier.set_replies(data) == data
+ end
+
+ test "sets `replies` collection with a limited number of self-replies" do
+ [user, another_user] = insert_list(2, :user)
+
+ {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{"status" => "1"})
+
+ {:ok, %{id: id2} = self_reply1} =
+ CommonAPI.post(user, %{"status" => "self-reply 1", "in_reply_to_status_id" => id1})
+
+ {:ok, self_reply2} =
+ CommonAPI.post(user, %{"status" => "self-reply 2", "in_reply_to_status_id" => id1})
+
+ # Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2
+ {:ok, _} =
+ CommonAPI.post(user, %{"status" => "self-reply 3", "in_reply_to_status_id" => id1})
+
+ {:ok, _} =
+ CommonAPI.post(user, %{
+ "status" => "self-reply to self-reply",
+ "in_reply_to_status_id" => id2
+ })
+
+ {:ok, _} =
+ CommonAPI.post(another_user, %{
+ "status" => "another user's reply",
+ "in_reply_to_status_id" => id1
+ })
+
+ object = Object.normalize(activity)
+ replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
+
+ assert %{"type" => "Collection", "items" => ^replies_uris} =
+ Transmogrifier.set_replies(object.data)["replies"]
+ end
+ end
end
diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs
index 13447dc29..acc855b98 100644
--- a/test/web/activity_pub/views/object_view_test.exs
+++ b/test/web/activity_pub/views/object_view_test.exs
@@ -36,6 +36,26 @@ test "renders a note activity" do
assert result["@context"]
end
+ describe "note activity's `replies` collection rendering" do
+ clear_config([:activitypub, :note_replies_output_limit]) do
+ Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
+ end
+
+ test "renders `replies` collection for a note activity" do
+ user = insert(:user)
+ activity = insert(:note_activity, user: user)
+
+ {:ok, self_reply1} =
+ CommonAPI.post(user, %{"status" => "self-reply 1", "in_reply_to_status_id" => activity.id})
+
+ replies_uris = [self_reply1.object.data["id"]]
+ result = ObjectView.render("object.json", %{object: refresh_record(activity)})
+
+ assert %{"type" => "Collection", "items" => ^replies_uris} =
+ get_in(result, ["object", "replies"])
+ end
+ end
+
test "renders a like activity" do
note = insert(:note_activity)
object = Object.normalize(note)
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 0cab546ac..7add75263 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -3064,6 +3064,52 @@ test "pleroma restarts", %{conn: conn} do
end
end
+ describe "GET /api/pleroma/admin/statuses" do
+ test "returns all public, unlisted, and direct statuses", %{conn: conn, admin: admin} do
+ blocked = insert(:user)
+ user = insert(:user)
+ User.block(admin, blocked)
+
+ {:ok, _} =
+ CommonAPI.post(user, %{"status" => "@#{admin.nickname}", "visibility" => "direct"})
+
+ {:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
+ {:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+ {:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
+ {:ok, _} = CommonAPI.post(blocked, %{"status" => ".", "visibility" => "public"})
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/statuses")
+ |> json_response(200)
+
+ refute "private" in Enum.map(response, & &1["visibility"])
+ assert length(response) == 4
+ end
+
+ test "returns only local statuses with local_only on", %{conn: conn} do
+ user = insert(:user)
+ remote_user = insert(:user, local: false, nickname: "archaeme@archae.me")
+ insert(:note_activity, user: user, local: true)
+ insert(:note_activity, user: remote_user, local: false)
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/statuses?local_only=true")
+ |> json_response(200)
+
+ assert length(response) == 1
+ end
+
+ test "returns private statuses with godmode on", %{conn: conn} do
+ user = insert(:user)
+ {:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+ {:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
+ conn = get(conn, "/api/pleroma/admin/statuses?godmode=true")
+ assert json_response(conn, 200) |> length() == 2
+ end
+ end
+
describe "GET /api/pleroma/admin/users/:nickname/statuses" do
setup do
user = insert(:user)
@@ -3114,6 +3160,20 @@ test "returns private statuses with godmode on", %{conn: conn, user: user} do
assert json_response(conn, 200) |> length() == 5
end
+
+ test "excludes reblogs by default", %{conn: conn, user: user} do
+ other_user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "."})
+ {:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, other_user)
+
+ conn_res = get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses")
+ assert json_response(conn_res, 200) |> length() == 0
+
+ conn_res =
+ get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses?with_reblogs=true")
+
+ assert json_response(conn_res, 200) |> length() == 1
+ end
end
describe "GET /api/pleroma/admin/moderation_log" do
@@ -3396,7 +3456,7 @@ test "GET /instances/:instance/statuses", %{conn: conn} do
user = insert(:user, local: false, nickname: "archaeme@archae.me")
user2 = insert(:user, local: false, nickname: "test@test.com")
insert_pair(:note_activity, user: user)
- insert(:note_activity, user: user2)
+ activity = insert(:note_activity, user: user2)
ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses")
@@ -3415,6 +3475,16 @@ test "GET /instances/:instance/statuses", %{conn: conn} do
response = json_response(ret_conn, 200)
assert Enum.empty?(response)
+
+ CommonAPI.repeat(activity.id, user)
+
+ ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses")
+ response = json_response(ret_conn, 200)
+ assert length(response) == 2
+
+ ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true")
+ response = json_response(ret_conn, 200)
+ assert length(response) == 3
end
end
@@ -3544,6 +3614,25 @@ test "GET /api/pleroma/admin/config/descriptions", %{conn: conn} do
assert String.starts_with?(child["group"], ":")
assert child["description"]
end
+
+ describe "/api/pleroma/admin/stats" do
+ test "status visibility count", %{conn: conn} do
+ admin = insert(:user, is_admin: true)
+ user = insert(:user)
+ CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
+ CommonAPI.post(user, %{"visibility" => "unlisted", "status" => "hey"})
+ CommonAPI.post(user, %{"visibility" => "unlisted", "status" => "hey"})
+
+ response =
+ conn
+ |> assign(:user, admin)
+ |> get("/api/pleroma/admin/stats")
+ |> json_response(200)
+
+ assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 2} =
+ response["status_visibility"]
+ end
+ end
end
# Needed for testing
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index 737976f1f..9f931c941 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors
+# Copyright © 2017-2020 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.ImplTest do
@@ -98,6 +98,14 @@ test "delete subscription if result send message between 400..500" do
refute Pleroma.Repo.get(Subscription, subscription.id)
end
+ test "deletes subscription when token has been deleted" do
+ subscription = insert(:push_subscription)
+
+ Pleroma.Repo.delete(subscription.token)
+
+ refute Pleroma.Repo.get(Subscription, subscription.id)
+ end
+
test "renders title and body for create activity" do
user = insert(:user, nickname: "Bob")