Migrate to Vue 3 and Vite
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Address do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Service.Geospatial
|
||||
alias Mobilizon.Service.{Geospatial, Pictures}
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -44,7 +44,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Address do
|
||||
%{longitude: longitude, latitude: latitude, zoom: zoom, locale: locale},
|
||||
_context
|
||||
) do
|
||||
addresses = Geospatial.service().geocode(longitude, latitude, lang: locale, zoom: zoom)
|
||||
addresses =
|
||||
Geospatial.service().geocode(longitude, latitude, lang: locale, zoom: zoom)
|
||||
|> Enum.map(fn address ->
|
||||
picture_info =
|
||||
Pictures.service().search(address.locality || address.region || address.country)
|
||||
|
||||
Map.put(address, :picture_info, picture_info)
|
||||
end)
|
||||
|
||||
{:ok, addresses}
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
||||
"""
|
||||
@spec get_config(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
||||
def get_config(_parent, _params, %{context: %{ip: ip}}) do
|
||||
# ip = "2a01:e0a:184:2000:1112:e19d:9779:88c8"
|
||||
geolix = Geolix.lookup(ip)
|
||||
|
||||
country_code =
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Statistics do
|
||||
alias Mobilizon.Service.Statistics, as: StatisticsModule
|
||||
|
||||
@doc """
|
||||
Gets config.
|
||||
Gets statistics.
|
||||
"""
|
||||
@spec get_statistics(any(), any(), any()) :: {:ok, map()}
|
||||
def get_statistics(_parent, _params, _context) do
|
||||
@@ -23,4 +23,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Statistics do
|
||||
number_of_instance_followers: StatisticsModule.get_cached_value(:instance_followers)
|
||||
}}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets category statistics
|
||||
"""
|
||||
@spec get_category_statistics(any(), any(), any()) :: {:ok, list()}
|
||||
def get_category_statistics(_parent, _params, _context) do
|
||||
{:ok, StatisticsModule.category_statistics()}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +22,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
|
||||
field(:id, :id, description: "The address's ID")
|
||||
field(:origin_id, :string, description: "The address's original ID from the provider")
|
||||
field(:timezone, :string, description: "The (estimated) timezone of the location")
|
||||
field(:picture_info, :picture_info, description: "A picture associated with the address")
|
||||
end
|
||||
|
||||
@desc """
|
||||
@@ -40,6 +41,20 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
|
||||
field(:info, :string)
|
||||
end
|
||||
|
||||
object :picture_info_element do
|
||||
field(:name, :string)
|
||||
field(:url, :string)
|
||||
end
|
||||
|
||||
@desc """
|
||||
A picture associated with an address
|
||||
"""
|
||||
object :picture_info do
|
||||
field(:url, :string)
|
||||
field(:author, :picture_info_element)
|
||||
field(:source, :picture_info_element)
|
||||
end
|
||||
|
||||
@desc """
|
||||
An address input
|
||||
"""
|
||||
|
||||
@@ -26,10 +26,20 @@ defmodule Mobilizon.GraphQL.Schema.StatisticsType do
|
||||
)
|
||||
end
|
||||
|
||||
object :category_statistics do
|
||||
field(:key, :string, description: "The key for the category")
|
||||
field(:number, :integer, description: "The number of events for the given category")
|
||||
end
|
||||
|
||||
object :statistics_queries do
|
||||
@desc "Get the instance statistics"
|
||||
field :statistics, :statistics do
|
||||
resolve(&Statistics.get_statistics/3)
|
||||
end
|
||||
|
||||
@desc "Get the instance's category statistics"
|
||||
field :category_statistics, list_of(:category_statistics) do
|
||||
resolve(&Statistics.get_category_statistics/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1605,6 +1605,13 @@ defmodule Mobilizon.Events do
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def category_statistics do
|
||||
Event
|
||||
|> group_by([e], e.category)
|
||||
|> select([e], {e.category, count(e.id)})
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec list_participations_for_user_query(integer()) :: Ecto.Query.t()
|
||||
defp list_participations_for_user_query(user_id) do
|
||||
from(
|
||||
|
||||
39
lib/service/http/generic_json_client.ex
Normal file
39
lib/service/http/generic_json_client.ex
Normal file
@@ -0,0 +1,39 @@
|
||||
defmodule Mobilizon.Service.HTTP.GenericJSONClient do
|
||||
@moduledoc """
|
||||
Tesla HTTP Client that is preconfigured to get JSON content
|
||||
"""
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
@default_opts [
|
||||
recv_timeout: 20_000
|
||||
]
|
||||
|
||||
@spec client(Keyword.t()) :: Tesla.Client.t()
|
||||
def client(options \\ []) do
|
||||
headers = Keyword.get(options, :headers, [])
|
||||
adapter = Application.get_env(:tesla, __MODULE__, [])[:adapter] || Tesla.Adapter.Hackney
|
||||
opts = Keyword.merge(@default_opts, Keyword.get(options, :opts, []))
|
||||
|
||||
middleware = [
|
||||
{Tesla.Middleware.Headers,
|
||||
[{"User-Agent", Config.instance_user_agent()}] ++
|
||||
headers},
|
||||
Tesla.Middleware.FollowRedirects,
|
||||
{Tesla.Middleware.Timeout, timeout: 10_000},
|
||||
Tesla.Middleware.JSON
|
||||
]
|
||||
|
||||
Tesla.client(middleware, {adapter, opts})
|
||||
end
|
||||
|
||||
@spec get(Tesla.Client.t(), String.t()) :: Tesla.Env.result()
|
||||
def get(client, url) do
|
||||
Tesla.get(client, url)
|
||||
end
|
||||
|
||||
@spec post(Tesla.Client.t(), String.t(), map() | String.t()) :: Tesla.Env.result()
|
||||
def post(client, url, data) do
|
||||
Tesla.post(client, url, data)
|
||||
end
|
||||
end
|
||||
@@ -44,7 +44,11 @@ defmodule Mobilizon.Service.Metadata.Instance do
|
||||
Tag.tag(:meta, property: "og:description", content: description),
|
||||
Tag.tag(:meta, property: "og:type", content: "website"),
|
||||
HTML.raw(instance_json_ld)
|
||||
] ++ maybe_add_instance_feeds(Config.get([:instance, :enable_instance_feeds]))
|
||||
] ++ maybe_add_instance_feeds(enable_instance_feeds())
|
||||
end
|
||||
|
||||
defp enable_instance_feeds do
|
||||
get_in(Application.get_env(:mobilizon, :instance), [:enable_instance_feeds])
|
||||
end
|
||||
|
||||
@spec maybe_add_instance_feeds(boolean()) :: list()
|
||||
|
||||
3
lib/service/pictures/information.ex
Normal file
3
lib/service/pictures/information.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule Mobilizon.Service.Pictures.Information do
|
||||
defstruct [:url, :author, :source]
|
||||
end
|
||||
17
lib/service/pictures/pictures.ex
Normal file
17
lib/service/pictures/pictures.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule Mobilizon.Service.Pictures do
|
||||
@moduledoc """
|
||||
Module to load the service adapter defined inside the configuration.
|
||||
|
||||
See `Mobilizon.Service.Pictures.Provider`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns the appropriate service adapter.
|
||||
|
||||
According to the config behind
|
||||
`config :mobilizon, Mobilizon.Service.Pictures,
|
||||
service: Mobilizon.Service.Pictures.Module`
|
||||
"""
|
||||
@spec service :: module
|
||||
def service, do: get_in(Application.get_env(:mobilizon, __MODULE__), [:service])
|
||||
end
|
||||
39
lib/service/pictures/provider.ex
Normal file
39
lib/service/pictures/provider.ex
Normal file
@@ -0,0 +1,39 @@
|
||||
defmodule Mobilizon.Service.Pictures.Provider do
|
||||
@moduledoc """
|
||||
Provider Behaviour for pictures stuff.
|
||||
|
||||
## Supported backends
|
||||
|
||||
* `Mobilizon.Service.Pictures.Unsplash` [🔗](https://unsplash.com/developers)
|
||||
* `Mobilizon.Service.Pictures.Flickr` [🔗](https://www.flickr.com/services/api/)
|
||||
|
||||
## Shared options
|
||||
|
||||
* `:lang` Lang in which to prefer results. Used as a request parameter or
|
||||
through an `Accept-Language` HTTP header. Defaults to `"en"`.
|
||||
* `:country_code` An ISO 3166 country code. String or `nil`
|
||||
* `:limit` Maximum limit for the number of results returned by the backend.
|
||||
Defaults to `10`
|
||||
* `:api_key` Allows to override the API key (if the backend requires one) set
|
||||
inside the configuration.
|
||||
* `:endpoint` Allows to override the endpoint set inside the configuration.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.Pictures.Information
|
||||
|
||||
@doc """
|
||||
Get a picture for a location
|
||||
|
||||
## Examples
|
||||
|
||||
iex> search("London")
|
||||
%Information{url: "https://some_url_to.a/picture.jpeg", author: %{name: "An author", url: "https://url.to/profile"}, source: %{name: "The source name", url: "The source URL" }}
|
||||
"""
|
||||
@callback search(location :: String.t(), options :: keyword) ::
|
||||
[Information.t()]
|
||||
|
||||
@spec endpoint(atom()) :: String.t()
|
||||
def endpoint(provider) do
|
||||
Application.get_env(:mobilizon, provider) |> get_in([:endpoint])
|
||||
end
|
||||
end
|
||||
65
lib/service/pictures/unsplash.ex
Normal file
65
lib/service/pictures/unsplash.ex
Normal file
@@ -0,0 +1,65 @@
|
||||
defmodule Mobilizon.Service.Pictures.Unsplash do
|
||||
@moduledoc """
|
||||
[Unsplash](https://unsplash.com) backend.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.Pictures.{Information, Provider}
|
||||
alias Mobilizon.Service.HTTP.GenericJSONClient
|
||||
require Logger
|
||||
|
||||
@unsplash_api "/search/photos"
|
||||
@unsplash_name "Unsplash"
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@impl Provider
|
||||
@doc """
|
||||
Unsplash implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||
"""
|
||||
@spec search(String.t(), keyword()) :: list(Information.t())
|
||||
def search(location, _options \\ []) do
|
||||
url = "#{unsplash_endpoint()}#{@unsplash_api}?query=#{location}&orientation=landscape"
|
||||
|
||||
client =
|
||||
GenericJSONClient.client(headers: [{:Authorization, "Client-ID #{unsplash_access_key()}"}])
|
||||
|
||||
with {:ok, %{status: 200, body: body}} <- GenericJSONClient.get(client, url),
|
||||
selectedPicture <- Enum.random(body["results"]) do
|
||||
%Information{
|
||||
url: selectedPicture["urls"]["small"],
|
||||
author: %{
|
||||
name: selectedPicture["user"]["name"],
|
||||
url: "#{selectedPicture["user"]["links"]["html"]}#{unsplash_utm_source()}"
|
||||
},
|
||||
source: %{
|
||||
name: @unsplash_name,
|
||||
url: unsplash_url()
|
||||
}
|
||||
}
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp unsplash_app_name do
|
||||
Application.get_env(:mobilizon, __MODULE__) |> get_in([:app_name])
|
||||
end
|
||||
|
||||
defp unsplash_utm_source do
|
||||
"?utm_source=#{unsplash_app_name()}&utm_medium=referral"
|
||||
end
|
||||
|
||||
defp unsplash_url do
|
||||
"https://unsplash.com/#{unsplash_utm_source()}"
|
||||
end
|
||||
|
||||
defp unsplash_endpoint do
|
||||
Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint]) ||
|
||||
"https://api.unsplash.com"
|
||||
end
|
||||
|
||||
defp unsplash_access_key do
|
||||
Application.get_env(:mobilizon, __MODULE__) |> get_in([:access_key])
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ defmodule Mobilizon.Service.Statistics do
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Discussions, Events, Users}
|
||||
alias Mobilizon.Events.Categories
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
|
||||
@spec get_cached_value(String.t()) :: any() | nil
|
||||
@@ -60,4 +61,23 @@ defmodule Mobilizon.Service.Statistics do
|
||||
relay_actor = Relay.get_actor()
|
||||
Actors.count_followings_for_actor(relay_actor)
|
||||
end
|
||||
|
||||
@spec category_statistics :: list({String.t(), non_neg_integer()})
|
||||
def category_statistics do
|
||||
case Cachex.fetch(:statistics, :categories, fn ->
|
||||
allowed_categories =
|
||||
Categories.list()
|
||||
|> Enum.map(fn %{id: category} -> category |> Atom.to_string() |> String.upcase() end)
|
||||
|
||||
statistics =
|
||||
Events.category_statistics()
|
||||
|> Enum.filter(fn {category, _} -> category in allowed_categories end)
|
||||
|> Enum.map(fn {category, number} -> %{key: category, number: number} end)
|
||||
|
||||
{:commit, statistics}
|
||||
end) do
|
||||
{status, value} when status in [:ok, :commit] -> value
|
||||
_err -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
24
lib/web/templates/page/index.html.heex
Normal file
24
lib/web/templates/page/index.html.heex
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang={assigns.locale} dir={language_direction(assigns)}>
|
||||
<head>
|
||||
<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="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={theme_color()}>
|
||||
<meta name="theme-color" content={theme_color()}>
|
||||
<%= tags(assigns) || assigns.tags %>
|
||||
<%= Vite.inlined_phx_manifest() %>
|
||||
<%= Vite.vite_client() %>
|
||||
<%= Vite.vite_snippet("src/main.ts") %>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>
|
||||
We're sorry but Mobilizon doesn't work properly without JavaScript enabled. Please enable it to continue.
|
||||
</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,15 +6,22 @@ defmodule Mobilizon.Web.AuthView do
|
||||
use Mobilizon.Web, :view
|
||||
alias Mobilizon.Service.Metadata.Instance
|
||||
alias Phoenix.HTML.Tag
|
||||
import Mobilizon.Web.Views.Utils
|
||||
|
||||
@spec render(String.t(), map()) :: String.t() | Plug.Conn.t()
|
||||
def render("callback.html", %{
|
||||
conn: conn,
|
||||
access_token: access_token,
|
||||
refresh_token: refresh_token,
|
||||
user: %{id: user_id, email: user_email, role: user_role, default_actor_id: user_actor_id}
|
||||
}) do
|
||||
def render(
|
||||
"callback.html",
|
||||
%{
|
||||
conn: _conn,
|
||||
access_token: access_token,
|
||||
refresh_token: refresh_token,
|
||||
user: %{
|
||||
id: user_id,
|
||||
email: user_email,
|
||||
role: user_role,
|
||||
default_actor_id: user_actor_id
|
||||
}
|
||||
} = assigns
|
||||
) do
|
||||
info_tags = [
|
||||
Tag.tag(:meta, name: "auth-access-token", content: access_token),
|
||||
Tag.tag(:meta, name: "auth-refresh-token", content: refresh_token),
|
||||
@@ -25,11 +32,8 @@ defmodule Mobilizon.Web.AuthView do
|
||||
]
|
||||
|
||||
with tags <- Instance.build_tags() ++ info_tags,
|
||||
{:ok, html} <- inject_tags(tags, get_locale(conn)) do
|
||||
html
|
||||
else
|
||||
{:error, error} ->
|
||||
return_error(conn, error)
|
||||
assigns <- Map.put(assigns, :tags, tags) do
|
||||
render("index.html", assigns)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,18 +3,10 @@ defmodule Mobilizon.Web.ErrorView do
|
||||
View for errors
|
||||
"""
|
||||
use Mobilizon.Web, :view
|
||||
alias Mobilizon.Service.Metadata.Instance
|
||||
import Mobilizon.Web.Views.Utils
|
||||
|
||||
@spec render(String.t(), map()) :: map() | String.t() | Plug.Conn.t()
|
||||
def render("404.html", %{conn: conn}) do
|
||||
with tags <- Instance.build_tags(),
|
||||
{:ok, html} <- inject_tags(tags, get_locale(conn)) do
|
||||
html
|
||||
else
|
||||
{:error, error} ->
|
||||
return_error(conn, error)
|
||||
end
|
||||
def render("404.html", _assigns) do
|
||||
render("index.html")
|
||||
end
|
||||
|
||||
def render("404.json", _assigns) do
|
||||
|
||||
@@ -63,28 +63,27 @@ defmodule Mobilizon.Web.PageView do
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render(page, %{object: object, conn: conn} = _assigns)
|
||||
def render(page, %{object: object, conn: conn} = assigns)
|
||||
when page in ["actor.html", "event.html", "comment.html", "post.html"] do
|
||||
with locale <- get_locale(conn),
|
||||
tags <- object |> Metadata.build_tags(locale),
|
||||
{:ok, html} <- inject_tags(tags, locale) do
|
||||
html
|
||||
else
|
||||
{:error, error} ->
|
||||
return_error(conn, error)
|
||||
assigns <- Map.put(assigns, :tags, tags) do
|
||||
render("index.html", assigns)
|
||||
end
|
||||
end
|
||||
|
||||
# Discussions are private, no need to embed metadata
|
||||
def render("discussion.html", params), do: render("index.html", params)
|
||||
|
||||
def render("index.html", %{conn: conn}) do
|
||||
with tags <- Instance.build_tags(),
|
||||
{:ok, html} <- inject_tags(tags, get_locale(conn)) do
|
||||
html
|
||||
else
|
||||
{:error, error} ->
|
||||
return_error(conn, error)
|
||||
end
|
||||
def tags(assigns) do
|
||||
Map.get(assigns, :tags, Instance.build_tags())
|
||||
end
|
||||
|
||||
def theme_color do
|
||||
"#ffd599"
|
||||
end
|
||||
|
||||
def language_direction(assigns) do
|
||||
get_language_direction(assigns.locale)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,58 +3,6 @@ defmodule Mobilizon.Web.Views.Utils do
|
||||
Utils for views
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.Metadata.Utils, as: MetadataUtils
|
||||
import Mobilizon.Web.Gettext, only: [dgettext: 2]
|
||||
import Plug.Conn, only: [put_status: 2, halt: 1]
|
||||
|
||||
# sobelow_skip ["Traversal.FileModule"]
|
||||
@spec inject_tags(Enum.t(), String.t()) :: {:ok, {:safe, String.t()}} | {:error, atom()}
|
||||
def inject_tags(tags, locale \\ "en") do
|
||||
with path <- Path.join(Application.app_dir(:mobilizon, "priv/static"), "index.html"),
|
||||
{:exists, true} <- {:exists, File.exists?(path)},
|
||||
{:ok, index_content} <- File.read(path),
|
||||
safe <- do_replacements(index_content, MetadataUtils.stringify_tags(tags), locale) do
|
||||
{:ok, {:safe, safe}}
|
||||
else
|
||||
{:exists, false} -> {:error, :index_not_found}
|
||||
{:error, error} when is_atom(error) -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec return_error(Plug.Conn.t(), atom()) :: Plug.Conn.t()
|
||||
def return_error(conn, error) do
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> Phoenix.Controller.put_view(Mobilizon.Web.ErrorView)
|
||||
|> Phoenix.Controller.render("500.html", %{details: details(error)})
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp details(:index_not_found) do
|
||||
[dgettext("errors", "Index file not found. You need to recompile the front-end.")]
|
||||
end
|
||||
|
||||
defp details(_) do
|
||||
[]
|
||||
end
|
||||
|
||||
@spec replace_meta(String.t(), String.t()) :: String.t()
|
||||
defp replace_meta(index_content, tags) do
|
||||
index_content
|
||||
|> String.replace("<meta name=\"server-injected-data\"/>", tags)
|
||||
|> String.replace("<meta name=\"server-injected-data\" />", tags)
|
||||
end
|
||||
|
||||
@spec do_replacements(String.t(), String.t(), String.t()) :: String.t()
|
||||
defp do_replacements(index_content, tags, locale) do
|
||||
index_content
|
||||
|> replace_meta(tags)
|
||||
|> String.replace(
|
||||
~s(<html lang="en" dir="auto">),
|
||||
~s(<html lang="#{locale}" dir="#{get_language_direction(locale)}">)
|
||||
)
|
||||
end
|
||||
|
||||
@spec get_locale(Plug.Conn.t()) :: String.t()
|
||||
def get_locale(%Plug.Conn{assigns: assigns}) do
|
||||
Map.get(assigns, :locale)
|
||||
@@ -63,6 +11,6 @@ defmodule Mobilizon.Web.Views.Utils do
|
||||
@ltr_languages ["ar", "ae", "he", "fa", "ku", "ur"]
|
||||
|
||||
@spec get_language_direction(String.t()) :: String.t()
|
||||
defp get_language_direction(locale) when locale in @ltr_languages, do: "rtl"
|
||||
defp get_language_direction(_locale), do: "ltr"
|
||||
def get_language_direction(locale) when locale in @ltr_languages, do: "rtl"
|
||||
def get_language_direction(_locale), do: "ltr"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user