mobilizon/lib/mobilizon/conversations/conversations.ex

346 lines
12 KiB
Elixir

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)
|> order_by([cp], desc: cp.unread, desc: cp.updated_at)
|> 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