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