2023-10-17 16:41:31 +02:00
|
|
|
defmodule Mobilizon.Conversations do
|
|
|
|
@moduledoc """
|
|
|
|
The conversations context
|
|
|
|
"""
|
|
|
|
|
|
|
|
import Ecto.Query
|
|
|
|
|
|
|
|
alias Ecto.Changeset
|
|
|
|
alias Ecto.Multi
|
|
|
|
alias Mobilizon.Actors.{Actor, Member}
|
|
|
|
alias Mobilizon.Conversations.{Conversation, ConversationParticipant}
|
|
|
|
alias Mobilizon.Discussions.Comment
|
|
|
|
alias Mobilizon.Events.Event
|
|
|
|
alias Mobilizon.Storage.{Page, Repo}
|
|
|
|
|
|
|
|
@conversation_preloads [
|
|
|
|
:origin_comment,
|
|
|
|
:last_comment,
|
|
|
|
:event,
|
|
|
|
:participants
|
|
|
|
]
|
|
|
|
|
|
|
|
@comment_preloads [
|
|
|
|
:actor,
|
|
|
|
:event,
|
|
|
|
:attributed_to,
|
|
|
|
:in_reply_to_comment,
|
|
|
|
:origin_comment,
|
|
|
|
:replies,
|
|
|
|
:tags,
|
|
|
|
:mentions,
|
|
|
|
:media
|
|
|
|
]
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Get a conversation by it's ID
|
|
|
|
"""
|
|
|
|
@spec get_conversation(String.t() | integer()) :: Conversation.t() | nil
|
|
|
|
def get_conversation(conversation_id) do
|
|
|
|
Conversation
|
|
|
|
|> Repo.get(conversation_id)
|
|
|
|
|> Repo.preload(@conversation_preloads)
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Get a conversation by it's ID
|
|
|
|
"""
|
|
|
|
@spec get_conversation_participant(String.t() | integer()) :: Conversation.t() | nil
|
|
|
|
def get_conversation_participant(conversation_participant_id) do
|
|
|
|
preload_conversation_participant_details()
|
|
|
|
|> where([cp], cp.id == ^conversation_participant_id)
|
|
|
|
|> Repo.one()
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_participant_by_conversation_and_actor(conversation_id, actor_id) do
|
|
|
|
preload_conversation_participant_details()
|
|
|
|
|> where([cp], cp.conversation_id == ^conversation_id and cp.actor_id == ^actor_id)
|
|
|
|
|> Repo.one()
|
|
|
|
end
|
|
|
|
|
|
|
|
defp preload_conversation_participant_details do
|
|
|
|
ConversationParticipant
|
|
|
|
|> join(:inner, [cp], c in Conversation, on: cp.conversation_id == c.id)
|
|
|
|
|> join(:left, [_cp, c], e in Event, on: c.event_id == e.id)
|
|
|
|
|> join(:inner, [cp], a in Actor, on: cp.actor_id == a.id)
|
|
|
|
|> join(:inner, [_cp, c], lc in Comment, on: c.last_comment_id == lc.id)
|
|
|
|
|> join(:inner, [_cp, c], oc in Comment, on: c.origin_comment_id == oc.id)
|
|
|
|
|> join(:inner, [_cp, c], p in ConversationParticipant, on: c.id == p.conversation_id)
|
|
|
|
|> join(:inner, [_cp, _c, _e, _a, _lc, _oc, p], ap in Actor, on: p.actor_id == ap.id)
|
|
|
|
|> preload([_cp, c, e, a, lc, oc, p, ap],
|
|
|
|
actor: a,
|
|
|
|
conversation:
|
|
|
|
{c, event: e, last_comment: lc, origin_comment: oc, participants: {p, actor: ap}}
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Get a paginated list of conversations for an actor
|
|
|
|
"""
|
|
|
|
@spec find_conversations_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
|
|
|
Page.t(Conversation.t())
|
|
|
|
def find_conversations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
|
|
|
Conversation
|
|
|
|
|> where([c], c.actor_id == ^actor_id)
|
|
|
|
|> preload(^@conversation_preloads)
|
|
|
|
|> order_by(desc: :updated_at)
|
|
|
|
|> Page.build_page(page, limit)
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec find_conversations_for_event(
|
|
|
|
String.t() | integer,
|
|
|
|
String.t() | integer,
|
|
|
|
integer | nil,
|
|
|
|
integer | nil
|
|
|
|
) :: Page.t(ConversationParticipant.t())
|
|
|
|
def find_conversations_for_event(event_id, actor_id, page \\ nil, limit \\ nil) do
|
|
|
|
ConversationParticipant
|
|
|
|
|> join(:inner, [cp], c in Conversation, on: cp.conversation_id == c.id)
|
|
|
|
|> join(:left, [_cp, c], e in Event, on: c.event_id == e.id)
|
|
|
|
|> join(:inner, [cp], a in Actor, on: cp.actor_id == a.id)
|
|
|
|
|> join(:inner, [_cp, c], lc in Comment, on: c.last_comment_id == lc.id)
|
|
|
|
|> join(:inner, [_cp, c], oc in Comment, on: c.origin_comment_id == oc.id)
|
|
|
|
|> join(:inner, [_cp, c], p in ConversationParticipant, on: c.id == p.conversation_id)
|
|
|
|
|> join(:inner, [_cp, _c, _e, _a, _lc, _oc, p], ap in Actor, on: p.actor_id == ap.id)
|
|
|
|
|> where([_cp, c], c.event_id == ^event_id)
|
|
|
|
|> where([cp], cp.actor_id == ^actor_id)
|
2023-11-21 15:28:56 +01:00
|
|
|
|> order_by([cp], desc: cp.unread, desc: cp.updated_at)
|
2023-10-17 16:41:31 +02:00
|
|
|
|> preload([_cp, c, e, a, lc, oc, p, ap],
|
|
|
|
actor: a,
|
|
|
|
conversation:
|
|
|
|
{c, event: e, last_comment: lc, origin_comment: oc, participants: {p, actor: ap}}
|
|
|
|
)
|
|
|
|
|> Page.build_page(page, limit)
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec list_conversation_participants_for_actor(
|
|
|
|
integer | String.t(),
|
|
|
|
integer | nil,
|
|
|
|
integer | nil
|
|
|
|
) ::
|
|
|
|
Page.t(ConversationParticipant.t())
|
|
|
|
def list_conversation_participants_for_actor(actor_id, page \\ nil, limit \\ nil) do
|
|
|
|
subquery =
|
|
|
|
ConversationParticipant
|
|
|
|
|> distinct([cp], cp.conversation_id)
|
|
|
|
|> join(:left, [cp], m in Member, on: cp.actor_id == m.parent_id)
|
|
|
|
|> where([cp], cp.actor_id == ^actor_id)
|
|
|
|
|> or_where(
|
|
|
|
[_cp, m],
|
|
|
|
m.actor_id == ^actor_id and m.role in [:creator, :administrator, :moderator]
|
|
|
|
)
|
|
|
|
|
|
|
|
subquery
|
|
|
|
|> subquery()
|
|
|
|
|> order_by([cp], desc: cp.unread, desc: cp.updated_at)
|
|
|
|
|> preload([:actor, conversation: [:last_comment, :participants]])
|
|
|
|
|> Page.build_page(page, limit)
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec list_conversation_participants_for_user(
|
|
|
|
integer | String.t(),
|
|
|
|
integer | nil,
|
|
|
|
integer | nil
|
|
|
|
) ::
|
|
|
|
Page.t(ConversationParticipant.t())
|
|
|
|
def list_conversation_participants_for_user(user_id, page \\ nil, limit \\ nil) do
|
|
|
|
ConversationParticipant
|
|
|
|
|> join(:inner, [cp], a in Actor, on: cp.actor_id == a.id)
|
|
|
|
|> where([_cp, a], a.user_id == ^user_id)
|
|
|
|
|> preload([:actor, conversation: [:last_comment, :participants]])
|
|
|
|
|> Page.build_page(page, limit)
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec list_conversation_participants_for_conversation(integer | String.t()) ::
|
|
|
|
list(ConversationParticipant.t())
|
|
|
|
def list_conversation_participants_for_conversation(conversation_id) do
|
|
|
|
ConversationParticipant
|
|
|
|
|> where([cp], cp.conversation_id == ^conversation_id)
|
|
|
|
|> Repo.all()
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec count_unread_conversation_participants_for_person(integer | String.t()) ::
|
|
|
|
non_neg_integer()
|
|
|
|
def count_unread_conversation_participants_for_person(actor_id) do
|
|
|
|
ConversationParticipant
|
|
|
|
|> where([cp], cp.actor_id == ^actor_id and cp.unread == true)
|
|
|
|
|> Repo.aggregate(:count)
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Creates a conversation.
|
|
|
|
"""
|
|
|
|
@spec create_conversation(map()) ::
|
|
|
|
{:ok, Conversation.t()} | {:error, atom(), Changeset.t(), map()}
|
|
|
|
def create_conversation(attrs) do
|
|
|
|
with {:ok, %{comment: %Comment{} = _comment, conversation: %Conversation{} = conversation}} <-
|
|
|
|
Multi.new()
|
|
|
|
|> Multi.insert(
|
|
|
|
:comment,
|
|
|
|
Comment.changeset(
|
|
|
|
%Comment{},
|
|
|
|
Map.merge(attrs, %{
|
|
|
|
actor_id: attrs.actor_id,
|
|
|
|
attributed_to_id: attrs.actor_id,
|
|
|
|
visibility: :private
|
|
|
|
})
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|> Multi.insert(:conversation, fn %{
|
|
|
|
comment: %Comment{
|
|
|
|
id: comment_id,
|
|
|
|
origin_comment_id: origin_comment_id
|
|
|
|
}
|
|
|
|
} ->
|
|
|
|
Conversation.changeset(
|
|
|
|
%Conversation{},
|
|
|
|
Map.merge(attrs, %{
|
|
|
|
last_comment_id: comment_id,
|
|
|
|
origin_comment_id: origin_comment_id || comment_id,
|
|
|
|
participants: attrs.participants
|
|
|
|
})
|
|
|
|
)
|
|
|
|
end)
|
|
|
|
|> Multi.update(:update_comment, fn %{
|
|
|
|
comment: %Comment{} = comment,
|
|
|
|
conversation: %Conversation{id: conversation_id}
|
|
|
|
} ->
|
|
|
|
Comment.changeset(
|
|
|
|
comment,
|
|
|
|
%{conversation_id: conversation_id}
|
|
|
|
)
|
|
|
|
end)
|
|
|
|
|> Multi.update_all(
|
|
|
|
:conversation_participants,
|
|
|
|
fn %{
|
|
|
|
conversation: %Conversation{
|
|
|
|
id: conversation_id
|
|
|
|
}
|
|
|
|
} ->
|
|
|
|
ConversationParticipant
|
|
|
|
|> where(
|
|
|
|
[cp],
|
|
|
|
cp.conversation_id == ^conversation_id and cp.actor_id == ^attrs.actor_id
|
|
|
|
)
|
|
|
|
|> update([cp], set: [unread: false, updated_at: ^NaiveDateTime.utc_now()])
|
|
|
|
end,
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
|> Repo.transaction(),
|
|
|
|
%Conversation{} = conversation <- Repo.preload(conversation, @conversation_preloads) do
|
|
|
|
{:ok, conversation}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Create a response to a conversation
|
|
|
|
"""
|
|
|
|
@spec reply_to_conversation(Conversation.t(), map()) ::
|
|
|
|
{:ok, Conversation.t()} | {:error, atom(), Ecto.Changeset.t(), map()}
|
|
|
|
def reply_to_conversation(%Conversation{id: conversation_id} = conversation, attrs \\ %{}) do
|
|
|
|
attrs =
|
|
|
|
Map.merge(attrs, %{
|
|
|
|
conversation_id: conversation_id,
|
|
|
|
actor_id: Map.get(attrs, :creator_id, Map.get(attrs, :actor_id)),
|
|
|
|
origin_comment_id: conversation.origin_comment_id,
|
|
|
|
in_reply_to_comment_id: conversation.last_comment_id,
|
|
|
|
visibility: :private
|
|
|
|
})
|
|
|
|
|
|
|
|
changeset =
|
|
|
|
Comment.changeset(
|
|
|
|
%Comment{},
|
|
|
|
attrs
|
|
|
|
)
|
|
|
|
|
|
|
|
with {:ok, %{comment: %Comment{} = comment, conversation: %Conversation{} = conversation}} <-
|
|
|
|
Multi.new()
|
|
|
|
|> Multi.insert(
|
|
|
|
:comment,
|
|
|
|
changeset
|
|
|
|
)
|
|
|
|
|> Multi.update(:conversation, fn %{comment: %Comment{id: comment_id}} ->
|
|
|
|
Conversation.changeset(
|
|
|
|
conversation,
|
|
|
|
%{last_comment_id: comment_id}
|
|
|
|
)
|
|
|
|
end)
|
|
|
|
|> Multi.update_all(
|
|
|
|
:conversation_participants,
|
|
|
|
fn %{
|
|
|
|
conversation: %Conversation{
|
|
|
|
id: conversation_id
|
|
|
|
}
|
|
|
|
} ->
|
|
|
|
ConversationParticipant
|
|
|
|
|> where(
|
|
|
|
[cp],
|
|
|
|
cp.conversation_id == ^conversation_id and cp.actor_id != ^attrs.actor_id
|
|
|
|
)
|
|
|
|
|> update([cp], set: [unread: true, updated_at: ^NaiveDateTime.utc_now()])
|
|
|
|
end,
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
|> Multi.update_all(
|
|
|
|
:conversation_participants_author,
|
|
|
|
fn %{
|
|
|
|
conversation: %Conversation{
|
|
|
|
id: conversation_id
|
|
|
|
}
|
|
|
|
} ->
|
|
|
|
ConversationParticipant
|
|
|
|
|> where(
|
|
|
|
[cp],
|
|
|
|
cp.conversation_id == ^conversation_id and cp.actor_id == ^attrs.actor_id
|
|
|
|
)
|
|
|
|
|> update([cp], set: [unread: false, updated_at: ^NaiveDateTime.utc_now()])
|
|
|
|
end,
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
|> Repo.transaction(),
|
|
|
|
# Conversation is not updated
|
|
|
|
%Comment{} = comment <- Repo.preload(comment, @comment_preloads) do
|
|
|
|
{:ok, %Conversation{conversation | last_comment: comment}}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Update a conversation.
|
|
|
|
"""
|
|
|
|
@spec update_conversation(Conversation.t(), map()) ::
|
|
|
|
{:ok, Conversation.t()} | {:error, Changeset.t()}
|
|
|
|
def update_conversation(%Conversation{} = conversation, attrs \\ %{}) do
|
|
|
|
conversation
|
|
|
|
|> Conversation.changeset(attrs)
|
|
|
|
|> Repo.update()
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Delete a conversation.
|
|
|
|
"""
|
|
|
|
@spec delete_conversation(Conversation.t()) ::
|
|
|
|
{:ok, %{comments: {integer() | nil, any()}}} | {:error, :comments, Changeset.t(), map()}
|
|
|
|
def delete_conversation(%Conversation{id: conversation_id}) do
|
|
|
|
Multi.new()
|
|
|
|
|> Multi.delete_all(:comments, fn _ ->
|
|
|
|
where(Comment, [c], c.conversation_id == ^conversation_id)
|
|
|
|
end)
|
|
|
|
# |> Multi.delete(:conversation, conversation)
|
|
|
|
|> Repo.transaction()
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Update a conversation participant. Only their read status for now
|
|
|
|
"""
|
|
|
|
@spec update_conversation_participant(ConversationParticipant.t(), map()) ::
|
|
|
|
{:ok, ConversationParticipant.t()} | {:error, Changeset.t()}
|
|
|
|
def update_conversation_participant(
|
|
|
|
%ConversationParticipant{} = conversation_participant,
|
|
|
|
attrs \\ %{}
|
|
|
|
) do
|
|
|
|
conversation_participant
|
|
|
|
|> ConversationParticipant.changeset(attrs)
|
|
|
|
|> Repo.update()
|
|
|
|
end
|
|
|
|
end
|