Introduce group basic federation, event new page and notifications

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-02-18 08:57:00 +01:00
parent 300ef8f245
commit 4144e9ffd0
416 changed files with 32220 additions and 16750 deletions

View File

@@ -3,12 +3,13 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
ActivityPub related cache.
"""
alias Mobilizon.{Actors, Events, Tombstone}
alias Mobilizon.{Actors, Conversations, Events, Resources, Todos, Tombstone}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes
@@ -60,7 +61,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
{:commit, Comment.t()} | {:ignore, nil}
def get_comment_by_uuid_with_preload(uuid) do
Cachex.fetch(@cache, "comment_" <> uuid, fn "comment_" <> uuid ->
case Events.get_comment_from_uuid_with_preload(uuid) do
case Conversations.get_comment_from_uuid_with_preload(uuid) do
%Comment{} = comment ->
{:commit, comment}
@@ -70,6 +71,57 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
end)
end
@doc """
Gets a resource by its UUID, with all associations loaded.
"""
@spec get_resource_by_uuid_with_preload(String.t()) ::
{:commit, Resource.t()} | {:ignore, nil}
def get_resource_by_uuid_with_preload(uuid) do
Cachex.fetch(@cache, "resource_" <> uuid, fn "resource_" <> uuid ->
case Resources.get_resource_with_preloads(uuid) do
%Resource{} = resource ->
{:commit, resource}
nil ->
{:ignore, nil}
end
end)
end
@doc """
Gets a todo list by its UUID, with all associations loaded.
"""
@spec get_todo_list_by_uuid_with_preload(String.t()) ::
{:commit, TodoList.t()} | {:ignore, nil}
def get_todo_list_by_uuid_with_preload(uuid) do
Cachex.fetch(@cache, "todo_list_" <> uuid, fn "todo_list_" <> uuid ->
case Todos.get_todo_list(uuid) do
%TodoList{} = todo_list ->
{:commit, todo_list}
nil ->
{:ignore, nil}
end
end)
end
@doc """
Gets a todo by its UUID, with all associations loaded.
"""
@spec get_todo_by_uuid_with_preload(String.t()) ::
{:commit, Todo.t()} | {:ignore, nil}
def get_todo_by_uuid_with_preload(uuid) do
Cachex.fetch(@cache, "todo_" <> uuid, fn "todo_" <> uuid ->
case Todos.get_todo(uuid) do
%Todo{} = todo ->
{:commit, todo}
nil ->
{:ignore, nil}
end
end)
end
@doc """
Gets a relay.
"""

View File

@@ -20,5 +20,8 @@ defmodule Mobilizon.Web.Cache do
defdelegate get_local_actor_by_name(name), to: ActivityPub
defdelegate get_public_event_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_comment_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_relay, to: ActivityPub
end

View File

@@ -67,6 +67,47 @@ defmodule Mobilizon.Web.ActivityPubController do
end
end
def members(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(
ActorView.render("members.json", %{
group: group,
page: page,
actor_applicant: Map.get(conn.assigns, :actor)
})
)
end
end
def members(conn, %{"name" => name}) do
with %Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(
ActorView.render("members.json", %{
group: group,
actor_applicant: Map.get(conn.assigns, :actor)
})
)
end
end
def resources(conn, %{"name" => name}) do
with %Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(
ActorView.render("resources.json", %{
group: group,
actor_applicant: Map.get(conn.assigns, :actor)
})
)
end
end
def outbox(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name(name) do

View File

@@ -4,31 +4,65 @@ defmodule Mobilizon.Web.PageController do
"""
use Mobilizon.Web, :controller
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Tombstone
alias Mobilizon.Web.Cache
alias Mobilizon.Web.{ActivityPubController, Cache}
plug(:put_layout, false)
action_fallback(Mobilizon.Web.FallbackController)
@spec index(Plug.Conn.t(), any) :: Plug.Conn.t()
def index(conn, _params), do: render(conn, :index)
@spec actor(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t()
def actor(conn, %{"name" => name}) do
{status, actor} = Cache.get_local_actor_by_name(name)
render_or_error(conn, &ok_status?/3, status, :actor, actor)
end
@spec event(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t()
def event(conn, %{"uuid" => uuid}) do
{status, event} = Cache.get_public_event_by_uuid_with_preload(uuid)
render_or_error(conn, &checks?/3, status, :event, event)
end
@spec comment(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t()
def comment(conn, %{"uuid" => uuid}) do
{status, comment} = Cache.get_comment_by_uuid_with_preload(uuid)
render_or_error(conn, &checks?/3, status, :comment, comment)
end
@spec resource(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found}
def resource(conn, %{"uuid" => uuid}) do
{status, resource} = Cache.get_resource_by_uuid_with_preload(uuid)
render_or_error(conn, &checks?/3, status, :resource, resource)
end
def resources(conn, %{"name" => _name}) do
case get_format(conn) do
"html" ->
render(conn, :index)
"activity-json" ->
ActivityPubController.call(conn, :resources)
end
end
@spec todo_list(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t()
def todo_list(conn, %{"uuid" => uuid}) do
{status, todo_list} = Cache.get_todo_list_by_uuid_with_preload(uuid)
render_or_error(conn, &checks?/3, status, :todo_list, todo_list)
end
@spec todo(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t()
def todo(conn, %{"uuid" => uuid}) do
{status, todo} = Cache.get_todo_by_uuid_with_preload(uuid)
render_or_error(conn, &checks?/3, status, :todo, todo)
end
@spec interact(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found}
def interact(conn, %{"uri" => uri}) do
case ActivityPub.fetch_object_from_url(uri) do
{:ok, %Event{uuid: uuid}} -> redirect(conn, to: "/events/#{uuid}")
@@ -60,6 +94,7 @@ defmodule Mobilizon.Web.PageController do
defp is_visible?(%{visibility: v}), do: v in [:public, :unlisted]
defp is_visible?(%Tombstone{}), do: true
defp is_visible?(_), do: true
defp ok_status?(status), do: status in [:ok, :commit]
defp ok_status?(_conn, status, _), do: ok_status?(status)
@@ -75,6 +110,6 @@ defmodule Mobilizon.Web.PageController do
end
end
defp is_local?(%Event{local: local}), do: if(local, do: true, else: :remote)
defp is_local?(%Comment{local: local}), do: if(local, do: true, else: :remote)
defp is_local?(%{local: local}), do: if(local, do: true, else: :remote)
defp is_local?(_), do: false
end

View File

@@ -19,7 +19,7 @@ defmodule Mobilizon.Web.Email.Event do
@important_changes [:title, :begins_on, :ends_on, :status]
@spec event_updated(User.t(), Actor.t(), Event.t(), Event.t(), list(), String.t()) ::
@spec event_updated(User.t(), Actor.t(), Event.t(), Event.t(), MapSet.t(), String.t()) ::
Bamboo.Email.t()
def event_updated(
%User{} = user,
@@ -82,4 +82,12 @@ defmodule Mobilizon.Web.Email.Event do
|> Email.Event.event_updated(actor, old_event, event, diff, locale)
|> Email.Mailer.deliver_later()
end
defp send_notification_for_event_update_to_participant(user, old_event, new_event, diff) do
require Logger
Logger.error(inspect(user))
Logger.error(inspect(old_event))
Logger.error(inspect(new_event))
Logger.error(inspect(diff))
end
end

47
lib/web/email/group.ex Normal file
View File

@@ -0,0 +1,47 @@
defmodule Mobilizon.Web.Email.Group do
@moduledoc """
Handles emails sent about participation.
"""
use Bamboo.Phoenix, view: Mobilizon.Web.EmailView
import Bamboo.Phoenix
import Mobilizon.Web.Gettext
alias Mobilizon.{Actors, Users}
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Users.User
alias Mobilizon.Web.{Email, Gettext}
@doc """
Send emails to local user
"""
def send_invite_to_user(
%Member{actor: %Actor{user_id: user_id}, parent: %Actor{} = group, role: :invited} =
member,
locale \\ "en"
) do
with %User{email: email} <- Users.get_user!(user_id) do
Gettext.put_locale(locale)
%Actor{name: invited_by_name} = inviter = Actors.get_actor(member.invited_by_id)
subject =
gettext(
"You have been invited by %{inviter} to join group %{group}",
inviter: invited_by_name,
group: group.name
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:inviter, inviter)
|> assign(:group, group)
|> assign(:subject, subject)
|> render(:group_invite)
|> Email.Mailer.deliver_later()
:ok
end
end
# TODO : def send_confirmation_to_inviter()
end

View File

@@ -0,0 +1,59 @@
defmodule Mobilizon.Web.Email.Notification do
@moduledoc """
Handles emails sent about event notifications.
"""
use Bamboo.Phoenix, view: Mobilizon.Web.EmailView
import Bamboo.Phoenix
import Mobilizon.Web.Gettext
alias Mobilizon.Events.Participant
alias Mobilizon.Users.{Setting, User}
alias Mobilizon.Web.{Email, Gettext}
@spec before_event_notification(String.t(), Participant.t(), String.t()) ::
Bamboo.Email.t()
def before_event_notification(
email,
%Participant{event: event, role: :participant} = participant,
locale \\ "en"
) do
Gettext.put_locale(locale)
subject =
gettext(
"Don't forget to go to %{title}",
title: event.title
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:participant, participant)
|> assign(:subject, subject)
|> render(:before_event_notification)
end
def on_day_notification(
%User{email: email, settings: %Setting{timezone: timezone}},
participations,
total,
locale \\ "en"
) do
Gettext.put_locale(locale)
participation = hd(participations)
subject =
ngettext("One event planned today", "%{nb_events} events planned today", total,
nb_events: total
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:participation, participation)
|> assign(:participations, participations)
|> assign(:total, total)
|> assign(:timezone, timezone)
|> assign(:subject, subject)
|> render(:on_day_notification)
end
end

View File

@@ -28,9 +28,11 @@ defmodule Mobilizon.Web.Endpoint do
plug(
Plug.Static,
at: "/",
from: :mobilizon,
from: {:mobilizon, "priv/static"},
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
only:
~w(index.html manifest.json service-worker.js css fonts images js favicon.ico robots.txt),
only_matching: ["precache-manifest"]
)
# Code reloading can be explicitly enabled under the

View File

@@ -30,6 +30,10 @@ defmodule Mobilizon.Web.Router do
pipeline :activity_pub_and_html do
plug(:accepts, ["html", "activity-json"])
plug(Cldr.Plug.AcceptLanguage,
cldr_backend: Mobilizon.Cldr
)
end
pipeline :atom_and_ical do
@@ -38,6 +42,11 @@ defmodule Mobilizon.Web.Router do
pipeline :browser do
plug(Plug.Static, at: "/", from: "priv/static")
plug(Cldr.Plug.AcceptLanguage,
cldr_backend: Mobilizon.Cldr
)
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
@@ -72,8 +81,13 @@ defmodule Mobilizon.Web.Router do
pipe_through(:activity_pub_signature)
get("/@:name", PageController, :actor)
get("/events/me", PageController, :index)
get("/events/:uuid", PageController, :event)
get("/comments/:uuid", PageController, :comment)
get("/resource/:uuid", PageController, :resource, as: "resource")
get("/todo-list/:uuid", PageController, :todo_list, as: "todo_list")
get("/todo/:uuid", PageController, :todo, as: "todo")
get("/@:name/resources", PageController, :resources)
end
scope "/", Mobilizon.Web do
@@ -82,6 +96,8 @@ defmodule Mobilizon.Web.Router do
get("/@:name/outbox", ActivityPubController, :outbox)
get("/@:name/following", ActivityPubController, :following)
get("/@:name/followers", ActivityPubController, :followers)
get("/@:name/members", ActivityPubController, :members)
get("/@:name/todo-lists", ActivityPubController, :todo_lists)
end
scope "/", Mobilizon.Web do
@@ -131,6 +147,7 @@ defmodule Mobilizon.Web.Router do
)
get("/validate/email/:token", PageController, :index, as: "user_email_validation")
get("/groups/me", PageController, :index, as: "my_groups")
get("/interact", PageController, :interact)
end

View File

@@ -0,0 +1,74 @@
<!-- HERO -->
<tr>
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Upcoming event" %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= gettext "Get ready for %{title}", title: @participant.event.title %>
</p>
</td>
</tr>
<!-- BULLETPROOF BUTTON -->
<tr>
<td bgcolor="#ffffff" align="left">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#424056"><a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @participant.event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #424056; display: inline-block;">
<%= gettext "Go to event page" %>
</a></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 20px;" >
<p style="margin: 0">
<%= gettext "If you need to cancel your participation, just access the event page through link above and click on the participation button." %>
</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View File

@@ -0,0 +1,5 @@
<%= gettext "Upcoming event" %>
==
<%= gettext "Get ready for %{title}", title: @participant.event.title %>
<%= gettext "View the event on: %{link}", link: page_url(Mobilizon.Web.Endpoint, :event, @participant.event.uuid) %>
<%= gettext "If you need to cancel your participation, just access the event page through link above and click on the participation button." %>

View File

@@ -121,4 +121,4 @@
</table>
<![endif]-->
</td>
</tr>
</tr>

View File

@@ -4,6 +4,17 @@
<%= gettext "The event %{title} was just updated", title: @old_event.title %>
<%= if MapSet.member?(@changes, :status) do %>
<%= case @event.status do %>
<% :confirmed -> %>
<%= gettext "Event has been confirmed" %>
<% :tentative -> %>
<%= gettext "Event status has been set as tentative" %>
<% :cancelled -> %>
<%= gettext "Event has been cancelled" %>
<% end %>
<% end %>
<%= if MapSet.member?(@changes, :title) do %>
<%= gettext "New title: %{title}", title: @event.title %>
<% end %>

View File

@@ -0,0 +1,76 @@
<!-- HERO -->
<tr>
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Come along!" %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= gettext "%{inviter} just invited you to join their group %{group}", group: @group.name, inviter: @inviter.name %>
</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0">
<%= gettext "To accept this invitation, head over to your groups." %>
</p>
</td>
</tr>
<!-- BULLETPROOF BUTTON -->
<tr>
<td bgcolor="#ffffff" align="left">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#424056">
<a href="<%= my_groups_url(Mobilizon.Web.Endpoint, :index) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #424056; display: inline-block;">
<%= gettext "See my groups" %>
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View File

@@ -0,0 +1,9 @@
<%= gettext "Come along!" %>
==
<%= gettext "%{inviter} just invited you to join their group %{group}", group: @group.name, inviter: @inviter.name %>
<%= gettext "To accept this invitation, head over to your groups." %>
<%= my_groups_url(Mobilizon.Web.Endpoint, :index) %>

View File

@@ -0,0 +1,81 @@
<!-- HERO -->
<tr>
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Events today" %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= ngettext "You have one event today:", "You have %{total} events today:", @total, total: @total %>
</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<%= if @total > 1 do %>
<ul style="margin: 0;">
<%= for participation <- @participations do %>
<li>
<strong>
<%= participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_time_string(@locale) %>
</strong>
<a href="<%= page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %>" target="_blank">
<%= participation.event.title %>
</a>
</li>
<% end %>
</ul>
<% else %>
<strong>
<%= @participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_time_string(@locale) %>
</strong>
<a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %>" target="_blank">
<%= @participation.event.title %>
</a>
<% end %>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 20px;" >
<p style="margin: 0">
<%= ngettext "If you need to cancel your participation, just access the event page through the link above and click on the participation button.", "If you need to cancel your participation, just access the event page through the links above and click on the participation button.", @total %>
</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View File

@@ -0,0 +1,14 @@
<%= gettext "Events today" %>
==
<%= ngettext "You have one event today:", "You have %{total} events today:", @total, total: @total %>
<%= if @total > 1 do %>
<%= for participation <- @participations do %>
- <%= participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_time_string(@locale) %> - <%= participation.event.title %> <%= page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %>
<% end %>
<% else %>
<%= DateTime.shift_zone!(@participation.event.begins_on, @timezone) |> datetime_to_time_string(@locale) %> - <%= @participation.event.title %> <%= page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %>
<% end %>
<%= ngettext "If you need to cancel your participation, just access the event page through the link above and click on the participation button.", "If you need to cancel your participation, just access the event page through the links above and click on the participation button.", @total %>

View File

@@ -2,7 +2,9 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
use Mobilizon.Web, :view
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Resources
alias Mobilizon.Resources.Resource
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
@@ -60,7 +62,7 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
else: @private_visibility_empty_collection
%{
"id" => Actor.build_url(actor.preferred_username, :followers),
"id" => actor.followers_url,
"type" => "OrderedCollection",
"totalItems" => total,
"first" => collection(followers, actor.preferred_username, :followers, 1, total)
@@ -68,6 +70,64 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
|> Map.merge(Utils.make_json_ld_header())
end
def render("members.json", %{group: group, page: page, actor_applicant: actor_applicant}) do
%{total: total, elements: members} =
if Actor.is_public_visibility(group) ||
actor_applicant_group_member?(group, actor_applicant),
do: Actors.list_members_for_group(group, page),
else: @private_visibility_empty_collection
members
|> collection(group.preferred_username, :members, page, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("members.json", %{group: group, actor_applicant: actor_applicant}) do
%{total: total, elements: members} =
if Actor.is_public_visibility(group) ||
actor_applicant_group_member?(group, actor_applicant),
do: Actors.list_members_for_group(group),
else: @private_visibility_empty_collection
%{
"id" => group.url,
"attributedTo" => group.url,
"type" => "OrderedCollection",
"totalItems" => total,
"first" => collection(members, group.preferred_username, :members, 1, total)
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("resources.json", %{group: group, page: page, actor_applicant: actor_applicant}) do
%{total: total, elements: resources} =
if Actor.is_public_visibility(group) ||
actor_applicant_group_member?(group, actor_applicant),
do: Resources.get_top_level_resources_for_group(group),
else: @private_visibility_empty_collection
resources
|> collection(group.preferred_username, :resources, page, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("resources.json", %{group: group, actor_applicant: actor_applicant}) do
%{total: total, elements: resources} =
if Actor.is_public_visibility(group) ||
actor_applicant_group_member?(group, actor_applicant),
do: Resources.get_top_level_resources_for_group(group),
else: @private_visibility_empty_collection
%{
"id" => group.resources_url,
"attributedTo" => group.url,
"type" => "OrderedCollection",
"totalItems" => total,
"first" => collection(resources, group.preferred_username, :resources, 1, total)
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("outbox.json", %{actor: actor, page: page}) do
%{total: total, elements: followers} =
if Actor.is_public_visibility(actor),
@@ -96,11 +156,12 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
@spec collection(list(), String.t(), atom(), integer(), integer()) :: map()
defp collection(collection, preferred_username, endpoint, page, total)
when endpoint in [:followers, :following, :outbox] do
when endpoint in [:followers, :following, :outbox, :members, :resources, :todos] do
offset = (page - 1) * 10
map = %{
"id" => Actor.build_url(preferred_username, endpoint, page: page),
"attributedTo" => Actor.build_url(preferred_username, :page),
"type" => "OrderedCollectionPage",
"partOf" => Actor.build_url(preferred_username, endpoint),
"orderedItems" => Enum.map(collection, &item/1)
@@ -115,4 +176,17 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
def item(%Activity{data: %{"id" => id}}), do: id
def item(%Actor{url: url}), do: url
def item(%Member{} = member), do: Convertible.model_to_as(member)
def item(%Resource{} = resource), do: Convertible.model_to_as(resource)
defp actor_applicant_group_member?(%Actor{}, nil), do: false
defp actor_applicant_group_member?(%Actor{id: group_id}, %Actor{id: actor_applicant_id}),
do:
Actors.get_member(actor_applicant_id, group_id, [
:member,
:moderator,
:administrator,
:creator
]) != {:error, :member_not_found}
end

View File

@@ -3,9 +3,16 @@ defmodule Mobilizon.Web.EmailView do
import Mobilizon.Web.Gettext
def datetime_to_string(%DateTime{} = datetime, locale \\ "en") do
def datetime_to_string(%DateTime{} = datetime, locale \\ "en", format \\ :medium) do
with {:ok, string} <-
Cldr.DateTime.to_string(datetime, Mobilizon.Cldr, format: :medium, locale: locale) do
Mobilizon.Cldr.DateTime.to_string(datetime, format: format, locale: locale) do
string
end
end
def datetime_to_time_string(%DateTime{} = datetime, locale \\ "en", format \\ :hm) do
with {:ok, string} <-
Mobilizon.Cldr.DateTime.to_string(datetime, format: format, locale: locale) do
string
end
end

View File

@@ -6,7 +6,9 @@ defmodule Mobilizon.Web.PageView do
use Mobilizon.Web, :view
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Resources.Resource
alias Mobilizon.Tombstone
alias Mobilizon.Service.Metadata
@@ -40,35 +42,56 @@ defmodule Mobilizon.Web.PageView do
|> Map.merge(Utils.make_json_ld_header())
end
def render(page, %{object: object} = _assigns)
def render("resource.activity-json", %{conn: %{assigns: %{object: %Resource{} = resource}}}) do
resource
|> Convertible.model_to_as()
|> Map.merge(Utils.make_json_ld_header())
end
def render(page, %{object: object, conn: conn} = _assigns)
when page in ["actor.html", "event.html", "comment.html"] do
tags = object |> Metadata.build_tags()
inject_tags(conn, tags)
end
def render("index.html", %{conn: conn}) do
tags = Instance.build_tags()
inject_tags(conn, tags)
end
@spec inject_tags(Conn.t(), List.t()) :: {:safe, String.t()}
defp inject_tags(conn, tags) do
with {:ok, index_content} <- File.read(index_file_path()) do
tags = object |> Metadata.build_tags() |> MetadataUtils.stringify_tags()
locale = get_locale(conn)
index_content = replace_meta(index_content, tags)
{:safe, index_content}
end
end
def render("index.html", _assigns) do
with {:ok, index_content} <- File.read(index_file_path()) do
tags = Instance.build_tags() |> MetadataUtils.stringify_tags()
index_content = replace_meta(index_content, tags)
{:safe, index_content}
do_replacements(index_content, MetadataUtils.stringify_tags(tags), locale)
end
end
@spec index_file_path :: String.t()
defp index_file_path do
Path.join(Application.app_dir(:mobilizon, "priv/static"), "index.html")
end
@spec replace_meta(String.t(), String.t()) :: String.t()
# TODO: Find why it's different in dev/prod and during tests
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()) :: {:safe, String.t()}
defp do_replacements(index_content, tags, locale) do
index_content
|> replace_meta(tags)
|> String.replace("<html lang=\"en\">", "<html lang=\"#{locale}\">")
|> String.replace("<html lang=en>", "<html lang=\"#{locale}\">")
|> (&{:safe, &1}).()
end
@spec get_locale(Conn.t()) :: String.t()
defp get_locale(%{private: %{cldr_locale: nil}}), do: "en"
defp get_locale(%{private: %{cldr_locale: %{requested_locale_name: locale}}}), do: locale
defp get_locale(_), do: "en"
end