fix: various fixes
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
3c288c5858
commit
b635937091
@ -147,6 +147,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Conversations do
|
|||||||
(args |> Map.get(:mentions, []) |> prepare_mentions()) ++
|
(args |> Map.get(:mentions, []) |> prepare_mentions()) ++
|
||||||
ConverterUtils.fetch_mentions(mentions)
|
ConverterUtils.fetch_mentions(mentions)
|
||||||
|
|
||||||
|
# Can't create a conversation with just ourselves
|
||||||
|
mentions =
|
||||||
|
Enum.filter(mentions, fn %{actor_id: actor_id} ->
|
||||||
|
to_string(actor_id) != to_string(args.actor_id)
|
||||||
|
end)
|
||||||
|
|
||||||
if Enum.empty?(mentions) do
|
if Enum.empty?(mentions) do
|
||||||
{:error, :empty_participants}
|
{:error, :empty_participants}
|
||||||
else
|
else
|
||||||
|
@ -11,8 +11,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
|
|||||||
alias Mobilizon.Storage.Page
|
alias Mobilizon.Storage.Page
|
||||||
alias Mobilizon.Users.User
|
alias Mobilizon.Users.User
|
||||||
alias Mobilizon.Web.Endpoint
|
alias Mobilizon.Web.Endpoint
|
||||||
# alias Mobilizon.Users.User
|
|
||||||
import Mobilizon.Web.Gettext, only: [dgettext: 2]
|
import Mobilizon.Web.Gettext, only: [dgettext: 2]
|
||||||
|
require Logger
|
||||||
|
|
||||||
@spec find_conversations_for_event(Event.t(), map(), Absinthe.Resolution.t()) ::
|
@spec find_conversations_for_event(Event.t(), map(), Absinthe.Resolution.t()) ::
|
||||||
{:ok, Page.t(ConversationView.t())} | {:error, :unauthenticated}
|
{:ok, Page.t(ConversationView.t())} | {:error, :unauthenticated}
|
||||||
@ -157,9 +157,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
|
|||||||
{:ok, conversation_to_view(conversation, conversation_participant_actor)}
|
{:ok, conversation_to_view(conversation, conversation_participant_actor)}
|
||||||
|
|
||||||
{:error, :empty_participants} ->
|
{:error, :empty_participants} ->
|
||||||
{:error, dgettext("errors", "Conversation needs to mention at least one participant")}
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"Conversation needs to mention at least one participant that's not yourself"
|
||||||
|
)}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
Logger.debug(
|
||||||
|
"Actor #{current_actor.id} is not authorized to reply to conversation #{inspect(Map.get(args, :conversation_id))}"
|
||||||
|
)
|
||||||
|
|
||||||
{:error, :unauthorized}
|
{:error, :unauthorized}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -259,7 +267,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
|
|||||||
%Conversation{participants: participants} ->
|
%Conversation{participants: participants} ->
|
||||||
participant_ids = Enum.map(participants, fn participant -> to_string(participant.id) end)
|
participant_ids = Enum.map(participants, fn participant -> to_string(participant.id) end)
|
||||||
|
|
||||||
current_actor_id in participant_ids or
|
to_string(current_actor_id) in participant_ids or
|
||||||
Enum.any?(participant_ids, fn participant_id ->
|
Enum.any?(participant_ids, fn participant_id ->
|
||||||
Actors.is_member?(current_actor_id, participant_id) and
|
Actors.is_member?(current_actor_id, participant_id) and
|
||||||
attributed_to_id == participant_id
|
attributed_to_id == participant_id
|
||||||
|
@ -2,8 +2,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||||||
@moduledoc """
|
@moduledoc """
|
||||||
Handles the participation-related GraphQL calls.
|
Handles the participation-related GraphQL calls.
|
||||||
"""
|
"""
|
||||||
# alias Mobilizon.Conversations.ConversationParticipant
|
alias Mobilizon.{Actors, Config, Conversations, Crypto, Events}
|
||||||
alias Mobilizon.{Actors, Config, Crypto, Events}
|
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Conversations.{Conversation, ConversationView}
|
alias Mobilizon.Conversations.{Conversation, ConversationView}
|
||||||
alias Mobilizon.Events.{Event, Participant}
|
alias Mobilizon.Events.{Event, Participant}
|
||||||
@ -386,6 +385,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||||||
{:member, false} ->
|
{:member, false} ->
|
||||||
{:error, :unauthorized}
|
{:error, :unauthorized}
|
||||||
|
|
||||||
|
{:error, :empty_participants} ->
|
||||||
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"There are no participants matching the audience you've selected."
|
||||||
|
)}
|
||||||
|
|
||||||
{:error, err} ->
|
{:error, err} ->
|
||||||
{:error, err}
|
{:error, err}
|
||||||
end
|
end
|
||||||
@ -394,11 +400,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||||||
def send_private_messages_to_participants(_parent, _args, _resolution),
|
def send_private_messages_to_participants(_parent, _args, _resolution),
|
||||||
do: {:error, :unauthorized}
|
do: {:error, :unauthorized}
|
||||||
|
|
||||||
defp conversation_to_view(%Conversation{} = conversation, %Actor{} = actor) do
|
defp conversation_to_view(
|
||||||
|
%Conversation{id: conversation_id} = conversation,
|
||||||
|
%Actor{id: actor_id} = actor
|
||||||
|
) do
|
||||||
value =
|
value =
|
||||||
conversation
|
conversation
|
||||||
|> Map.from_struct()
|
|> Map.from_struct()
|
||||||
|> Map.put(:actor, actor)
|
|> Map.put(:actor, actor)
|
||||||
|
|> Map.put(:unread, false)
|
||||||
|
|> Map.put(
|
||||||
|
:conversation_participant_id,
|
||||||
|
Conversations.get_participant_by_conversation_and_actor(conversation_id, actor_id).id
|
||||||
|
)
|
||||||
|
|
||||||
struct(ConversationView, value)
|
struct(ConversationView, value)
|
||||||
end
|
end
|
||||||
|
@ -77,7 +77,7 @@ defmodule Mobilizon.Discussions.Comment do
|
|||||||
belongs_to(:conversation, Conversation)
|
belongs_to(:conversation, Conversation)
|
||||||
has_many(:replies, Comment, foreign_key: :origin_comment_id)
|
has_many(:replies, Comment, foreign_key: :origin_comment_id)
|
||||||
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
|
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
|
||||||
has_many(:mentions, Mention)
|
has_many(:mentions, Mention, on_replace: :delete)
|
||||||
many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete)
|
many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete)
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
|
@ -79,11 +79,16 @@ defmodule Mobilizon.Service.Activity.Conversation do
|
|||||||
defp send_participant_notifications(_, _, _, _), do: {:ok, :skipped}
|
defp send_participant_notifications(_, _, _, _), do: {:ok, :skipped}
|
||||||
|
|
||||||
defp event_subject_params(%Conversation{
|
defp event_subject_params(%Conversation{
|
||||||
event: %Event{id: conversation_event_id, title: conversation_event_title}
|
event: %Event{
|
||||||
|
id: conversation_event_id,
|
||||||
|
title: conversation_event_title,
|
||||||
|
uuid: conversation_event_uuid
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
do: %{
|
do: %{
|
||||||
conversation_event_id: conversation_event_id,
|
conversation_event_id: conversation_event_id,
|
||||||
conversation_event_title: conversation_event_title
|
conversation_event_title: conversation_event_title,
|
||||||
|
conversation_event_uuid: conversation_event_uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
defp event_subject_params(_), do: %{}
|
defp event_subject_params(_), do: %{}
|
||||||
|
@ -14,6 +14,8 @@ defmodule Mobilizon.Service.Formatter.HTML do
|
|||||||
|
|
||||||
def filter_tags(html), do: Sanitizer.scrub(html, DefaultScrubbler)
|
def filter_tags(html), do: Sanitizer.scrub(html, DefaultScrubbler)
|
||||||
|
|
||||||
|
defdelegate basic_html(html), to: FastSanitize
|
||||||
|
|
||||||
@spec strip_tags(String.t()) :: String.t() | no_return()
|
@spec strip_tags(String.t()) :: String.t() | no_return()
|
||||||
def strip_tags(html) do
|
def strip_tags(html) do
|
||||||
case FastSanitize.strip_tags(html) do
|
case FastSanitize.strip_tags(html) do
|
||||||
@ -39,5 +41,17 @@ defmodule Mobilizon.Service.Formatter.HTML do
|
|||||||
|
|
||||||
def strip_tags_and_insert_spaces(html), do: html
|
def strip_tags_and_insert_spaces(html), do: html
|
||||||
|
|
||||||
|
@spec html_to_text(String.t()) :: String.t()
|
||||||
|
def html_to_text(html) do
|
||||||
|
html
|
||||||
|
|> String.replace(~r/<li>/, "\\g{1}- ", global: true)
|
||||||
|
|> String.replace(
|
||||||
|
~r/<\/?\s?br>|<\/\s?p>|<\/\s?li>|<\/\s?div>|<\/\s?h.>/,
|
||||||
|
"\\g{1}\n\r",
|
||||||
|
global: true
|
||||||
|
)
|
||||||
|
|> strip_tags()
|
||||||
|
end
|
||||||
|
|
||||||
def filter_tags_for_oembed(html), do: Sanitizer.scrub(html, OEmbed)
|
def filter_tags_for_oembed(html), do: Sanitizer.scrub(html, OEmbed)
|
||||||
end
|
end
|
||||||
|
37
lib/service/formatter/text.ex
Normal file
37
lib/service/formatter/text.ex
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
defmodule Mobilizon.Service.Formatter.Text do
|
||||||
|
@moduledoc """
|
||||||
|
Helps to format text blocks
|
||||||
|
|
||||||
|
Inspired from https://elixirforum.com/t/is-there-are-text-wrapping-library-for-elixir/21733/4
|
||||||
|
Using the Knuth-Plass Line Wrapping Algorithm https://www.students.cs.ubc.ca/~cs-490/2015W2/lectures/Knuth.pdf
|
||||||
|
"""
|
||||||
|
|
||||||
|
def quote_paragraph(string, max_line_length) do
|
||||||
|
paragraph(string, max_line_length, "> ")
|
||||||
|
end
|
||||||
|
|
||||||
|
def paragraph(string, max_line_length, prefix \\ "") do
|
||||||
|
string
|
||||||
|
|> String.split("\n\n", trim: true)
|
||||||
|
|> Enum.map(&subparagraph(&1, max_line_length, prefix))
|
||||||
|
|> Enum.join("\n#{prefix}\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp subparagraph(string, max_line_length, prefix) do
|
||||||
|
[word | rest] = String.split(string, ~r/\s+/, trim: true)
|
||||||
|
|
||||||
|
lines_assemble(rest, max_line_length - String.length(prefix), String.length(word), word, [])
|
||||||
|
|> Enum.map(&"#{prefix}#{&1}")
|
||||||
|
|> Enum.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp lines_assemble([], _, _, line, acc), do: [line | acc] |> Enum.reverse()
|
||||||
|
|
||||||
|
defp lines_assemble([word | rest], max, line_length, line, acc) do
|
||||||
|
if line_length + 1 + String.length(word) > max do
|
||||||
|
lines_assemble(rest, max, String.length(word), word, [line | acc])
|
||||||
|
else
|
||||||
|
lines_assemble(rest, max, line_length + 1 + String.length(word), line <> " " <> word, acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -22,6 +22,13 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
|
|||||||
notify_anonymous_participants(get_in(args, ["subject_params", "event_id"]), activity)
|
notify_anonymous_participants(get_in(args, ["subject_params", "event_id"]), activity)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if args["subject"] == "conversation_created" do
|
||||||
|
notify_anonymous_participants(
|
||||||
|
get_in(args, ["subject_params", "conversation_event_id"]),
|
||||||
|
activity
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
args
|
args
|
||||||
|> users_to_notify(author_id: args["author_id"], group_id: Map.get(args, "group_id"))
|
|> users_to_notify(author_id: args["author_id"], group_id: Map.get(args, "group_id"))
|
||||||
|> Enum.each(¬ify_user(&1, activity))
|
|> Enum.each(¬ify_user(&1, activity))
|
||||||
|
@ -10,6 +10,7 @@ defmodule Mobilizon.Web.Email.Activity do
|
|||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Config
|
alias Mobilizon.Config
|
||||||
alias Mobilizon.Web.Email
|
alias Mobilizon.Web.Email
|
||||||
|
require Logger
|
||||||
|
|
||||||
@spec direct_activity(String.t(), list(), Keyword.t()) :: Swoosh.Email.t()
|
@spec direct_activity(String.t(), list(), Keyword.t()) :: Swoosh.Email.t()
|
||||||
def direct_activity(
|
def direct_activity(
|
||||||
@ -39,6 +40,36 @@ defmodule Mobilizon.Web.Email.Activity do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@spec anonymous_activity(String.t(), Activity.t(), Keyword.t()) :: Swoosh.Email.t()
|
@spec anonymous_activity(String.t(), Activity.t(), Keyword.t()) :: Swoosh.Email.t()
|
||||||
|
def anonymous_activity(
|
||||||
|
email,
|
||||||
|
%Activity{subject_params: subject_params, type: :conversation} = activity,
|
||||||
|
options
|
||||||
|
) do
|
||||||
|
locale = Keyword.get(options, :locale, "en")
|
||||||
|
|
||||||
|
subject =
|
||||||
|
dgettext(
|
||||||
|
"activity",
|
||||||
|
"Informations about your event %{event}",
|
||||||
|
event: subject_params["conversation_event_title"]
|
||||||
|
)
|
||||||
|
|
||||||
|
conversation = Mobilizon.Conversations.get_conversation(activity.object_id)
|
||||||
|
|
||||||
|
Logger.debug("Going to send anonymous activity of type #{activity.type} to #{email}")
|
||||||
|
|
||||||
|
[to: email, subject: subject]
|
||||||
|
|> Email.base_email()
|
||||||
|
|> render_body(:email_anonymous_activity, %{
|
||||||
|
subject: subject,
|
||||||
|
activity: activity,
|
||||||
|
locale: locale,
|
||||||
|
extra: %{
|
||||||
|
"conversation" => conversation
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
def anonymous_activity(email, %Activity{subject_params: subject_params} = activity, options) do
|
def anonymous_activity(email, %Activity{subject_params: subject_params} = activity, options) do
|
||||||
locale = Keyword.get(options, :locale, "en")
|
locale = Keyword.get(options, :locale, "en")
|
||||||
|
|
||||||
@ -49,6 +80,8 @@ defmodule Mobilizon.Web.Email.Activity do
|
|||||||
event: subject_params["event_title"]
|
event: subject_params["event_title"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Logger.debug("Going to send anonymous activity of type #{activity.type} to #{email}")
|
||||||
|
|
||||||
[to: email, subject: subject]
|
[to: email, subject: subject]
|
||||||
|> Email.base_email()
|
|> Email.base_email()
|
||||||
|> render_body(:email_anonymous_activity, %{
|
|> render_body(:email_anonymous_activity, %{
|
||||||
|
@ -35,6 +35,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="600">
|
<td align="center" valign="top" width="600">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
|
<%= case @activity.type do %>
|
||||||
|
<% :comment -> %>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
||||||
<!-- COPY -->
|
<!-- COPY -->
|
||||||
<tr>
|
<tr>
|
||||||
@ -47,9 +49,10 @@
|
|||||||
>
|
>
|
||||||
<%= dgettext(
|
<%= dgettext(
|
||||||
"activity",
|
"activity",
|
||||||
"%{profile} has posted an announcement under event %{event}.",
|
"%{profile} has posted a public announcement under event %{event}.",
|
||||||
%{
|
%{
|
||||||
profile: "<b>#{escape_html(display_name_and_username(@activity.author))}</b>",
|
profile:
|
||||||
|
"<b>#{escape_html(display_name_and_username(@activity.author))}</b>",
|
||||||
event:
|
event:
|
||||||
"<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
|
"<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
|
||||||
:event,
|
:event,
|
||||||
@ -90,6 +93,106 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<% :conversation -> %>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
||||||
|
<!-- COPY -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="left">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="border-radius: 3px; text-align: left; padding: 10px 5% 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 400;line-height: 25px;"
|
||||||
|
>
|
||||||
|
<%= dgettext(
|
||||||
|
"activity",
|
||||||
|
"%{profile} has posted a private announcement about event %{event}.",
|
||||||
|
%{
|
||||||
|
profile:
|
||||||
|
"<b>#{escape_html(display_name_and_username(@activity.author))}</b>",
|
||||||
|
event:
|
||||||
|
"<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
|
||||||
|
:event,
|
||||||
|
@activity.subject_params["conversation_event_uuid"]) |> URI.decode()}\">
|
||||||
|
#{escape_html(@activity.subject_params["conversation_event_title"])}
|
||||||
|
</a>"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> raw %>
|
||||||
|
<%= dgettext(
|
||||||
|
"activity",
|
||||||
|
"It might give details on how to join the event, so make sure to read it appropriately."
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<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">
|
||||||
|
<blockquote style="border-left-width: 0.25rem;border-left-color: #e2e8f0;border-left-style: solid;padding-left: 1em;margin: 0;text-align: start;color: #474467;font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 400;line-height: 25px;">
|
||||||
|
<%= @extra["conversation"].last_comment.text
|
||||||
|
|> sanitize_to_basic_html()
|
||||||
|
|> raw() %>
|
||||||
|
</blockquote>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="left">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="border-radius: 3px; text-align: left; padding: 10px 5% 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 400;line-height: 25px;"
|
||||||
|
>
|
||||||
|
<%= dgettext(
|
||||||
|
"activity",
|
||||||
|
"This information is sent privately to you as a person who registered for this event. Share the informations above with other people with caution."
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<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="#3C376E">
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
"#{Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["conversation_event_uuid"])}"
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"
|
||||||
|
>
|
||||||
|
<%= gettext("Visit event page") %>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<% end %>
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,11 +1,30 @@
|
|||||||
<%= @subject %>
|
<%= @subject %>
|
||||||
|
|
||||||
==
|
==
|
||||||
|
<%= case @activity.type do %>
|
||||||
<%= dgettext("activity", "%{profile} has posted an announcement under event %{event}.",
|
<% :comment -> %>
|
||||||
|
<%= dgettext("activity", "%{profile} has posted a public announcement under event %{event}.",
|
||||||
%{
|
%{
|
||||||
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
|
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
|
||||||
event: @activity.subject_params["event_title"]
|
event: @activity.subject_params["event_title"]
|
||||||
}
|
}
|
||||||
) %>
|
) %>
|
||||||
<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %>
|
<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %>
|
||||||
|
<% :conversation -> %>
|
||||||
|
<%= dgettext("activity", "%{profile} has posted a private announcement about event %{event}.",
|
||||||
|
%{
|
||||||
|
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
|
||||||
|
event: @activity.subject_params["conversation_event_title"]
|
||||||
|
}
|
||||||
|
) %>
|
||||||
|
<%= dgettext("activity", "It might give details on how to join the event, so make sure to read it appropriately.") %>
|
||||||
|
|
||||||
|
--
|
||||||
|
|
||||||
|
<%= @extra["conversation"].last_comment.text |> html_to_text() |> mail_quote() %>
|
||||||
|
|
||||||
|
--
|
||||||
|
|
||||||
|
<%= dgettext("activity", "This information is sent privately to you as a person who registered for this event. Share the informations above with other people with caution.") %>
|
||||||
|
<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["conversation_event_uuid"]) |> URI.decode() %>
|
||||||
|
<% end %>
|
@ -7,6 +7,7 @@ defmodule Mobilizon.Web.EmailView do
|
|||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Service.Address
|
alias Mobilizon.Service.Address
|
||||||
alias Mobilizon.Service.DateTime, as: DateTimeRenderer
|
alias Mobilizon.Service.DateTime, as: DateTimeRenderer
|
||||||
|
alias Mobilizon.Service.Formatter.{HTML, Text}
|
||||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||||
import Mobilizon.Web.Gettext
|
import Mobilizon.Web.Gettext
|
||||||
import Mobilizon.Service.Metadata.Utils, only: [process_description: 1]
|
import Mobilizon.Service.Metadata.Utils, only: [process_description: 1]
|
||||||
@ -35,6 +36,21 @@ defmodule Mobilizon.Web.EmailView do
|
|||||||
|> safe_to_string()
|
|> safe_to_string()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec sanitize_to_basic_html(String.t()) :: String.t()
|
||||||
|
def sanitize_to_basic_html(html) do
|
||||||
|
case HTML.basic_html(html) do
|
||||||
|
{:ok, html} -> html
|
||||||
|
_ -> ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defdelegate html_to_text(html), to: HTML
|
||||||
|
|
||||||
|
def mail_quote(text) do
|
||||||
|
# https://www.emailonacid.com/blog/article/email-development/line-length-in-html-email/
|
||||||
|
Text.quote_paragraph(text, 78)
|
||||||
|
end
|
||||||
|
|
||||||
def escaped_display_name_and_username(actor) do
|
def escaped_display_name_and_username(actor) do
|
||||||
actor
|
actor
|
||||||
|> display_name_and_username()
|
|> display_name_and_username()
|
||||||
|
@ -42,13 +42,13 @@ body {
|
|||||||
@apply bg-transparent text-black dark:text-white font-semibold py-2 px-4 border border-mbz-bluegreen dark:border-violet-3;
|
@apply bg-transparent text-black dark:text-white font-semibold py-2 px-4 border border-mbz-bluegreen dark:border-violet-3;
|
||||||
}
|
}
|
||||||
.btn-outlined-success {
|
.btn-outlined-success {
|
||||||
@apply border-mbz-success hover:bg-mbz-success;
|
@apply border-2 border-mbz-success bg-transparent text-mbz-success hover:bg-mbz-success;
|
||||||
}
|
}
|
||||||
.btn-outlined-warning {
|
.btn-outlined-warning {
|
||||||
@apply bg-transparent border dark:text-white hover:dark:text-slate-900 hover:bg-mbz-warning border-mbz-warning;
|
@apply bg-transparent border dark:text-white hover:dark:text-slate-900 hover:bg-mbz-warning border-mbz-warning;
|
||||||
}
|
}
|
||||||
.btn-outlined-danger {
|
.btn-outlined-danger {
|
||||||
@apply border-mbz-danger hover:bg-mbz-danger;
|
@apply border-2 bg-transparent border-mbz-danger text-mbz-danger hover:bg-mbz-danger;
|
||||||
}
|
}
|
||||||
.btn-outlined-text {
|
.btn-outlined-text {
|
||||||
@apply bg-transparent hover:text-slate-900;
|
@apply bg-transparent hover:text-slate-900;
|
||||||
@ -161,15 +161,18 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item-active {
|
.dropdown-item-active {
|
||||||
@apply bg-white dark:bg-zinc-700 dark:text-zinc-100 text-black;
|
@apply bg-mbz-yellow-500 dark:bg-mbz-yellow-900 dark:text-zinc-100 text-black;
|
||||||
}
|
}
|
||||||
.dropdown-button {
|
.dropdown-button {
|
||||||
@apply inline-flex gap-1;
|
@apply inline-flex gap-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox */
|
/* Checkbox */
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
|
margin-inline-end: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-check {
|
||||||
@apply appearance-none bg-primary border-primary;
|
@apply appearance-none bg-primary border-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<o-inputitems
|
<o-inputitems
|
||||||
:modelValue="modelValue"
|
:modelValue="modelValueWithDisplayName"
|
||||||
@update:modelValue="(val: IActor[]) => $emit('update:modelValue', val)"
|
@update:modelValue="(val: IActor[]) => $emit('update:modelValue', val)"
|
||||||
:data="availableActors"
|
:data="availableActors"
|
||||||
:allow-autocomplete="true"
|
:allow-autocomplete="true"
|
||||||
@ -21,10 +21,10 @@ import { SEARCH_PERSON_AND_GROUPS } from "@/graphql/search";
|
|||||||
import { IActor, IGroup, IPerson, displayName } from "@/types/actor";
|
import { IActor, IGroup, IPerson, displayName } from "@/types/actor";
|
||||||
import { Paginate } from "@/types/paginate";
|
import { Paginate } from "@/types/paginate";
|
||||||
import { useLazyQuery } from "@vue/apollo-composable";
|
import { useLazyQuery } from "@vue/apollo-composable";
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import ActorInline from "./ActorInline.vue";
|
import ActorInline from "./ActorInline.vue";
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: IActor[];
|
modelValue: IActor[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@ -32,6 +32,15 @@ defineEmits<{
|
|||||||
"update:modelValue": [value: IActor[]];
|
"update:modelValue": [value: IActor[]];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const modelValue = computed(() => props.modelValue);
|
||||||
|
|
||||||
|
const modelValueWithDisplayName = computed(() =>
|
||||||
|
modelValue.value.map((actor) => ({
|
||||||
|
...actor,
|
||||||
|
displayName: displayName(actor),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
load: loadSearchPersonsAndGroupsQuery,
|
load: loadSearchPersonsAndGroupsQuery,
|
||||||
refetch: refetchSearchPersonsAndGroupsQuery,
|
refetch: refetchSearchPersonsAndGroupsQuery,
|
||||||
|
@ -39,8 +39,18 @@
|
|||||||
v-html="actor.summary"
|
v-html="actor.summary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pr-2">
|
<div class="flex pr-2" v-if="actor.type === ActorType.PERSON">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.CONVERSATION_LIST,
|
||||||
|
query: {
|
||||||
|
newMessage: 'true',
|
||||||
|
personMentions: usernameWithDomain(actor),
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
<Email />
|
<Email />
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div
|
<!-- <div
|
||||||
@ -85,6 +95,8 @@
|
|||||||
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
||||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||||
import Email from "vue-material-design-icons/Email.vue";
|
import Email from "vue-material-design-icons/Email.vue";
|
||||||
|
import RouteName from "@/router/name";
|
||||||
|
import { ActorType } from "@/types/enums";
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -24,15 +24,11 @@
|
|||||||
@{{ usernameWithDomain(actor) }}
|
@{{ usernameWithDomain(actor) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pr-2 self-center">
|
|
||||||
<Email />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
||||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||||
import Email from "vue-material-design-icons/Email.vue";
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
actor: IActor;
|
actor: IActor;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
class="flex gap-2 w-full items-center px-2 py-4 border-b-stone-200 border-b bg-white dark:bg-transparent"
|
class="flex gap-4 w-full items-center px-2 py-4 border-b-stone-200 border-b bg-white dark:bg-transparent"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.CONVERSATION,
|
name: RouteName.CONVERSATION,
|
||||||
|
@ -9,6 +9,15 @@
|
|||||||
:currentActor="currentActor"
|
:currentActor="currentActor"
|
||||||
:placeholder="t('Write a new message')"
|
:placeholder="t('Write a new message')"
|
||||||
/>
|
/>
|
||||||
|
<o-notification
|
||||||
|
class="my-2"
|
||||||
|
variant="danger"
|
||||||
|
:closable="false"
|
||||||
|
v-for="error in errors"
|
||||||
|
:key="error"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</o-notification>
|
||||||
<footer class="flex gap-2 py-3 mx-2 justify-end">
|
<footer class="flex gap-2 py-3 mx-2 justify-end">
|
||||||
<o-button :disabled="!canSend" nativeType="submit">{{
|
<o-button :disabled="!canSend" nativeType="submit">{{
|
||||||
t("Send")
|
t("Send")
|
||||||
@ -18,13 +27,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { IActor, IPerson, usernameWithDomain } from "@/types/actor";
|
import { IActor, IGroup, IPerson, usernameWithDomain } from "@/types/actor";
|
||||||
import { computed, defineAsyncComponent, provide, ref } from "vue";
|
import { computed, defineAsyncComponent, provide, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import ActorAutoComplete from "../../components/Account/ActorAutoComplete.vue";
|
import ActorAutoComplete from "../../components/Account/ActorAutoComplete.vue";
|
||||||
import {
|
import {
|
||||||
DefaultApolloClient,
|
DefaultApolloClient,
|
||||||
provideApolloClient,
|
provideApolloClient,
|
||||||
|
useLazyQuery,
|
||||||
useMutation,
|
useMutation,
|
||||||
} from "@vue/apollo-composable";
|
} from "@vue/apollo-composable";
|
||||||
import { apolloClient } from "@/vue-apollo";
|
import { apolloClient } from "@/vue-apollo";
|
||||||
@ -34,12 +44,15 @@ import { IConversation } from "@/types/conversation";
|
|||||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import RouteName from "@/router/name";
|
import RouteName from "@/router/name";
|
||||||
|
import { FETCH_PERSON } from "@/graphql/actor";
|
||||||
|
import { FETCH_GROUP_PUBLIC } from "@/graphql/group";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
mentions?: IActor[];
|
personMentions?: string[];
|
||||||
|
groupMentions?: string[];
|
||||||
}>(),
|
}>(),
|
||||||
{ mentions: () => [] }
|
{ personMentions: () => [], groupMentions: () => [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
provide(DefaultApolloClient, apolloClient);
|
provide(DefaultApolloClient, apolloClient);
|
||||||
@ -48,15 +61,36 @@ const router = useRouter();
|
|||||||
|
|
||||||
const emit = defineEmits(["close"]);
|
const emit = defineEmits(["close"]);
|
||||||
|
|
||||||
const actorMentions = ref(props.mentions);
|
const errors = ref<string[]>([]);
|
||||||
|
|
||||||
const textMentions = computed(() =>
|
const textPersonMentions = computed(() => props.personMentions);
|
||||||
(props.mentions ?? []).map((actor) => usernameWithDomain(actor)).join(" ")
|
const textGroupMentions = computed(() => props.groupMentions);
|
||||||
|
const actorMentions = ref<IActor[]>([]);
|
||||||
|
|
||||||
|
const { load: fetchPerson } = provideApolloClient(apolloClient)(() =>
|
||||||
|
useLazyQuery<{ fetchPerson: IPerson }, { username: string }>(FETCH_PERSON)
|
||||||
);
|
);
|
||||||
|
const { load: fetchGroup } = provideApolloClient(apolloClient)(() =>
|
||||||
|
useLazyQuery<{ group: IGroup }, { name: string }>(FETCH_GROUP_PUBLIC)
|
||||||
|
);
|
||||||
|
textPersonMentions.value.forEach(async (textPersonMention) => {
|
||||||
|
const result = await fetchPerson(FETCH_PERSON, {
|
||||||
|
username: textPersonMention,
|
||||||
|
});
|
||||||
|
if (!result) return;
|
||||||
|
actorMentions.value.push(result.fetchPerson);
|
||||||
|
});
|
||||||
|
textGroupMentions.value.forEach(async (textGroupMention) => {
|
||||||
|
const result = await fetchGroup(FETCH_GROUP_PUBLIC, {
|
||||||
|
name: textGroupMention,
|
||||||
|
});
|
||||||
|
if (!result) return;
|
||||||
|
actorMentions.value.push(result.group);
|
||||||
|
});
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
const text = ref(textMentions.value);
|
const text = ref("");
|
||||||
|
|
||||||
const Editor = defineAsyncComponent(
|
const Editor = defineAsyncComponent(
|
||||||
() => import("../../components/TextEditor.vue")
|
() => import("../../components/TextEditor.vue")
|
||||||
@ -70,8 +104,8 @@ const canSend = computed(() => {
|
|||||||
return actorMentions.value.length > 0 || /@.+/.test(text.value);
|
return actorMentions.value.length > 0 || /@.+/.test(text.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: postPrivateMessageMutate } = provideApolloClient(apolloClient)(
|
const { mutate: postPrivateMessageMutate, onError: onPrivateMessageError } =
|
||||||
() =>
|
provideApolloClient(apolloClient)(() =>
|
||||||
useMutation<
|
useMutation<
|
||||||
{
|
{
|
||||||
postPrivateMessage: IConversation;
|
postPrivateMessage: IConversation;
|
||||||
@ -118,6 +152,12 @@ const { mutate: postPrivateMessageMutate } = provideApolloClient(apolloClient)(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onPrivateMessageError((err) => {
|
||||||
|
err.graphQLErrors.forEach((error) => {
|
||||||
|
errors.value.push(error.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const sendForm = async (e: Event) => {
|
const sendForm = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.debug("Sending new private message");
|
console.debug("Sending new private message");
|
||||||
|
@ -1,5 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit="sendForm">
|
<form @submit="sendForm">
|
||||||
|
<h2>{{ t("New announcement") }}</h2>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<o-field class="mt-2 mb-4">
|
||||||
|
<o-checkbox
|
||||||
|
v-model="selectedRoles"
|
||||||
|
:native-value="ParticipantRole.PARTICIPANT"
|
||||||
|
:label="t('Participant')"
|
||||||
|
/>
|
||||||
|
<o-checkbox
|
||||||
|
v-model="selectedRoles"
|
||||||
|
:native-value="ParticipantRole.NOT_APPROVED"
|
||||||
|
:label="t('Not approved')"
|
||||||
|
/>
|
||||||
|
<o-checkbox
|
||||||
|
v-model="selectedRoles"
|
||||||
|
:native-value="ParticipantRole.REJECTED"
|
||||||
|
:label="t('Rejected')"
|
||||||
|
/>
|
||||||
|
</o-field>
|
||||||
<Editor
|
<Editor
|
||||||
v-model="text"
|
v-model="text"
|
||||||
mode="basic"
|
mode="basic"
|
||||||
@ -8,6 +33,15 @@
|
|||||||
:currentActor="currentActor"
|
:currentActor="currentActor"
|
||||||
:placeholder="t('Write a new message')"
|
:placeholder="t('Write a new message')"
|
||||||
/>
|
/>
|
||||||
|
<o-notification
|
||||||
|
class="my-2"
|
||||||
|
variant="danger"
|
||||||
|
:closable="true"
|
||||||
|
v-for="error in errors"
|
||||||
|
:key="error"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</o-notification>
|
||||||
<o-button class="mt-3" nativeType="submit">{{ t("Send") }}</o-button>
|
<o-button class="mt-3" nativeType="submit">{{ t("Send") }}</o-button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@ -32,9 +66,15 @@ const props = defineProps<{
|
|||||||
const event = computed(() => props.event);
|
const event = computed(() => props.event);
|
||||||
|
|
||||||
const text = ref("");
|
const text = ref("");
|
||||||
|
|
||||||
|
const errors = ref<string[]>([]);
|
||||||
|
|
||||||
|
const selectedRoles = ref<ParticipantRole[]>([ParticipantRole.PARTICIPANT]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: eventPrivateMessageMutate,
|
mutate: eventPrivateMessageMutate,
|
||||||
onDone: onEventPrivateMessageMutated,
|
onDone: onEventPrivateMessageMutated,
|
||||||
|
onError: onEventPrivateMessageError,
|
||||||
} = useMutation<
|
} = useMutation<
|
||||||
{
|
{
|
||||||
sendEventPrivateMessage: IConversation;
|
sendEventPrivateMessage: IConversation;
|
||||||
@ -43,8 +83,7 @@ const {
|
|||||||
text: string;
|
text: string;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
roles?: string;
|
roles?: ParticipantRole[];
|
||||||
inReplyToActorId?: ParticipantRole[];
|
|
||||||
language?: string;
|
language?: string;
|
||||||
}
|
}
|
||||||
>(SEND_EVENT_PRIVATE_MESSAGE_MUTATION, {
|
>(SEND_EVENT_PRIVATE_MESSAGE_MUTATION, {
|
||||||
@ -96,6 +135,7 @@ const sendForm = (e: Event) => {
|
|||||||
event.value.organizerActor?.id ??
|
event.value.organizerActor?.id ??
|
||||||
currentActor.value?.id,
|
currentActor.value?.id,
|
||||||
eventId: event.value.id,
|
eventId: event.value.id,
|
||||||
|
roles: selectedRoles.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,6 +143,12 @@ onEventPrivateMessageMutated(() => {
|
|||||||
text.value = "";
|
text.value = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onEventPrivateMessageError((err) => {
|
||||||
|
err.graphQLErrors.forEach((error) => {
|
||||||
|
errors.value.push(error.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const Editor = defineAsyncComponent(
|
const Editor = defineAsyncComponent(
|
||||||
() => import("../../components/TextEditor.vue")
|
() => import("../../components/TextEditor.vue")
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }">
|
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }">
|
||||||
<figure class="flex justify-center my-2">
|
<figure class="flex justify-center my-2">
|
||||||
<img
|
<img
|
||||||
src="../../../public/img/undraw_profile.svg"
|
src="/img/undraw_profile.svg"
|
||||||
alt="Profile illustration"
|
alt="Profile illustration"
|
||||||
width="128"
|
width="128"
|
||||||
height="128"
|
height="128"
|
||||||
@ -55,7 +55,7 @@
|
|||||||
<img
|
<img
|
||||||
width="128"
|
width="128"
|
||||||
height="128"
|
height="128"
|
||||||
src="../../../public/img/undraw_mail_2.svg"
|
src="/img/undraw_mail_2.svg"
|
||||||
alt="Privacy illustration"
|
alt="Privacy illustration"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
@ -66,7 +66,7 @@
|
|||||||
<a :href="`${event.url}/participate/without-account`" v-else>
|
<a :href="`${event.url}/participate/without-account`" v-else>
|
||||||
<figure class="flex justify-center my-2">
|
<figure class="flex justify-center my-2">
|
||||||
<img
|
<img
|
||||||
src="../../../public/img/undraw_mail_2.svg"
|
src="/img/undraw_mail_2.svg"
|
||||||
width="128"
|
width="128"
|
||||||
height="128"
|
height="128"
|
||||||
alt="Privacy illustration"
|
alt="Privacy illustration"
|
||||||
|
@ -54,7 +54,6 @@ export const SEND_EVENT_PRIVATE_MESSAGE_MUTATION = gql`
|
|||||||
$actorId: ID!
|
$actorId: ID!
|
||||||
$eventId: ID!
|
$eventId: ID!
|
||||||
$roles: [ParticipantRoleEnum]
|
$roles: [ParticipantRoleEnum]
|
||||||
$attributedToId: ID
|
|
||||||
$language: String
|
$language: String
|
||||||
) {
|
) {
|
||||||
sendEventPrivateMessage(
|
sendEventPrivateMessage(
|
||||||
@ -62,7 +61,6 @@ export const SEND_EVENT_PRIVATE_MESSAGE_MUTATION = gql`
|
|||||||
actorId: $actorId
|
actorId: $actorId
|
||||||
eventId: $eventId
|
eventId: $eventId
|
||||||
roles: $roles
|
roles: $roles
|
||||||
attributedToId: $attributedToId
|
|
||||||
language: $language
|
language: $language
|
||||||
) {
|
) {
|
||||||
...ConversationQuery
|
...ConversationQuery
|
||||||
|
@ -1625,5 +1625,8 @@
|
|||||||
"You have access to this conversation as a member of the {group} group": "You have access to this conversation as a member of the {group} group",
|
"You have access to this conversation as a member of the {group} group": "You have access to this conversation as a member of the {group} group",
|
||||||
"Comment from an event announcement": "Comment from an event announcement",
|
"Comment from an event announcement": "Comment from an event announcement",
|
||||||
"Comment from a private conversation": "Comment from a private conversation",
|
"Comment from a private conversation": "Comment from a private conversation",
|
||||||
"I've been mentionned in a conversation": "I've been mentionned in a conversation"
|
"I've been mentionned in a conversation": "I've been mentionned in a conversation",
|
||||||
|
"New announcement": "New announcement",
|
||||||
|
"This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.": "This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.",
|
||||||
|
"The following participants are groups, which means group members are able to reply to this conversation:": "The following participants are groups, which means group members are able to reply to this conversation:"
|
||||||
}
|
}
|
@ -1621,5 +1621,8 @@
|
|||||||
"You have access to this conversation as a member of the {group} group": "Vous avez accès à cette conversation en tant que membre du groupe {group}",
|
"You have access to this conversation as a member of the {group} group": "Vous avez accès à cette conversation en tant que membre du groupe {group}",
|
||||||
"Comment from an event announcement": "Commentaire d'une annonce d'événement",
|
"Comment from an event announcement": "Commentaire d'une annonce d'événement",
|
||||||
"Comment from a private conversation": "Commentaire d'une conversation privée",
|
"Comment from a private conversation": "Commentaire d'une conversation privée",
|
||||||
"I've been mentionned in a conversation": "J'ai été mentionnée dans une conversation"
|
"I've been mentionned in a conversation": "J'ai été mentionnée dans une conversation",
|
||||||
|
"New announcement": "Nouvelle annonce",
|
||||||
|
"This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.": "Cette annonce sera envoyée à tous les participant·es ayant le statut sélectionné ci-dessous. Iels ne pourront pas répondre à votre annonce, mais iels peuvent créer une nouvelle conversation avec vous.",
|
||||||
|
"The following participants are groups, which means group members are able to reply to this conversation:": "Les participants suivants sont des groupes, ce qui signifie que les membres du groupes peuvent répondre dans cette conversation:"
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,8 @@ export const orugaConfig = {
|
|||||||
variantClass: "icon-",
|
variantClass: "icon-",
|
||||||
},
|
},
|
||||||
checkbox: {
|
checkbox: {
|
||||||
checkClass: "checkbox",
|
rootClass: "checkbox",
|
||||||
|
checkClass: "checkbox-check",
|
||||||
checkCheckedClass: "checkbox-checked",
|
checkCheckedClass: "checkbox-checked",
|
||||||
labelClass: "checkbox-label",
|
labelClass: "checkbox-label",
|
||||||
},
|
},
|
||||||
|
10
src/utils/route.ts
Normal file
10
src/utils/route.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { RouteQueryTransformer } from "vue-use-route-query";
|
||||||
|
|
||||||
|
export const arrayTransformer: RouteQueryTransformer<string[]> = {
|
||||||
|
fromQuery(query: string) {
|
||||||
|
return query.split(",");
|
||||||
|
},
|
||||||
|
toQuery(value: string[]) {
|
||||||
|
return value.join(",");
|
||||||
|
},
|
||||||
|
};
|
@ -80,7 +80,7 @@
|
|||||||
<img
|
<img
|
||||||
class="w-12"
|
class="w-12"
|
||||||
v-if="instance.hasRelay"
|
v-if="instance.hasRelay"
|
||||||
src="../../../public/img/logo.svg"
|
src="/img/logo.svg"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<CloudQuestion v-else :size="36" />
|
<CloudQuestion v-else :size="36" />
|
||||||
|
@ -44,19 +44,28 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { useQuery } from "@vue/apollo-composable";
|
import { useQuery } from "@vue/apollo-composable";
|
||||||
import { computed, defineAsyncComponent, ref } from "vue";
|
import { computed, defineAsyncComponent, ref, watchEffect } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
import {
|
||||||
|
booleanTransformer,
|
||||||
|
integerTransformer,
|
||||||
|
useRouteQuery,
|
||||||
|
} from "vue-use-route-query";
|
||||||
import { PROFILE_CONVERSATIONS } from "@/graphql/event";
|
import { PROFILE_CONVERSATIONS } from "@/graphql/event";
|
||||||
import ConversationListItem from "../../components/Conversations/ConversationListItem.vue";
|
import ConversationListItem from "../../components/Conversations/ConversationListItem.vue";
|
||||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||||
import { useHead } from "@vueuse/head";
|
import { useHead } from "@vueuse/head";
|
||||||
import { IPerson } from "@/types/actor";
|
import { IPerson } from "@/types/actor";
|
||||||
import { useProgrammatic } from "@oruga-ui/oruga-next";
|
import { useProgrammatic } from "@oruga-ui/oruga-next";
|
||||||
|
import { arrayTransformer } from "@/utils/route";
|
||||||
|
|
||||||
const page = useRouteQuery("page", 1, integerTransformer);
|
const page = useRouteQuery("page", 1, integerTransformer);
|
||||||
const CONVERSATIONS_PER_PAGE = 10;
|
const CONVERSATIONS_PER_PAGE = 10;
|
||||||
|
|
||||||
|
const showModal = useRouteQuery("newMessage", false, booleanTransformer);
|
||||||
|
const personMentions = useRouteQuery("personMentions", [], arrayTransformer);
|
||||||
|
const groupMentions = useRouteQuery("groupMentions", [], arrayTransformer);
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
@ -69,6 +78,7 @@ const { result: conversationsResult } = useQuery<{
|
|||||||
loggedPerson: Pick<IPerson, "conversations">;
|
loggedPerson: Pick<IPerson, "conversations">;
|
||||||
}>(PROFILE_CONVERSATIONS, () => ({
|
}>(PROFILE_CONVERSATIONS, () => ({
|
||||||
page: page.value,
|
page: page.value,
|
||||||
|
limit: CONVERSATIONS_PER_PAGE,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const conversations = computed(
|
const conversations = computed(
|
||||||
@ -88,7 +98,17 @@ const NewConversation = defineAsyncComponent(
|
|||||||
const openNewMessageModal = () => {
|
const openNewMessageModal = () => {
|
||||||
oruga.modal.open({
|
oruga.modal.open({
|
||||||
component: NewConversation,
|
component: NewConversation,
|
||||||
|
props: {
|
||||||
|
personMentions: personMentions.value,
|
||||||
|
groupMentions: groupMentions.value,
|
||||||
|
},
|
||||||
trapFocus: true,
|
trapFocus: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (showModal.value) {
|
||||||
|
openNewMessageModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -14,13 +14,13 @@
|
|||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="conversation.event"
|
v-if="conversation.event && !isCurrentActorAuthor"
|
||||||
class="bg-mbz-yellow p-6 mb-6 rounded flex gap-2 items-center"
|
class="bg-mbz-yellow p-6 mb-3 rounded flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<Calendar :size="36" />
|
<Calendar :size="36" />
|
||||||
<i18n-t
|
<i18n-t
|
||||||
tag="p"
|
tag="p"
|
||||||
keypath="This is a announcement from the organizers of event {event}"
|
keypath="This is a announcement from the organizers of event {event}. You can't reply to it, but you can send a private message to event organizers."
|
||||||
>
|
>
|
||||||
<template #event>
|
<template #event>
|
||||||
<b>
|
<b>
|
||||||
@ -35,10 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<o-notification v-if="isCurrentActorAuthor" variant="info" closable>
|
||||||
v-if="currentActor && currentActor.id !== conversation.actor?.id"
|
|
||||||
class="bg-mbz-info p-6 rounded flex gap-2 items-center my-3"
|
|
||||||
>
|
|
||||||
<i18n-t
|
<i18n-t
|
||||||
keypath="You have access to this conversation as a member of the {group} group"
|
keypath="You have access to this conversation as a member of the {group} group"
|
||||||
tag="p"
|
tag="p"
|
||||||
@ -55,7 +52,36 @@
|
|||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</o-notification>
|
||||||
|
<o-notification
|
||||||
|
v-else-if="groupParticipants.length > 0 && !conversation.event"
|
||||||
|
variant="info"
|
||||||
|
closable
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"The following participants are groups, which means group members are able to reply to this conversation:"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc">
|
||||||
|
<li
|
||||||
|
v-for="groupParticipant in groupParticipants"
|
||||||
|
:key="groupParticipant.id"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.GROUP,
|
||||||
|
params: {
|
||||||
|
preferredUsername: usernameWithDomain(groupParticipant),
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
><b>{{ displayName(groupParticipant) }}</b></router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</o-notification>
|
||||||
<o-notification v-if="error" variant="danger">
|
<o-notification v-if="error" variant="danger">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</o-notification>
|
</o-notification>
|
||||||
@ -107,7 +133,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<div
|
<div
|
||||||
v-else-if="conversation.event"
|
v-else-if="conversation.event"
|
||||||
class="bg-mbz-yellow p-6 rounded flex gap-2 items-center mt-6"
|
class="bg-mbz-yellow p-6 rounded flex gap-2 items-center mt-3"
|
||||||
>
|
>
|
||||||
<Calendar :size="36" />
|
<Calendar :size="36" />
|
||||||
<i18n-t
|
<i18n-t
|
||||||
@ -239,6 +265,12 @@ const otherParticipants = computed(
|
|||||||
) ?? []
|
) ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groupParticipants = computed(() => {
|
||||||
|
return otherParticipants.value.filter(
|
||||||
|
(participant) => participant.type === ActorType.GROUP
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const Editor = defineAsyncComponent(
|
const Editor = defineAsyncComponent(
|
||||||
() => import("../../components/TextEditor.vue")
|
() => import("../../components/TextEditor.vue")
|
||||||
);
|
);
|
||||||
@ -253,8 +285,15 @@ const title = computed(() =>
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isCurrentActorAuthor = computed(
|
||||||
|
() =>
|
||||||
|
currentActor.value &&
|
||||||
|
conversation.value &&
|
||||||
|
currentActor.value.id !== conversation.value?.actor?.id
|
||||||
|
);
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: title.value,
|
title: () => title.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newComment = ref("");
|
const newComment = ref("");
|
||||||
|
@ -650,12 +650,6 @@ const FullAddressAutoComplete = defineAsyncComponent(
|
|||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: computed(() =>
|
|
||||||
props.isUpdate ? t("Event edition") : t("Event creation")
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
eventId?: undefined | string;
|
eventId?: undefined | string;
|
||||||
@ -667,6 +661,12 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const eventId = computed(() => props.eventId);
|
const eventId = computed(() => props.eventId);
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: computed(() =>
|
||||||
|
props.isUpdate ? t("Event edition") : t("Event creation")
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
const event = ref<IEditableEvent>(new EventModel());
|
const event = ref<IEditableEvent>(new EventModel());
|
||||||
const unmodifiedEvent = ref<IEditableEvent>(new EventModel());
|
const unmodifiedEvent = ref<IEditableEvent>(new EventModel());
|
||||||
|
|
||||||
|
@ -225,6 +225,7 @@
|
|||||||
@click="acceptParticipants(checkedRows)"
|
@click="acceptParticipants(checkedRows)"
|
||||||
variant="success"
|
variant="success"
|
||||||
:disabled="!canAcceptParticipants"
|
:disabled="!canAcceptParticipants"
|
||||||
|
outlined
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
t(
|
t(
|
||||||
@ -238,6 +239,7 @@
|
|||||||
@click="refuseParticipants(checkedRows)"
|
@click="refuseParticipants(checkedRows)"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
:disabled="!canRefuseParticipants"
|
:disabled="!canRefuseParticipants"
|
||||||
|
outlined
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
t(
|
t(
|
||||||
|
@ -266,6 +266,21 @@
|
|||||||
: t("Deactivate notifications")
|
: t("Deactivate notifications")
|
||||||
}}</span>
|
}}</span>
|
||||||
</o-button>
|
</o-button>
|
||||||
|
<o-button
|
||||||
|
outlined
|
||||||
|
tag="router-link"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.CONVERSATION_LIST,
|
||||||
|
query: {
|
||||||
|
newMessage: 'true',
|
||||||
|
groupMentions: usernameWithDomain(group),
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
icon-left="email"
|
||||||
|
v-if="!isCurrentActorAGroupMember || previewPublic"
|
||||||
|
>
|
||||||
|
{{ t("Contact") }}
|
||||||
|
</o-button>
|
||||||
<o-button
|
<o-button
|
||||||
outlined
|
outlined
|
||||||
icon-left="share"
|
icon-left="share"
|
||||||
|
@ -5,19 +5,19 @@
|
|||||||
<div class="-z-10 overflow-hidden">
|
<div class="-z-10 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
src="../../public/img/shape-1.svg"
|
src="/img/shape-1.svg"
|
||||||
class="-z-10 absolute left-[2%] top-36"
|
class="-z-10 absolute left-[2%] top-36"
|
||||||
width="300"
|
width="300"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
src="../../public/img/shape-2.svg"
|
src="/img/shape-2.svg"
|
||||||
class="-z-10 absolute left-[50%] top-[5%] -translate-x-2/4 opacity-60"
|
class="-z-10 absolute left-[50%] top-[5%] -translate-x-2/4 opacity-60"
|
||||||
width="800"
|
width="800"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
src="../../public/img/shape-3.svg"
|
src="/img/shape-3.svg"
|
||||||
class="-z-10 absolute top-0 right-36"
|
class="-z-10 absolute top-0 right-36"
|
||||||
width="200"
|
width="200"
|
||||||
/>
|
/>
|
||||||
|
@ -749,7 +749,6 @@ import {
|
|||||||
useRouteQuery,
|
useRouteQuery,
|
||||||
enumTransformer,
|
enumTransformer,
|
||||||
booleanTransformer,
|
booleanTransformer,
|
||||||
RouteQueryTransformer,
|
|
||||||
} from "vue-use-route-query";
|
} from "vue-use-route-query";
|
||||||
import Calendar from "vue-material-design-icons/Calendar.vue";
|
import Calendar from "vue-material-design-icons/Calendar.vue";
|
||||||
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
|
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
|
||||||
@ -776,6 +775,7 @@ import lodashSortBy from "lodash/sortBy";
|
|||||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||||
import SkeletonGroupResultList from "@/components/Group/SkeletonGroupResultList.vue";
|
import SkeletonGroupResultList from "@/components/Group/SkeletonGroupResultList.vue";
|
||||||
import SkeletonEventResultList from "@/components/Event/SkeletonEventResultList.vue";
|
import SkeletonEventResultList from "@/components/Event/SkeletonEventResultList.vue";
|
||||||
|
import { arrayTransformer } from "@/utils/route";
|
||||||
|
|
||||||
const EventMarkerMap = defineAsyncComponent(
|
const EventMarkerMap = defineAsyncComponent(
|
||||||
() => import("@/components/Search/EventMarkerMap.vue")
|
() => import("@/components/Search/EventMarkerMap.vue")
|
||||||
@ -840,15 +840,6 @@ enum SortValues {
|
|||||||
MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC",
|
MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC",
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrayTransformer: RouteQueryTransformer<string[]> = {
|
|
||||||
fromQuery(query: string) {
|
|
||||||
return query.split(",");
|
|
||||||
},
|
|
||||||
toQuery(value: string[]) {
|
|
||||||
return value.join(",");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tag?: string;
|
tag?: string;
|
||||||
}>();
|
}>();
|
||||||
|
Loading…
Reference in New Issue
Block a user