Merge branch '1397-instance-personnalization' into 'main'
Draft: enable instance personnalization Closes #1087 and #1397 See merge request framasoft/mobilizon!1538
This commit is contained in:
commit
37afc27ec7
4
Makefile
4
Makefile
|
@ -19,8 +19,8 @@ stop:
|
|||
@bash docker/message.sh "Mobilizon is stopped"
|
||||
test: stop
|
||||
@bash docker/message.sh "Running tests"
|
||||
docker compose -f docker compose.yml -f docker compose.test.yml run api mix prepare_test
|
||||
docker compose -f docker compose.yml -f docker compose.test.yml run api mix test $(only)
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml run api mix prepare_test
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml run api mix test $(only)
|
||||
@bash docker/message.sh "Done running tests"
|
||||
format:
|
||||
docker compose run --rm api bash -c "mix format && mix credo --strict"
|
||||
|
|
|
@ -5,11 +5,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
|
||||
import Mobilizon.Users.Guards
|
||||
|
||||
alias Mobilizon.{Actors, Admin, Config, Events, Instances, Users}
|
||||
alias Mobilizon.{Actors, Admin, Config, Events, Instances, Media, Users}
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Admin.{ActionLog, Setting}
|
||||
alias Mobilizon.Admin.{ActionLog, Setting, SettingMedia}
|
||||
alias Mobilizon.Cldr.Language
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Relay}
|
||||
|
@ -20,6 +19,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Web.Email
|
||||
|
||||
alias Mobilizon.GraphQL.Resolvers.Media, as: MediaResolver
|
||||
|
||||
import Mobilizon.Web.Gettext
|
||||
require Logger
|
||||
|
||||
|
@ -268,8 +270,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
with {:ok, res} <- Admin.save_settings("instance", args),
|
||||
res <-
|
||||
res
|
||||
|> Enum.map(fn {key, %Setting{value: value}} ->
|
||||
{key, Admin.get_setting_value(value)}
|
||||
|> Enum.map(fn {key, val} ->
|
||||
case val do
|
||||
%Setting{value: value} -> {key, Admin.get_setting_value(value)}
|
||||
%SettingMedia{media: media} -> {key, media}
|
||||
end
|
||||
end)
|
||||
|> Enum.into(%{}),
|
||||
:ok <- eventually_update_instance_actor(res) do
|
||||
|
@ -284,6 +289,38 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
dgettext("errors", "You need to be logged-in and an administrator to save admin settings")}
|
||||
end
|
||||
|
||||
@spec get_media_setting(any(), any(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Media.t()} | {:error, String.t()}
|
||||
def get_media_setting(_parent, %{group: group, name: name}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
{:ok, MediaResolver.transform_media(Admin.get_admin_setting_media(group, name, nil))}
|
||||
end
|
||||
|
||||
def get_media_setting(_parent, _args, _resolution) do
|
||||
{:error,
|
||||
dgettext("errors", "You need to be logged-in and an administrator to access admin settings")}
|
||||
end
|
||||
|
||||
@spec get_instance_logo(any(), any(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Media.t() | nil} | {:error, String.t()}
|
||||
def get_instance_logo(parent, _args, resolution) do
|
||||
get_media_setting(parent, %{group: "instance", name: "instance_logo"}, resolution)
|
||||
end
|
||||
|
||||
@spec get_instance_favicon(any(), any(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Media.t() | nil} | {:error, String.t()}
|
||||
def get_instance_favicon(parent, _args, resolution) do
|
||||
get_media_setting(parent, %{group: "instance", name: "instance_favicon"}, resolution)
|
||||
end
|
||||
|
||||
@spec get_default_picture(any(), any(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Media.t() | nil} | {:error, String.t()}
|
||||
def get_default_picture(parent, _args, resolution) do
|
||||
get_media_setting(parent, %{group: "instance", name: "default_picture"}, resolution)
|
||||
end
|
||||
|
||||
@spec update_user(any, map(), Absinthe.Resolution.t()) ::
|
||||
{:error, :invalid_argument | :user_not_found | binary | Ecto.Changeset.t()}
|
||||
| {:ok, Mobilizon.Users.User.t()}
|
||||
|
|
|
@ -5,8 +5,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Events.Categories
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Service.{AntiSpam, FrontEndAnalytics}
|
||||
|
||||
alias Mobilizon.GraphQL.Resolvers.Media, as: MediaResolver
|
||||
|
||||
@doc """
|
||||
Gets config.
|
||||
"""
|
||||
|
@ -31,6 +34,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||
{:ok, data}
|
||||
end
|
||||
|
||||
@spec instance_logo(any(), map(), Absinthe.Resolution.t()) :: {:ok, Media.t()}
|
||||
def instance_logo(_parent, _params, _resolution) do
|
||||
{:ok, MediaResolver.transform_media(Config.instance_logo())}
|
||||
end
|
||||
|
||||
@spec default_picture(any(), map(), Absinthe.Resolution.t()) :: {:ok, Media.t()}
|
||||
def default_picture(_parent, _params, _resolution) do
|
||||
{:ok, MediaResolver.transform_media(Config.default_picture())}
|
||||
end
|
||||
|
||||
@spec terms(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
||||
def terms(_parent, %{locale: locale}, _resolution) do
|
||||
type = Config.instance_terms_type()
|
||||
|
@ -98,6 +111,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||
long_description: Config.instance_long_description(),
|
||||
slogan: Config.instance_slogan(),
|
||||
languages: Config.instance_languages(),
|
||||
instance_logo: Config.instance_logo(),
|
||||
primary_color: Config.primary_color(),
|
||||
secondary_color: Config.secondary_color(),
|
||||
default_picture: Config.default_picture(),
|
||||
anonymous: %{
|
||||
participation: %{
|
||||
allowed: Config.anonymous_participation?(),
|
||||
|
|
|
@ -18,6 +18,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
|
|||
do_fetch_media(media_id)
|
||||
end
|
||||
|
||||
def media(%{media_id: media_id} = _parent, _args, _resolution) do
|
||||
do_fetch_media(media_id)
|
||||
end
|
||||
|
||||
def media(%{picture: media} = _parent, _args, _resolution), do: {:ok, media}
|
||||
def media(_parent, %{id: media_id}, _resolution), do: do_fetch_media(media_id)
|
||||
def media(_parent, _args, _resolution), do: {:ok, nil}
|
||||
|
@ -133,8 +137,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
|
|||
|
||||
def user_size(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@spec transform_media(Media.t()) :: map()
|
||||
defp transform_media(%Media{id: id, file: file, metadata: metadata}) do
|
||||
@spec transform_media(Media.t() | nil) :: map() | nil
|
||||
def transform_media(nil), do: nil
|
||||
|
||||
def transform_media(%Media{id: id, file: file, metadata: metadata}) do
|
||||
%{
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
|
|
|
@ -124,6 +124,24 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
|||
field(:instance_terms_type, :instance_terms_type, description: "The instance's terms type")
|
||||
field(:instance_terms_url, :string, description: "The instance's terms URL")
|
||||
|
||||
field(:instance_logo, :media,
|
||||
description: "The instance's logo",
|
||||
resolve: &Admin.get_instance_logo/3
|
||||
)
|
||||
|
||||
field(:instance_favicon, :media,
|
||||
description: "The instance's favicon",
|
||||
resolve: &Admin.get_instance_favicon/3
|
||||
)
|
||||
|
||||
field(:default_picture, :media,
|
||||
description: "The default picture",
|
||||
resolve: &Admin.get_default_picture/3
|
||||
)
|
||||
|
||||
field(:primary_color, :string, description: "The instance's primary color")
|
||||
field(:secondary_color, :string, description: "The instance's secondary color")
|
||||
|
||||
field(:instance_privacy_policy, :string,
|
||||
description: "The instance's privacy policy body text"
|
||||
)
|
||||
|
@ -412,6 +430,25 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
|||
arg(:instance_long_description, :string, description: "The instance's long description")
|
||||
arg(:instance_slogan, :string, description: "The instance's slogan")
|
||||
arg(:contact, :string, description: "The instance's contact details")
|
||||
|
||||
arg(:instance_logo, :media_input,
|
||||
description:
|
||||
"The instance's logo, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:instance_favicon, :media_input,
|
||||
description:
|
||||
"The instance's favicon, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:default_picture, :media_input,
|
||||
description:
|
||||
"The default picture, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:primary_color, :string, description: "The instance's primary color")
|
||||
arg(:secondary_color, :string, description: "The instance's secondary color")
|
||||
|
||||
arg(:instance_terms, :string, description: "The instance's terms body text")
|
||||
arg(:instance_terms_type, :instance_terms_type, description: "The instance's terms type")
|
||||
arg(:instance_terms_url, :string, description: "The instance's terms URL")
|
||||
|
|
|
@ -59,6 +59,17 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
|
|||
resolve(&Config.terms/3)
|
||||
end
|
||||
|
||||
field(:instance_logo, :media, description: "The instance's logo") do
|
||||
resolve(&Config.instance_logo/3)
|
||||
end
|
||||
|
||||
field(:default_picture, :media, description: "The default picture") do
|
||||
resolve(&Config.default_picture/3)
|
||||
end
|
||||
|
||||
field(:primary_color, :string, description: "The instance's primary color")
|
||||
field(:secondary_color, :string, description: "The instance's secondary color")
|
||||
|
||||
field(:privacy, :privacy, description: "The instance's privacy policy") do
|
||||
arg(:locale, :string,
|
||||
default_value: "en",
|
||||
|
|
|
@ -9,7 +9,8 @@ defmodule Mobilizon.Admin do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.{Admin, Users}
|
||||
alias Mobilizon.Admin.ActionLog
|
||||
alias Mobilizon.Admin.Setting
|
||||
alias Mobilizon.Admin.{Setting, SettingMedia}
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
|
@ -78,9 +79,47 @@ defmodule Mobilizon.Admin do
|
|||
|
||||
defp stringify_struct(struct), do: struct
|
||||
|
||||
@spec get_all_admin_settings :: list(Setting.t())
|
||||
@spec get_all_admin_settings :: map()
|
||||
def get_all_admin_settings do
|
||||
Repo.all(Setting)
|
||||
medias =
|
||||
SettingMedia
|
||||
|> Repo.all()
|
||||
|> Repo.preload(:media)
|
||||
|> Enum.map(fn %SettingMedia{group: group, name: name, media: media} ->
|
||||
{group, name, media}
|
||||
end)
|
||||
|
||||
values =
|
||||
Setting
|
||||
|> Repo.all()
|
||||
|> Enum.map(fn %Setting{group: group, name: name, value: value} ->
|
||||
{group, name, get_setting_value(value)}
|
||||
end)
|
||||
|
||||
all_settings = Enum.concat(values, medias)
|
||||
|
||||
Enum.reduce(
|
||||
all_settings,
|
||||
%{},
|
||||
# For each {group,name,value}
|
||||
fn {group, name, value}, acc ->
|
||||
# We update the %{group: map} in the accumulator
|
||||
{_, new_acc} =
|
||||
Map.get_and_update(
|
||||
acc,
|
||||
group,
|
||||
# We put the %{name: value} into the %{group: map}
|
||||
fn group_map ->
|
||||
{
|
||||
group_map,
|
||||
Map.put(group_map || %{}, name, value)
|
||||
}
|
||||
end
|
||||
)
|
||||
|
||||
new_acc
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@spec get_admin_setting_value(String.t(), String.t(), String.t() | nil) ::
|
||||
|
@ -119,21 +158,40 @@ defmodule Mobilizon.Admin do
|
|||
end
|
||||
end
|
||||
|
||||
@spec get_admin_setting_media(String.t(), String.t(), String.t() | nil) ::
|
||||
{:ok, Media.t()} | {:error, :not_found} | nil
|
||||
def get_admin_setting_media(group, name, fallback \\ nil)
|
||||
when is_binary(group) and is_binary(name) do
|
||||
case SettingMedia
|
||||
|> where(group: ^group)
|
||||
|> where(name: ^name)
|
||||
|> preload(:media)
|
||||
|> Repo.one() do
|
||||
nil ->
|
||||
fallback
|
||||
|
||||
%SettingMedia{media: media} ->
|
||||
media
|
||||
|
||||
%SettingMedia{} ->
|
||||
fallback
|
||||
end
|
||||
end
|
||||
|
||||
@spec save_settings(String.t(), map()) :: {:ok, any} | {:error, any}
|
||||
def save_settings(group, args) do
|
||||
{medias, values} = Map.split(args, [:instance_logo, :instance_favicon, :default_picture])
|
||||
|
||||
Multi.new()
|
||||
|> do_save_setting(group, args)
|
||||
|> do_save_media_setting(group, medias)
|
||||
|> do_save_value_setting(group, values)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
def clear_settings(group) do
|
||||
Setting |> where([s], s.group == ^group) |> Repo.delete_all()
|
||||
end
|
||||
@spec do_save_value_setting(Ecto.Multi.t(), String.t(), map()) :: Ecto.Multi.t()
|
||||
defp do_save_value_setting(transaction, _group, args) when args == %{}, do: transaction
|
||||
|
||||
@spec do_save_setting(Ecto.Multi.t(), String.t(), map()) :: Ecto.Multi.t()
|
||||
defp do_save_setting(transaction, _group, args) when args == %{}, do: transaction
|
||||
|
||||
defp do_save_setting(transaction, group, args) do
|
||||
defp do_save_value_setting(transaction, group, args) do
|
||||
key = hd(Map.keys(args))
|
||||
{val, rest} = Map.pop(args, key)
|
||||
|
||||
|
@ -150,7 +208,40 @@ defmodule Mobilizon.Admin do
|
|||
conflict_target: [:group, :name]
|
||||
)
|
||||
|
||||
do_save_setting(transaction, group, rest)
|
||||
do_save_value_setting(transaction, group, rest)
|
||||
end
|
||||
|
||||
@spec do_save_media_setting(Ecto.Multi.t(), String.t(), map()) :: Ecto.Multi.t()
|
||||
defp do_save_media_setting(transaction, _group, args) when args == %{}, do: transaction
|
||||
|
||||
defp do_save_media_setting(transaction, group, args) do
|
||||
key = hd(Map.keys(args))
|
||||
{val, rest} = Map.pop(args, key)
|
||||
|
||||
transaction =
|
||||
case val do
|
||||
val ->
|
||||
Multi.insert(
|
||||
transaction,
|
||||
key,
|
||||
SettingMedia.changeset(%SettingMedia{}, %{
|
||||
group: group,
|
||||
name: Atom.to_string(key),
|
||||
media: val
|
||||
}),
|
||||
on_conflict: :replace_all,
|
||||
conflict_target: [:group, :name]
|
||||
)
|
||||
end
|
||||
|
||||
do_save_media_setting(transaction, group, rest)
|
||||
end
|
||||
|
||||
def clear_settings(group) do
|
||||
Multi.new()
|
||||
|> Multi.delete_all(:settings, Setting |> where([s], s.group == ^group))
|
||||
|> Multi.delete_all(:settings_medias, SettingMedia |> where([s], s.group == ^group))
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
@spec convert_to_string(any()) :: String.t()
|
||||
|
|
|
@ -4,6 +4,7 @@ defmodule Mobilizon.Admin.Setting do
|
|||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Ecto.Changeset
|
||||
|
||||
@required_attrs [:group, :name]
|
||||
@optional_attrs [:value]
|
||||
|
@ -32,3 +33,93 @@ defmodule Mobilizon.Admin.Setting do
|
|||
|> unique_constraint(:group, name: :admin_settings_group_name_index)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Admin.SettingMedia do
|
||||
@moduledoc """
|
||||
A Key-Value settings table for media settings
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Ecto.Changeset
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
alias Mobilizon.Medias
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
@required_attrs [:group, :name]
|
||||
|
||||
@type t :: %{
|
||||
group: String.t(),
|
||||
name: String.t(),
|
||||
media: Media.t()
|
||||
}
|
||||
|
||||
schema "admin_settings_medias" do
|
||||
field(:group, :string)
|
||||
field(:name, :string)
|
||||
belongs_to(:media, Media, on_replace: :delete)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
|
||||
def changeset(setting_media, attrs) do
|
||||
setting_media
|
||||
|> Repo.preload(:media)
|
||||
|> cast(attrs, @required_attrs)
|
||||
|> put_media(attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:group, name: :admin_settings_medias_group_name_index)
|
||||
end
|
||||
|
||||
# # In case the provided media is an existing one
|
||||
@spec put_media(Changeset.t(), map) :: Changeset.t()
|
||||
defp put_media(%Changeset{} = changeset, %{media: %{media_id: id}}) do
|
||||
%Media{} = media = Medias.get_media!(id)
|
||||
put_assoc(changeset, :media, media)
|
||||
end
|
||||
|
||||
# In case it's a new media
|
||||
defp put_media(%Changeset{} = changeset, %{media: %{media: media}}) do
|
||||
{:ok, media} = upload_media(media)
|
||||
put_assoc(changeset, :media, media)
|
||||
end
|
||||
|
||||
# In case there is no media
|
||||
defp put_media(%Changeset{} = changeset, _media) do
|
||||
put_assoc(changeset, :media, nil)
|
||||
end
|
||||
|
||||
import Mobilizon.Web.Gettext
|
||||
@spec upload_media(map) :: {:ok, Media.t()} | {:error, any}
|
||||
defp upload_media(%{file: %Plug.Upload{} = file} = args) do
|
||||
with {:ok,
|
||||
%{
|
||||
name: _name,
|
||||
url: url,
|
||||
content_type: content_type,
|
||||
size: size
|
||||
} = uploaded} <-
|
||||
Mobilizon.Web.Upload.store(file),
|
||||
args <-
|
||||
args
|
||||
|> Map.put(:url, url)
|
||||
|> Map.put(:size, size)
|
||||
|> Map.put(:content_type, content_type),
|
||||
{:ok, media = %Media{}} <-
|
||||
Medias.create_media(%{
|
||||
file: args,
|
||||
actor_id: Map.get(args, :actor_id, Relay.get_actor().id),
|
||||
metadata: Map.take(uploaded, [:width, :height, :blurhash])
|
||||
}) do
|
||||
{:ok, media}
|
||||
else
|
||||
{:error, :mime_type_not_allowed} ->
|
||||
{:error, dgettext("errors", "File doesn't have an allowed MIME type.")}
|
||||
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,8 @@ defmodule Mobilizon.Config do
|
|||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Admin.Setting
|
||||
alias Mobilizon.Admin
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Service.GitStatus
|
||||
require Logger
|
||||
import Mobilizon.Service.Export.Participants.Common, only: [enabled_formats: 0]
|
||||
|
@ -29,56 +30,18 @@ defmodule Mobilizon.Config do
|
|||
@spec instance_config :: mobilizon_config
|
||||
def instance_config, do: Application.get_env(:mobilizon, :instance)
|
||||
|
||||
@spec db_instance_config :: list(Setting.t())
|
||||
def db_instance_config, do: Mobilizon.Admin.get_all_admin_settings()
|
||||
|
||||
@spec config_cache :: map()
|
||||
def config_cache do
|
||||
case Cachex.fetch(:config, :all_db_config, fn _key ->
|
||||
value =
|
||||
Enum.reduce(
|
||||
Mobilizon.Admin.get_all_admin_settings(),
|
||||
%{},
|
||||
&arrange_values/2
|
||||
)
|
||||
|
||||
{:commit, value}
|
||||
end) do
|
||||
case Cachex.fetch(
|
||||
:config,
|
||||
:all_db_config,
|
||||
fn _key -> {:commit, Admin.get_all_admin_settings()} end
|
||||
) do
|
||||
{status, value} when status in [:ok, :commit] -> value
|
||||
_err -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
@spec arrange_values(Setting.t(), map()) :: map()
|
||||
defp arrange_values(setting, acc) do
|
||||
{_, new_data} =
|
||||
Map.get_and_update(acc, setting.group, fn current_value ->
|
||||
new_value = current_value || %{}
|
||||
|
||||
{current_value, Map.put(new_value, setting.name, process_value(setting.value))}
|
||||
end)
|
||||
|
||||
new_data
|
||||
end
|
||||
|
||||
@spec process_value(String.t() | nil) :: any()
|
||||
defp process_value(nil), do: nil
|
||||
defp process_value(""), do: nil
|
||||
|
||||
defp process_value(value) do
|
||||
case Jason.decode(value) do
|
||||
{:ok, val} ->
|
||||
val
|
||||
|
||||
{:error, _} ->
|
||||
case value do
|
||||
"true" -> true
|
||||
"false" -> false
|
||||
value -> value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec config_cached_value(String.t(), String.t(), String.t()) :: any()
|
||||
def config_cached_value(group, name, fallback \\ nil) do
|
||||
config_cache()
|
||||
|
@ -115,10 +78,23 @@ defmodule Mobilizon.Config do
|
|||
@spec instance_slogan :: String.t() | nil
|
||||
def instance_slogan, do: config_cached_value("instance", "instance_slogan")
|
||||
|
||||
@spec instance_logo :: Media.t() | nil
|
||||
def instance_logo, do: config_cached_value("instance", "instance_logo")
|
||||
|
||||
@spec instance_favicon :: Media.t() | nil
|
||||
def instance_favicon, do: config_cached_value("instance", "instance_favicon")
|
||||
|
||||
@spec default_picture :: Media.t() | nil
|
||||
def default_picture, do: config_cached_value("instance", "default_picture")
|
||||
|
||||
@spec primary_color :: Media.t() | nil
|
||||
def primary_color, do: config_cached_value("instance", "primary_color")
|
||||
|
||||
@spec secondary_color :: Media.t() | nil
|
||||
def secondary_color, do: config_cached_value("instance", "secondary_color")
|
||||
|
||||
@spec contact :: String.t() | nil
|
||||
def contact do
|
||||
config_cached_value("instance", "contact")
|
||||
end
|
||||
def contact, do: config_cached_value("instance", "contact")
|
||||
|
||||
@spec instance_terms(String.t()) :: String.t()
|
||||
def instance_terms(locale \\ "en") do
|
||||
|
@ -470,6 +446,9 @@ defmodule Mobilizon.Config do
|
|||
instance_slogan: instance_slogan(),
|
||||
registrations_open: instance_registrations_open?(),
|
||||
contact: contact(),
|
||||
primary_color: primary_color(),
|
||||
secondary_color: secondary_color(),
|
||||
instance_logo: instance_logo(),
|
||||
instance_terms: instance_terms(),
|
||||
instance_terms_type: instance_terms_type(),
|
||||
instance_terms_url: instance_terms_url(),
|
||||
|
|
|
@ -185,7 +185,8 @@ defmodule Mobilizon.Medias do
|
|||
[from: "events_medias", param: "media_id"],
|
||||
[from: "posts", param: "picture_id"],
|
||||
[from: "posts_medias", param: "media_id"],
|
||||
[from: "comments_medias", param: "media_id"]
|
||||
[from: "comments_medias", param: "media_id"],
|
||||
[from: "admin_settings_medias", param: "media_id"]
|
||||
]
|
||||
|> Enum.map_join(" UNION ", fn [from: from, param: param] ->
|
||||
"SELECT 1 FROM #{from} WHERE #{from}.#{param} = m0.id"
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
defmodule Mobilizon.Web.ManifestController do
|
||||
use Mobilizon.Web, :controller
|
||||
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Medias.Media
|
||||
|
||||
@spec manifest(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||
def manifest(conn, _params) do
|
||||
favicons =
|
||||
case Config.instance_favicon() do
|
||||
%Media{file: %{url: url}, metadata: metadata} ->
|
||||
[
|
||||
Map.merge(
|
||||
%{
|
||||
src: url
|
||||
},
|
||||
case metadata do
|
||||
%{width: width} -> %{sizes: "#{width}x#{width}"}
|
||||
_ -> %{}
|
||||
end
|
||||
)
|
||||
]
|
||||
|
||||
_ ->
|
||||
[
|
||||
%{
|
||||
src: "./img/icons/android-chrome-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png"
|
||||
},
|
||||
%{
|
||||
src: "./img/icons/android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
json(conn, %{
|
||||
name: Config.instance_name(),
|
||||
start_url: "/",
|
||||
scope: "/",
|
||||
display: "standalone",
|
||||
background_color: "#ffffff",
|
||||
theme_color: "#ffd599",
|
||||
orientation: "portrait-primary",
|
||||
icons: favicons
|
||||
})
|
||||
end
|
||||
|
||||
@spec favicon(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||
def favicon(conn, _params) do
|
||||
case Config.instance_favicon() do
|
||||
%Media{file: %{url: url}} -> redirect(conn, external: url)
|
||||
_ -> redirect(conn, to: "/img/icons/favicon.ico")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,8 +18,7 @@ defmodule Mobilizon.Web do
|
|||
"""
|
||||
|
||||
def static_paths,
|
||||
do:
|
||||
~w(index.html manifest.json manifest.webmanifest service-worker.js css fonts img js favicon.ico robots.txt assets)
|
||||
do: ~w(index.html service-worker.js css fonts img js robots.txt assets)
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
|
|
|
@ -113,6 +113,12 @@ defmodule Mobilizon.Web.Router do
|
|||
get("/nodeinfo/:version", NodeInfoController, :nodeinfo)
|
||||
end
|
||||
|
||||
scope "/", Mobilizon.Web do
|
||||
get("/manifest.webmanifest", ManifestController, :manifest)
|
||||
get("/manifest.json", ManifestController, :manifest)
|
||||
get("/favicon.ico", ManifestController, :favicon)
|
||||
end
|
||||
|
||||
scope "/", Mobilizon.Web do
|
||||
pipe_through(:activity_pub_and_html)
|
||||
pipe_through(:activity_pub_signature)
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png" sizes="152x152" />
|
||||
<link rel="apple-touch-icon" href={favicon_url()} sizes={favicon_sizes()} />
|
||||
<link rel="icon" href={favicon_url()} sizes={favicon_sizes()} />
|
||||
<link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={theme_color()} />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="theme-color" content={theme_color()} />
|
||||
<script>
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
</script>
|
||||
<%= if root?(assigns) do %>
|
||||
|
@ -24,6 +25,7 @@
|
|||
<%= Vite.vite_client() %>
|
||||
<%= Vite.vite_snippet("src/main.ts") %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Mobilizon.Web.PageView do
|
|||
use Mobilizon.Web, :view
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Posts.Post
|
||||
|
@ -91,4 +92,27 @@ defmodule Mobilizon.Web.PageView do
|
|||
def root?(assigns) do
|
||||
assigns |> Map.get(:conn, %{request_path: "/"}) |> Map.get(:request_path, "/") == "/"
|
||||
end
|
||||
|
||||
defp favicon do
|
||||
case Config.instance_favicon() do
|
||||
%{file: %{url: url}, metadata: metadata} ->
|
||||
%{
|
||||
src: url,
|
||||
sizes:
|
||||
case metadata do
|
||||
%{width: width} -> "#{width}x#{width}"
|
||||
_ -> "any"
|
||||
end
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{
|
||||
src: "/img/icons/apple-touch-icon-152x152.png",
|
||||
sizes: "152x152"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def favicon_url, do: Map.get(favicon(), :src)
|
||||
def favicon_sizes, do: Map.get(favicon(), :sizes)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
defmodule Mobilizon.Storage.Repo.Migrations.CreateAdminSettingsMedias do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:admin_settings_medias) do
|
||||
add(:group, :string)
|
||||
add(:name, :string)
|
||||
add(:media_id, references(:medias, on_delete: :delete_all))
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create(unique_index(:admin_settings_medias, [:group, :name]))
|
||||
end
|
||||
end
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -169,6 +169,18 @@ type Config {
|
|||
"The instance's slogan"
|
||||
slogan: String
|
||||
|
||||
"The instance's logo"
|
||||
instanceLogo: Media
|
||||
|
||||
"The default picture"
|
||||
defaultPicture: Media
|
||||
|
||||
"The instance's primary color"
|
||||
primaryColor: String
|
||||
|
||||
"The instance's secondary color"
|
||||
secondaryColor: String
|
||||
|
||||
"The instance's contact details"
|
||||
contact: String
|
||||
|
||||
|
@ -1878,6 +1890,15 @@ type RootMutationType {
|
|||
"The instance's contact details"
|
||||
contact: String
|
||||
|
||||
"The instance's logo"
|
||||
instanceLogo: MediaInput
|
||||
|
||||
"The instance's favicon"
|
||||
instanceFavicon: MediaInput
|
||||
|
||||
"The default picture"
|
||||
defaultPicture: MediaInput
|
||||
|
||||
"The instance's terms body text"
|
||||
instanceTerms: String
|
||||
|
||||
|
@ -3698,6 +3719,15 @@ type AdminSettings {
|
|||
"The instance's contact details"
|
||||
contact: String
|
||||
|
||||
"The instance's logo"
|
||||
instanceLogo: Media
|
||||
|
||||
"The instance's favicon"
|
||||
instanceFavicon: Media
|
||||
|
||||
"The default picture"
|
||||
defaultPicture: Media
|
||||
|
||||
"The instance's terms body text"
|
||||
instanceTerms: String
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ import { IAnalyticsConfig, IConfig } from "@/types/config.model";
|
|||
import { computed, defineAsyncComponent, ref } from "vue";
|
||||
import { useQuery, useQueryLoading } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useAnalytics } from "@/composition/apollo/config";
|
||||
import { INSTANCE_NAME } from "@/graphql/config";
|
||||
const SentryFeedback = defineAsyncComponent(
|
||||
|
|
|
@ -16,7 +16,7 @@ import { useGroup } from "@/composition/apollo/group";
|
|||
import { displayName } from "@/types/actor";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const props = defineProps<{
|
||||
preferredUsername: string;
|
||||
|
|
|
@ -11,8 +11,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { IMedia } from "@/types/media.model";
|
||||
import { useDefaultPicture } from "@/composition/apollo/config";
|
||||
import LazyImage from "../Image/LazyImage.vue";
|
||||
|
||||
const { defaultPicture } = useDefaultPicture();
|
||||
|
||||
const DEFAULT_CARD_URL = "/img/mobilizon_default_card.png";
|
||||
const DEFAULT_BLURHASH = "MCHKI4El-P-U}+={R-WWoes,Iu-P=?R,xD";
|
||||
const DEFAULT_WIDTH = 630;
|
||||
|
@ -38,6 +41,9 @@ const props = withDefaults(
|
|||
|
||||
const pictureOrDefault = computed(() => {
|
||||
if (props.picture === null) {
|
||||
if (defaultPicture?.value?.url) {
|
||||
return defaultPicture.value;
|
||||
}
|
||||
return DEFAULT_PICTURE;
|
||||
}
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<svg
|
||||
class="bg-white dark:bg-zinc-900 dark:fill-white"
|
||||
v-if="!instanceLogoUrl"
|
||||
class="bg-white dark:bg-zinc-900 dark:fill-white max-h-12"
|
||||
:class="{ 'bg-gray-900': invert }"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 248.16 46.78"
|
||||
|
@ -30,9 +31,14 @@
|
|||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<img v-else alt="" class="max-h-12 w-auto" :src="instanceLogoUrl" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useInstanceLogoUrl } from "@/composition/apollo/config";
|
||||
|
||||
const { instanceLogoUrl } = useInstanceLogoUrl();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
invert?: boolean;
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-zinc-900"
|
||||
id="navbar"
|
||||
>
|
||||
<div
|
||||
class="container mx-auto flex flex-wrap items-center mx-auto gap-2 sm:gap-4"
|
||||
>
|
||||
<div class="container mx-auto flex flex-wrap items-center gap-2 sm:gap-4">
|
||||
<router-link
|
||||
:to="{ name: RouteName.HOME }"
|
||||
class="flex items-center"
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
|
|
|
@ -45,7 +45,7 @@ import { LEAVE_EVENT } from "../../graphql/event";
|
|||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { IActor } from "@/types/actor";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { useAnonymousActorId } from "@/composition/apollo/config";
|
||||
|
|
|
@ -70,7 +70,7 @@ import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
|||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<script lang="ts" setup>
|
||||
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
|
||||
import { useFetchEvent } from "@/composition/apollo/event";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ import { useFetchEventBasic } from "@/composition/apollo/event";
|
|||
import { useAnonymousActorId } from "@/composition/apollo/config";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
|
||||
const error = ref<boolean | string>(false);
|
||||
|
|
|
@ -99,7 +99,7 @@ import { useFetchEvent } from "@/composition/apollo/event";
|
|||
import { useAnonymousParticipationConfig } from "@/composition/apollo/config";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ uuid: string }>();
|
||||
|
|
|
@ -4,11 +4,14 @@ import {
|
|||
ANONYMOUS_ACTOR_ID,
|
||||
ANONYMOUS_PARTICIPATION_CONFIG,
|
||||
ANONYMOUS_REPORTS_CONFIG,
|
||||
DEFAULT_PICTURE,
|
||||
DEMO_MODE,
|
||||
EVENT_CATEGORIES,
|
||||
EVENT_PARTICIPANTS,
|
||||
FEATURES,
|
||||
GEOCODING_AUTOCOMPLETE,
|
||||
COLORS,
|
||||
INSTANCE_LOGO,
|
||||
LOCATION,
|
||||
MAPS_TILES,
|
||||
REGISTRATIONS,
|
||||
|
@ -76,6 +79,36 @@ export function useInstanceName() {
|
|||
return { instanceName, error, loading };
|
||||
}
|
||||
|
||||
export function useInstanceLogoUrl() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "instanceLogo">;
|
||||
}>(INSTANCE_LOGO);
|
||||
|
||||
const instanceLogoUrl = computed(
|
||||
() => result.value?.config?.instanceLogo?.url
|
||||
);
|
||||
return { instanceLogoUrl, error, loading };
|
||||
}
|
||||
|
||||
export function useColors() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "primaryColor" | "secondaryColor">;
|
||||
}>(COLORS);
|
||||
|
||||
const primaryColor = computed(() => result.value?.config?.primaryColor);
|
||||
const secondaryColor = computed(() => result.value?.config?.secondaryColor);
|
||||
return { primaryColor, secondaryColor, error, loading };
|
||||
}
|
||||
|
||||
export function useDefaultPicture() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "defaultPicture">;
|
||||
}>(DEFAULT_PICTURE);
|
||||
|
||||
const defaultPicture = computed(() => result.value?.config?.defaultPicture);
|
||||
return { defaultPicture, error, loading };
|
||||
}
|
||||
|
||||
export function useAnonymousActorId() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "anonymous">;
|
||||
|
|
|
@ -4,6 +4,12 @@ export const useHost = (): string => {
|
|||
return window.location.hostname;
|
||||
};
|
||||
|
||||
export const useDefaultMaxSize = (): number | undefined => {
|
||||
const { uploadLimits } = useUploadLimits();
|
||||
|
||||
return uploadLimits.value?.default;
|
||||
};
|
||||
|
||||
export const useAvatarMaxSize = (): number | undefined => {
|
||||
const { uploadLimits } = useUploadLimits();
|
||||
|
||||
|
|
|
@ -195,6 +195,23 @@ export const ADMIN_SETTINGS_FRAGMENT = gql`
|
|||
instanceLongDescription
|
||||
instanceSlogan
|
||||
contact
|
||||
instanceLogo {
|
||||
id
|
||||
url
|
||||
name
|
||||
}
|
||||
instanceFavicon {
|
||||
id
|
||||
url
|
||||
name
|
||||
}
|
||||
defaultPicture {
|
||||
id
|
||||
url
|
||||
name
|
||||
}
|
||||
primaryColor
|
||||
secondaryColor
|
||||
instanceTerms
|
||||
instanceTermsType
|
||||
instanceTermsUrl
|
||||
|
@ -223,6 +240,11 @@ export const SAVE_ADMIN_SETTINGS = gql`
|
|||
$instanceLongDescription: String
|
||||
$instanceSlogan: String
|
||||
$contact: String
|
||||
$instanceLogo: MediaInput
|
||||
$instanceFavicon: MediaInput
|
||||
$defaultPicture: MediaInput
|
||||
$primaryColor: String
|
||||
$secondaryColor: String
|
||||
$instanceTerms: String
|
||||
$instanceTermsType: InstanceTermsType
|
||||
$instanceTermsUrl: String
|
||||
|
@ -239,6 +261,11 @@ export const SAVE_ADMIN_SETTINGS = gql`
|
|||
instanceLongDescription: $instanceLongDescription
|
||||
instanceSlogan: $instanceSlogan
|
||||
contact: $contact
|
||||
instanceLogo: $instanceLogo
|
||||
instanceFavicon: $instanceFavicon
|
||||
defaultPicture: $defaultPicture
|
||||
primaryColor: $primaryColor
|
||||
secondaryColor: $secondaryColor
|
||||
instanceTerms: $instanceTerms
|
||||
instanceTermsType: $instanceTermsType
|
||||
instanceTermsUrl: $instanceTermsUrl
|
||||
|
|
|
@ -12,6 +12,21 @@ export const CONFIG = gql`
|
|||
demoMode
|
||||
countryCode
|
||||
languages
|
||||
primaryColor
|
||||
secondaryColor
|
||||
instanceLogo {
|
||||
url
|
||||
}
|
||||
defaultPicture {
|
||||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
width
|
||||
height
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
eventCategories {
|
||||
id
|
||||
label
|
||||
|
@ -454,6 +469,42 @@ export const SEARCH_CONFIG = gql`
|
|||
}
|
||||
`;
|
||||
|
||||
export const INSTANCE_LOGO = gql`
|
||||
query InstanceLogo {
|
||||
config {
|
||||
instanceLogo {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const COLORS = gql`
|
||||
query Colors {
|
||||
config {
|
||||
primaryColor
|
||||
secondaryColor
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DEFAULT_PICTURE = gql`
|
||||
query DefaultPicture {
|
||||
config {
|
||||
defaultPicture {
|
||||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
width
|
||||
height
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const REGISTRATIONS = gql`
|
||||
query Registrations {
|
||||
config {
|
||||
|
|
|
@ -416,6 +416,14 @@
|
|||
"No one is participating|One person participating|{going} people participating": "No one is participating|One person participating|{going} people participating",
|
||||
"Date and time": "Date and time",
|
||||
"Location": "Location",
|
||||
"Logo": "Logo",
|
||||
"Logo of the instance. Defaults to the upstream Mobilizon logo.": "Logo of the instance. Defaults to the upstream Mobilizon logo.",
|
||||
"Favicon": "Favicon",
|
||||
"Browser tab icon and PWA icon of the instance. Defaults to the upstream Mobilizon icon.": "Browser tab icon and PWA icon of the instance. Defaults to the upstream Mobilizon icon.",
|
||||
"Default Picture": "Default Picture",
|
||||
"Default picture when an event or group doesn't have one.": "Default picture when an event or group doesn't have one.",
|
||||
"Primary Color": "Primary Color",
|
||||
"Secondary Color": "Secondary Color",
|
||||
"No resources selected": "No resources selected|One resources selected|{count} resources selected",
|
||||
"You have been invited by {invitedBy} to the following group:": "You have been invited by {invitedBy} to the following group:",
|
||||
"Accept": "Accept",
|
||||
|
@ -1645,4 +1653,4 @@
|
|||
"Domain or instance name": "Domain or instance name",
|
||||
"You need to enter a text": "You need to enter a text",
|
||||
"Error while adding tag: {error}": "Error while adding tag: {error}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -627,6 +627,14 @@
|
|||
"Local times ({timezone})": "Heures locales ({timezone})",
|
||||
"Locality": "Commune",
|
||||
"Location": "Lieu",
|
||||
"Logo": "Logo",
|
||||
"Logo of the instance. Defaults to the upstream Mobilizon logo.": "Logo de l'instance.",
|
||||
"Favicon": "Favicon",
|
||||
"Browser tab icon and PWA icon of the instance. Defaults to the upstream Mobilizon icon.": "Icône de l'onglet du navigateur et de la progressive web app.",
|
||||
"Default Picture": "Image par défaut",
|
||||
"Default picture when an event or group doesn't have one.": "Image par défaut quand un évènement ou groupe n'en a pas.",
|
||||
"Primary Color": "Couleur primaire",
|
||||
"Secondary Color": "Couleur secondaire",
|
||||
"Log in": "Se connecter",
|
||||
"Log out": "Se déconnecter",
|
||||
"Login": "Se connecter",
|
||||
|
|
15
src/main.ts
15
src/main.ts
|
@ -57,6 +57,21 @@ apolloClient
|
|||
})
|
||||
.then(({ data: configData }) => {
|
||||
instanceName.value = configData.config?.name;
|
||||
|
||||
const primaryColor = configData.config?.primaryColor;
|
||||
if (primaryColor) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--custom-primary",
|
||||
primaryColor
|
||||
);
|
||||
}
|
||||
const secondaryColor = configData.config?.secondaryColor;
|
||||
if (secondaryColor) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--custom-secondary",
|
||||
secondaryColor
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const head = createHead();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { IEvent } from "@/types/event.model";
|
||||
import type { IMedia } from "@/types/media.model";
|
||||
import type { IGroup } from "./actor";
|
||||
import { InstancePrivacyType, InstanceTermsType } from "./enums";
|
||||
|
||||
|
@ -25,6 +26,10 @@ export interface IAdminSettings {
|
|||
instanceSlogan: string;
|
||||
instanceLongDescription: string;
|
||||
contact: string;
|
||||
instanceLogo: IMedia | null;
|
||||
defaultPicture: IMedia | null;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
instanceTerms: string;
|
||||
instanceTermsType: InstanceTermsType;
|
||||
instanceTermsUrl: string | null;
|
||||
|
|
|
@ -37,6 +37,10 @@ export interface IConfig {
|
|||
longDescription: string;
|
||||
contact: string;
|
||||
slogan: string;
|
||||
instanceLogo: { url: string };
|
||||
defaultPicture: { url: string };
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
|
||||
registrationsOpen: boolean;
|
||||
registrationsAllowlist: boolean;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type { Ref } from "vue";
|
||||
|
||||
export interface IMedia {
|
||||
id: string;
|
||||
url: string;
|
||||
|
@ -21,3 +23,9 @@ export interface IMediaMetadata {
|
|||
height?: number;
|
||||
blurhash?: string;
|
||||
}
|
||||
|
||||
export interface IModifiableMedia {
|
||||
file: Ref<File | null>;
|
||||
firstHash: string | null;
|
||||
hash: string | null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { computed } from "vue";
|
||||
import { provideApolloClient, useQuery } from "@vue/apollo-composable";
|
||||
import { useHead as unHead } from "@unhead/vue";
|
||||
import { apolloClient } from "@/vue-apollo";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { ABOUT } from "@/graphql/config";
|
||||
|
||||
const { result } = provideApolloClient(apolloClient)(() =>
|
||||
useQuery<{ config: Pick<IConfig, "name"> }>(ABOUT)
|
||||
);
|
||||
const instanceName = computed(() => result.value?.config?.name);
|
||||
|
||||
export function useHead(args: any) {
|
||||
return unHead({
|
||||
...args,
|
||||
title: computed(() =>
|
||||
args?.title?.value
|
||||
? `${args.title.value} - ${instanceName.value}`
|
||||
: instanceName.value
|
||||
),
|
||||
});
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { IMedia } from "@/types/media.model";
|
||||
import { IMedia, IModifiableMedia } from "@/types/media.model";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
export async function buildFileFromIMedia(
|
||||
obj: IMedia | null | undefined
|
||||
|
@ -29,18 +30,83 @@ export function buildFileVariable(
|
|||
};
|
||||
}
|
||||
|
||||
export function readFileAsync(
|
||||
file: File
|
||||
): Promise<string | ArrayBuffer | null> {
|
||||
export function readFileAsync(file: File): Promise<ArrayBuffer | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
resolve(reader.result as ArrayBuffer);
|
||||
};
|
||||
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsBinaryString(file);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function fileHash(file: File): Promise<string | null> {
|
||||
const data = await readFileAsync(file);
|
||||
if (data === null) return null;
|
||||
const hash = await crypto.subtle.digest("SHA-1", data);
|
||||
const b64Hash = btoa(
|
||||
Array.from(new Uint8Array(hash))
|
||||
.map((b) => String.fromCharCode(b))
|
||||
.join("")
|
||||
);
|
||||
return b64Hash;
|
||||
}
|
||||
|
||||
export function initWrappedMedia(): IModifiableMedia {
|
||||
return {
|
||||
file: ref<File | null>(null),
|
||||
firstHash: null,
|
||||
hash: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadWrappedMedia(
|
||||
modifiableMedia: IModifiableMedia,
|
||||
media: IMedia | null
|
||||
) {
|
||||
watch(modifiableMedia.file, async () => {
|
||||
if (modifiableMedia.file.value) {
|
||||
modifiableMedia.hash = await fileHash(modifiableMedia.file.value);
|
||||
} else {
|
||||
modifiableMedia.hash = null;
|
||||
}
|
||||
});
|
||||
try {
|
||||
modifiableMedia.file.value = await buildFileFromIMedia(media);
|
||||
} catch (e) {
|
||||
console.error("catched error while building media", e);
|
||||
}
|
||||
if (modifiableMedia.file.value) {
|
||||
modifiableMedia.firstHash = await fileHash(modifiableMedia.file.value);
|
||||
}
|
||||
}
|
||||
|
||||
export function asMediaInput(
|
||||
mmedia: IModifiableMedia,
|
||||
name: string,
|
||||
fallbackId: number
|
||||
): any {
|
||||
const ret = {
|
||||
[name]: {},
|
||||
};
|
||||
if (mmedia.file.value) {
|
||||
if (mmedia.firstHash != mmedia.hash) {
|
||||
ret[name] = {
|
||||
media: {
|
||||
name: mmedia.file.value?.name,
|
||||
alt: "",
|
||||
file: mmedia.file.value,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
ret[name] = {
|
||||
mediaId: fallbackId,
|
||||
};
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ import { IStatistics } from "../../types/statistics.model";
|
|||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(ABOUT);
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ABOUT } from "../../graphql/config";
|
||||
|
|
|
@ -14,7 +14,7 @@ import { PRIVACY } from "@/graphql/config";
|
|||
import { IConfig } from "@/types/config.model";
|
||||
import { InstancePrivacyType } from "@/types/enums";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
import { RULES } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { TERMS } from "@/graphql/config";
|
|||
import { IConfig } from "@/types/config.model";
|
||||
import { InstanceTermsType } from "@/types/enums";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ import { useQuery } from "@vue/apollo-composable";
|
|||
import { computed } from "vue";
|
||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { currentUser } = useCurrentUserClient();
|
||||
|
||||
|
|
|
@ -140,7 +140,7 @@ import { useRouter } from "vue-router";
|
|||
import { registerAccount } from "@/composition/apollo/user";
|
||||
import { convertToUsername } from "@/utils/username";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { getValueFromMeta } from "@/utils/html";
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
|
@ -224,7 +224,7 @@ import { Dialog } from "@/plugins/dialog";
|
|||
import { Notifier } from "@/plugins/notifier";
|
||||
import { AbsintheGraphQLErrors } from "@/types/errors.model";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const router = useRouter();
|
||||
|
|
|
@ -336,7 +336,7 @@ import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
|||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
|
|
|
@ -319,7 +319,7 @@ import { MemberRole } from "@/types/enums";
|
|||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
|
|
|
@ -327,7 +327,7 @@ import { ADMIN_UPDATE_USER, LANGUAGES_CODES } from "@/graphql/admin";
|
|||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { ILanguage } from "@/types/admin.model";
|
||||
import { computed, inject, reactive, ref, watch } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import { useRouter } from "vue-router";
|
||||
|
|
|
@ -90,7 +90,7 @@ import RouteName from "@/router/name";
|
|||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import NumberDashboardTile from "@/components/Dashboard/NumberDashboardTile.vue";
|
||||
import LinkedNumberDashboardTile from "@/components/Dashboard/LinkedNumberDashboardTile.vue";
|
||||
import { InstanceFilterFollowStatus } from "@/types/enums";
|
||||
|
|
|
@ -119,7 +119,7 @@ import {
|
|||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IGroup } from "@/types/actor";
|
||||
|
|
|
@ -235,7 +235,7 @@ import {
|
|||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import MastodonLogo from "@/components/Share/MastodonLogo.vue";
|
||||
|
|
|
@ -103,7 +103,7 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
|||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import {
|
||||
useRouteQuery,
|
||||
booleanTransformer,
|
||||
|
|
|
@ -58,6 +58,68 @@
|
|||
</small>
|
||||
<o-input v-model="settingsToWrite.contact" id="instance-contact" />
|
||||
</div>
|
||||
<label class="field flex flex-col">
|
||||
<p>{{ t("Logo") }}</p>
|
||||
<small>
|
||||
{{
|
||||
t(
|
||||
"Logo of the instance. Defaults to the upstream Mobilizon logo."
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<picture-upload
|
||||
v-model:modelValue="instanceLogoFile"
|
||||
:defaultImage="settingsToWrite.instanceLogo"
|
||||
:textFallback="t('Logo')"
|
||||
:maxSize="maxSize"
|
||||
/>
|
||||
</label>
|
||||
<label class="field flex flex-col">
|
||||
<p>{{ t("Favicon") }}</p>
|
||||
<small>
|
||||
{{
|
||||
t(
|
||||
"Browser tab icon and PWA icon of the instance. Defaults to the upstream Mobilizon icon."
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<picture-upload
|
||||
v-model:modelValue="instanceFaviconFile"
|
||||
:defaultImage="settingsToWrite.instanceFavicon"
|
||||
:textFallback="t('Favicon')"
|
||||
:maxSize="maxSize"
|
||||
/>
|
||||
</label>
|
||||
<label class="field flex flex-col">
|
||||
<p>{{ t("Default Picture") }}</p>
|
||||
<small>
|
||||
{{ t("Default picture when an event or group doesn't have one.") }}
|
||||
</small>
|
||||
<picture-upload
|
||||
v-model:modelValue="defaultPictureFile"
|
||||
:defaultImage="settingsToWrite.defaultPicture"
|
||||
:textFallback="t('Default Picture')"
|
||||
:maxSize="maxSize"
|
||||
/>
|
||||
</label>
|
||||
<div class="field flex flex-col">
|
||||
<label class="" for="primary-color">{{ t("Primary Color") }}</label>
|
||||
<o-input
|
||||
type="color"
|
||||
v-model="settingsToWrite.primaryColor"
|
||||
id="primary-color"
|
||||
/>
|
||||
</div>
|
||||
<div class="field flex flex-col">
|
||||
<label class="" for="secondary-color">{{
|
||||
t("Secondary Color")
|
||||
}}</label>
|
||||
<o-input
|
||||
type="color"
|
||||
v-model="settingsToWrite.secondaryColor"
|
||||
id="secondary-color"
|
||||
/>
|
||||
</div>
|
||||
<o-field :label="t('Allow registrations')">
|
||||
<o-switch v-model="settingsToWrite.registrationsOpen">
|
||||
<p
|
||||
|
@ -389,15 +451,29 @@ import RouteName from "@/router/name";
|
|||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { ref, computed, watch, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import type { Notifier } from "@/plugins/notifier";
|
||||
|
||||
// Media upload related
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import {
|
||||
initWrappedMedia,
|
||||
loadWrappedMedia,
|
||||
asMediaInput,
|
||||
} from "@/utils/image";
|
||||
import { useDefaultMaxSize } from "@/composition/config";
|
||||
|
||||
const defaultAdminSettings: IAdminSettings = {
|
||||
instanceName: "",
|
||||
instanceDescription: "",
|
||||
instanceSlogan: "",
|
||||
instanceLongDescription: "",
|
||||
contact: "",
|
||||
instanceLogo: null,
|
||||
instanceFavicon: null,
|
||||
defaultPicture: null,
|
||||
primaryColor: "",
|
||||
secondaryColor: "",
|
||||
instanceTerms: "",
|
||||
instanceTermsType: InstanceTermsType.DEFAULT,
|
||||
instanceTermsUrl: null,
|
||||
|
@ -409,12 +485,30 @@ const defaultAdminSettings: IAdminSettings = {
|
|||
instanceLanguages: [],
|
||||
};
|
||||
|
||||
const { result: adminSettingsResult } = useQuery<{
|
||||
const { onResult: onAdminSettingsResult } = useQuery<{
|
||||
adminSettings: IAdminSettings;
|
||||
}>(ADMIN_SETTINGS);
|
||||
const adminSettings = computed(
|
||||
() => adminSettingsResult.value?.adminSettings ?? defaultAdminSettings
|
||||
);
|
||||
|
||||
const adminSettings = ref<IAdminSettings>();
|
||||
|
||||
onAdminSettingsResult(async ({ data }) => {
|
||||
if (!data) return;
|
||||
adminSettings.value =
|
||||
{
|
||||
...data.adminSettings,
|
||||
} ?? defaultAdminSettings;
|
||||
|
||||
loadWrappedMedia(instanceLogo, adminSettings.value.instanceLogo);
|
||||
loadWrappedMedia(instanceFavicon, adminSettings.value.instanceFavicon);
|
||||
loadWrappedMedia(defaultPicture, adminSettings.value.defaultPicture);
|
||||
});
|
||||
|
||||
const instanceLogo = initWrappedMedia();
|
||||
const { file: instanceLogoFile } = instanceLogo;
|
||||
const instanceFavicon = initWrappedMedia();
|
||||
const { file: instanceFaviconFile } = instanceFavicon;
|
||||
const defaultPicture = initWrappedMedia();
|
||||
const { file: defaultPictureFile } = defaultPicture;
|
||||
|
||||
const { result: languageResult } = useQuery<{ languages: ILanguage[] }>(
|
||||
LANGUAGES
|
||||
|
@ -463,6 +557,9 @@ const {
|
|||
} = useMutation(SAVE_ADMIN_SETTINGS);
|
||||
|
||||
saveAdminSettingsDone(() => {
|
||||
instanceLogo.firstHash = instanceLogo.hash;
|
||||
instanceFavicon.firstHash = instanceFavicon.hash;
|
||||
defaultPicture.firstHash = defaultPicture.hash;
|
||||
notifier?.success(t("Admin settings successfully saved.") as string);
|
||||
});
|
||||
|
||||
|
@ -472,11 +569,29 @@ saveAdminSettingsError((e) => {
|
|||
});
|
||||
|
||||
const updateSettings = async (): Promise<void> => {
|
||||
const variables = { ...settingsToWrite.value };
|
||||
console.debug("updating settings with variables", variables);
|
||||
const variables = {
|
||||
...settingsToWrite.value,
|
||||
...asMediaInput(
|
||||
instanceLogo,
|
||||
"instanceLogo",
|
||||
adminSettings.value?.instanceLogo?.id
|
||||
),
|
||||
...asMediaInput(
|
||||
instanceFavicon,
|
||||
"instanceFavicon",
|
||||
adminSettings.value?.instanceFavicon?.id
|
||||
),
|
||||
...asMediaInput(
|
||||
defaultPicture,
|
||||
"defaultPicture",
|
||||
adminSettings.value?.defaultPicture?.id
|
||||
),
|
||||
};
|
||||
saveAdminSettings(variables);
|
||||
};
|
||||
|
||||
const maxSize = useDefaultMaxSize();
|
||||
|
||||
const getFilteredLanguages = (text: string): void => {
|
||||
filteredLanguages.value = languages.value
|
||||
? languages.value
|
||||
|
|
|
@ -110,7 +110,7 @@ import { useQuery } from "@vue/apollo-composable";
|
|||
import { ILanguage } from "@/types/admin.model";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ import {
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { useEventCategories } from "@/composition/apollo/config";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ import {
|
|||
import { PROFILE_CONVERSATIONS } from "@/graphql/event";
|
||||
import ConversationListItem from "../../components/Conversations/ConversationListItem.vue";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { useOruga } from "@oruga-ui/oruga-next";
|
||||
import { arrayTransformer } from "@/utils/route";
|
||||
|
|
|
@ -189,7 +189,7 @@ import {
|
|||
onMounted,
|
||||
onUnmounted,
|
||||
} from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCurrentActorClient } from "../../composition/apollo/actor";
|
||||
import { AbsintheGraphQLError } from "../../types/errors.model";
|
||||
|
|
|
@ -70,7 +70,7 @@ import { useI18n } from "vue-i18n";
|
|||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { IDiscussion } from "@/types/discussions";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { AbsintheGraphQLError } from "@/types/errors.model";
|
||||
|
||||
|
|
|
@ -172,7 +172,7 @@ import {
|
|||
computed,
|
||||
inject,
|
||||
} from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { AbsintheGraphQLError } from "@/types/errors.model";
|
||||
|
|
|
@ -83,7 +83,7 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
|||
import { usePersonStatusGroup } from "@/composition/apollo/actor";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery, integerTransformer } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ErrorCode } from "@/types/enums";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery } from "vue-use-route-query";
|
||||
import { computed } from "vue";
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
import RouteName from "@/router/name";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import EventConversations from "../../components/Conversations/EventConversations.vue";
|
||||
import NewPrivateMessage from "../../components/Participation/NewPrivateMessage.vue";
|
||||
import { useFetchEvent } from "@/composition/apollo/event";
|
||||
|
|
|
@ -635,7 +635,7 @@ import {
|
|||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useOruga } from "@oruga-ui/oruga-next";
|
||||
import type { Locale } from "date-fns";
|
||||
import sortBy from "lodash/sortBy";
|
||||
|
|
|
@ -326,7 +326,7 @@ import {
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { AbsintheGraphQLErrors } from "@/types/errors.model";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const IntegrationTwitch = defineAsyncComponent(
|
||||
() => import("@/components/Event/Integrations/TwitchIntegration.vue")
|
||||
|
|
|
@ -116,7 +116,7 @@ import {
|
|||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const EVENTS_PAGE_LIMIT = 10;
|
||||
|
|
|
@ -237,7 +237,7 @@ import {
|
|||
import { Locale } from "date-fns";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRestrictions } from "@/composition/apollo/config";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const EventParticipationCard = defineAsyncComponent(
|
||||
() => import("@/components/Event/EventParticipationCard.vue")
|
||||
|
|
|
@ -284,7 +284,7 @@ import Incognito from "vue-material-design-icons/Incognito.vue";
|
|||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import Tag from "@/components/TagElement.vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 10;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
|
|
@ -231,7 +231,7 @@ import {
|
|||
useHost,
|
||||
} from "@/composition/config";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { Openness, GroupVisibility } from "@/types/enums";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ import {
|
|||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { computed, inject } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { usePersonStatusGroup } from "@/composition/apollo/actor";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
|
|
|
@ -250,7 +250,7 @@ import {
|
|||
} from "@/graphql/member";
|
||||
import { usernameWithDomain, displayName, IGroup } from "@/types/actor";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref } from "vue";
|
||||
|
|
|
@ -208,7 +208,7 @@ import { DELETE_GROUP } from "@/graphql/group";
|
|||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
const Editor = defineAsyncComponent(
|
||||
|
|
|
@ -706,7 +706,7 @@ import AccountMultiplePlus from "vue-material-design-icons/AccountMultiplePlus.v
|
|||
import Earth from "vue-material-design-icons/Earth.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useCreateReport } from "@/composition/apollo/report";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import Discussions from "@/components/Group/Sections/DiscussionsSection.vue";
|
||||
import Resources from "@/components/Group/Sections/ResourcesSection.vue";
|
||||
import Posts from "@/components/Group/Sections/PostsSection.vue";
|
||||
|
|
|
@ -92,7 +92,7 @@ import { useMutation, useQuery } from "@vue/apollo-composable";
|
|||
import { IUser } from "@/types/current-user.model";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { computed, inject } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import RouteName from "@/router/name";
|
||||
import SettingMenuSection from "@/components/Settings/SettingMenuSection.vue";
|
||||
import SettingMenuItem from "@/components/Settings/SettingMenuItem.vue";
|
||||
|
|
|
@ -166,7 +166,7 @@ import SkeletonActivityItem from "../../components/Activity/SkeletonActivityItem
|
|||
import RouteName from "../../router/name";
|
||||
import TimelineText from "vue-material-design-icons/TimelineText.vue";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { enumTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { computed, defineAsyncComponent, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
|
|
@ -38,7 +38,7 @@ import { computed, reactive } from "vue";
|
|||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
|
|
@ -442,7 +442,7 @@ import { displayNameAndUsername, displayName } from "../../types/actor";
|
|||
import { Paginate } from "@/types/paginate";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
|
|
|
@ -95,7 +95,7 @@ import { Paginate } from "@/types/paginate";
|
|||
import debounce from "lodash/debounce";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
enumTransformer,
|
||||
|
|
|
@ -414,7 +414,7 @@ import { GraphQLError } from "graphql";
|
|||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import { useLazyQuery, useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ref, computed, inject } from "vue";
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { DEVICE_ACTIVATION } from "@/graphql/application";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import AuthorizeApplication from "@/components/OAuth/AuthorizeApplication.vue";
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</section>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
|
|
|
@ -154,7 +154,7 @@ import {
|
|||
useCurrentActorClient,
|
||||
usePersonStatusGroup,
|
||||
} from "@/composition/apollo/actor";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, inject, onMounted, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
|
|
@ -83,7 +83,7 @@ import MultiPostListItem from "../../components/Post/MultiPostListItem.vue";
|
|||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
|
|
|
@ -268,7 +268,7 @@ import { useMutation, useQuery } from "@vue/apollo-composable";
|
|||
import { computed, inject, ref } from "vue";
|
||||
import { IPost } from "@/types/post.model";
|
||||
import { DELETE_POST, FETCH_POST } from "@/graphql/post";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCreateReport } from "@/composition/apollo/report";
|
||||
|
|
|
@ -245,7 +245,7 @@ import { computed, nextTick, reactive, ref, watch } from "vue";
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useResourceProviders } from "@/composition/apollo/config";
|
||||
import Folder from "vue-material-design-icons/Folder.vue";
|
||||
import Link from "vue-material-design-icons/Link.vue";
|
||||
|
|
|
@ -770,7 +770,7 @@ import Calendar from "vue-material-design-icons/Calendar.vue";
|
|||
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
|
||||
import Magnify from "vue-material-design-icons/Magnify.vue";
|
||||
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import type { Locale } from "date-fns";
|
||||
import FilterSection from "@/components/Search/filters/FilterSection.vue";
|
||||
import { listShortDisjunctionFormatter } from "@/utils/listFormat";
|
||||
|
|
|
@ -235,7 +235,7 @@ import { useLoggedUser } from "@/composition/apollo/user";
|
|||
import { Notifier } from "@/plugins/notifier";
|
||||
import { IAuthProvider } from "@/types/enums";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { GraphQLError } from "graphql/error/GraphQLError";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
|
|
@ -82,7 +82,7 @@ import {
|
|||
REVOKED_AUTHORIZED_APPLICATION,
|
||||
} from "@/graphql/application";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import RouteName from "../../router/name";
|
||||
|
|
|
@ -339,7 +339,7 @@ import {
|
|||
} from "vue";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
|
||||
type NotificationSubType = { label: string; id: string };
|
||||
|
|
|
@ -148,7 +148,7 @@ import { AddressSearchType } from "@/types/enums";
|
|||
import { Address, IAddress } from "@/types/address.model";
|
||||
import { useTimezones } from "@/composition/apollo/config";
|
||||
import { useUserSettings, updateLocale } from "@/composition/apollo/user";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, defineAsyncComponent, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
import SettingsMenu from "../components/Settings/SettingsMenu.vue";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ import RouteName from "../../router/name";
|
|||
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const props = defineProps<{ id: string }>();
|
||||
|
|
|
@ -70,7 +70,7 @@ import { ITodoList } from "@/types/todolist";
|
|||
import RouteName from "../../router/name";
|
||||
import { useGroup } from "@/composition/apollo/group";
|
||||
import { computed, reactive } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import FullTodo from "@/components/Todo/FullTodo.vue";
|
|||
import RouteName from "../../router/name";
|
||||
import { displayName, usernameWithDomain } from "@/types/actor";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{ todoId: string }>();
|
||||
|
|
|
@ -143,7 +143,7 @@ import AuthProviders from "@/components/User/AuthProviders.vue";
|
|||
import RouteName from "@/router/name";
|
||||
import { LoginError, LoginErrorCode } from "@/types/enums";
|
||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { enumTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useLazyCurrentUserIdentities } from "@/composition/apollo/actor";
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue