57
lib/mobilizon/conversations/conversation.ex
Normal file
57
lib/mobilizon/conversations/conversation.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
defmodule Mobilizon.Conversations.Conversation do
|
||||
@moduledoc """
|
||||
Represents a conversation
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Ecto.Changeset
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.ConversationParticipant
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t(),
|
||||
origin_comment: Comment.t(),
|
||||
last_comment: Comment.t(),
|
||||
participants: list(Actor.t())
|
||||
}
|
||||
|
||||
@required_attrs [:origin_comment_id, :last_comment_id]
|
||||
@optional_attrs [:event_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "conversations" do
|
||||
belongs_to(:origin_comment, Comment)
|
||||
belongs_to(:last_comment, Comment)
|
||||
belongs_to(:event, Event)
|
||||
has_many(:comments, Comment)
|
||||
|
||||
many_to_many(:participants, Actor,
|
||||
join_through: ConversationParticipant,
|
||||
join_keys: [conversation_id: :id, actor_id: :id],
|
||||
on_replace: :delete
|
||||
)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = conversation, attrs) do
|
||||
conversation
|
||||
|> cast(attrs, @attrs)
|
||||
|> maybe_set_participants(attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
defp maybe_set_participants(%Changeset{} = changeset, %{participants: participants})
|
||||
when length(participants) > 0 do
|
||||
put_assoc(changeset, :participants, participants)
|
||||
end
|
||||
|
||||
defp maybe_set_participants(%Changeset{} = changeset, _), do: changeset
|
||||
end
|
||||
40
lib/mobilizon/conversations/conversation_participant.ex
Normal file
40
lib/mobilizon/conversations/conversation_participant.ex
Normal file
@@ -0,0 +1,40 @@
|
||||
defmodule Mobilizon.Conversations.ConversationParticipant do
|
||||
@moduledoc """
|
||||
Represents a conversation participant
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Conversation
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
conversation: Conversation.t(),
|
||||
actor: Actor.t(),
|
||||
unread: boolean()
|
||||
}
|
||||
|
||||
@required_attrs [:actor_id, :conversation_id]
|
||||
@optional_attrs [:unread]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "conversation_participants" do
|
||||
belongs_to(:conversation, Conversation)
|
||||
belongs_to(:actor, Actor)
|
||||
field(:unread, :boolean, default: true)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = conversation, attrs) do
|
||||
conversation
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> foreign_key_constraint(:conversation_id)
|
||||
|> foreign_key_constraint(:actor_id)
|
||||
end
|
||||
end
|
||||
22
lib/mobilizon/conversations/conversation_view.ex
Normal file
22
lib/mobilizon/conversations/conversation_view.ex
Normal file
@@ -0,0 +1,22 @@
|
||||
defmodule Mobilizon.Conversations.ConversationView do
|
||||
@moduledoc """
|
||||
Represents a conversation view for GraphQL API
|
||||
"""
|
||||
|
||||
defstruct [
|
||||
:id,
|
||||
:conversation_participant_id,
|
||||
:origin_comment,
|
||||
:origin_comment_id,
|
||||
:last_comment,
|
||||
:last_comment_id,
|
||||
:event,
|
||||
:event_id,
|
||||
:actor,
|
||||
:actor_id,
|
||||
:unread,
|
||||
:inserted_at,
|
||||
:updated_at,
|
||||
:participants
|
||||
]
|
||||
end
|
||||
344
lib/mobilizon/conversations/conversations.ex
Normal file
344
lib/mobilizon/conversations/conversations.ex
Normal file
@@ -0,0 +1,344 @@
|
||||
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)
|
||||
|> 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
|
||||
Reference in New Issue
Block a user