Move conversation controller functionality into context

This commit is contained in:
Liam 2024-07-14 19:18:25 -04:00
parent 17a434aa36
commit 23332bec28
6 changed files with 197 additions and 134 deletions

View file

@ -9,45 +9,7 @@ defmodule Philomena.Conversations do
alias Philomena.Conversations.Conversation
alias Philomena.Conversations.Message
alias Philomena.Reports
@doc """
Creates a conversation.
## Examples
iex> create_conversation(%{field: value})
{:ok, %Conversation{}}
iex> create_conversation(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_conversation(from, attrs \\ %{}) do
%Conversation{}
|> Conversation.creation_changeset(from, attrs)
|> Repo.insert()
|> case do
{:ok, conversation} ->
report_non_approved_message(hd(conversation.messages))
{:ok, conversation}
error ->
error
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking conversation changes.
## Examples
iex> change_conversation(conversation)
%Ecto.Changeset{source: %Conversation{}}
"""
def change_conversation(%Conversation{} = conversation) do
Conversation.changeset(conversation, %{})
end
alias Philomena.Users
@doc """
Returns the number of unread conversations for the given user.
@ -75,6 +37,96 @@ defmodule Philomena.Conversations do
|> Repo.aggregate(:count)
end
@doc """
Returns a `m:Scrivener.Page` of conversations between the partner and the user.
## Examples
iex> list_conversations_with("123", %User{}, page_size: 10)
%Scrivener.Page{}
"""
def list_conversations_with(partner_id, user, pagination) do
query =
from c in Conversation,
where:
(c.from_id == ^partner_id and c.to_id == ^user.id) or
(c.to_id == ^partner_id and c.from_id == ^user.id)
list_conversations(query, user, pagination)
end
@doc """
Returns a `m:Scrivener.Page` of conversations sent by or received from the user.
## Examples
iex> list_conversations_with("123", %User{}, page_size: 10)
%Scrivener.Page{}
"""
def list_conversations(queryable \\ Conversation, user, pagination) do
query =
from c in queryable,
as: :conversations,
where:
(c.from_id == ^user.id and not c.from_hidden) or
(c.to_id == ^user.id and not c.to_hidden),
inner_lateral_join:
cnt in subquery(
from m in Message,
where: m.conversation_id == parent_as(:conversations).id,
select: %{count: count()}
),
on: true,
order_by: [desc: :last_message_at],
preload: [:to, :from],
select: %{c | message_count: cnt.count}
Repo.paginate(query, pagination)
end
@doc """
Creates a conversation.
## Examples
iex> create_conversation(from, to, %{field: value})
{:ok, %Conversation{}}
iex> create_conversation(from, to, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_conversation(from, attrs \\ %{}) do
to = Users.get_user_by_name(attrs["recipient"])
%Conversation{}
|> Conversation.creation_changeset(from, to, attrs)
|> Repo.insert()
|> case do
{:ok, conversation} ->
report_non_approved_message(hd(conversation.messages))
{:ok, conversation}
error ->
error
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking conversation changes.
## Examples
iex> change_conversation(conversation)
%Ecto.Changeset{source: %Conversation{}}
"""
def change_conversation(%Conversation{} = conversation) do
Conversation.changeset(conversation, %{})
end
@doc """
Marks a conversation as read or unread from the perspective of the given user.
@ -138,6 +190,59 @@ defmodule Philomena.Conversations do
end
end
@doc """
Returns the number of messages in the given conversation.
## Example
iex> count_messages(%Conversation{})
3
"""
def count_messages(conversation) do
Message
|> where(conversation_id: ^conversation.id)
|> Repo.aggregate(:count)
end
@doc """
Returns a `m:Scrivener.Page` of 2-tuples of messages and rendered output
within a conversation.
Messages are ordered by user message preference (`messages_newest_first`).
When coerced to a list and rendered as Markdown, the result may look like:
[
{%Message{body: "hello *world*"}, "hello <strong>world</strong>"}
]
## Example
iex> list_messages(%Conversation{}, %User{}, & &1.body, page_size: 10)
%Scrivener.Page{}
"""
def list_messages(conversation, user, collection_renderer, pagination) do
direction =
if user.messages_newest_first do
:desc
else
:asc
end
query =
from m in Message,
where: m.conversation_id == ^conversation.id,
order_by: [{^direction, :created_at}],
preload: :from
messages = Repo.paginate(query, pagination)
rendered = collection_renderer.(messages)
put_in(messages.entries, Enum.zip(messages.entries, rendered))
end
@doc """
Creates a message within a conversation.

View file

@ -4,7 +4,6 @@ defmodule Philomena.Conversations.Conversation do
alias Philomena.Users.User
alias Philomena.Conversations.Message
alias Philomena.Repo
@derive {Phoenix.Param, key: :slug}
@ -20,6 +19,8 @@ defmodule Philomena.Conversations.Conversation do
field :from_hidden, :boolean, default: false
field :slug, :string
field :last_message_at, :utc_datetime
field :message_count, :integer, virtual: true
field :recipient, :string, virtual: true
timestamps(inserted_at: :created_at, type: :utc_datetime)
@ -43,17 +44,17 @@ defmodule Philomena.Conversations.Conversation do
end
@doc false
def creation_changeset(conversation, from, attrs) do
def creation_changeset(conversation, from, to, attrs) do
conversation
|> cast(attrs, [:title, :recipient])
|> validate_required([:title, :recipient])
|> validate_length(:title, max: 300, count: :bytes)
|> cast(attrs, [:title])
|> put_assoc(:from, from)
|> put_recipient()
|> set_slug()
|> set_last_message()
|> put_assoc(:to, to)
|> put_change(:slug, Ecto.UUID.generate())
|> cast_assoc(:messages, with: &Message.creation_changeset(&1, &2, from))
|> set_last_message()
|> validate_length(:messages, is: 1)
|> validate_length(:title, max: 300, count: :bytes)
|> validate_required([:title, :from, :to])
end
@doc false
@ -64,21 +65,7 @@ defmodule Philomena.Conversations.Conversation do
|> set_last_message()
end
defp set_slug(changeset) do
changeset
|> change(slug: Ecto.UUID.generate())
end
defp set_last_message(changeset) do
change(changeset, last_message_at: DateTime.utc_now(:second))
end
defp put_recipient(changeset) do
recipient = changeset |> get_field(:recipient)
user = Repo.get_by(User, name: recipient)
changeset
|> put_change(:to, user)
|> validate_required(:to)
end
end

View file

@ -55,6 +55,22 @@ defmodule Philomena.Users do
Repo.get_by(User, email: email)
end
@doc """
Gets a user by name.
## Examples
iex> get_user_by_name("Administrator")
%User{}
iex> get_user_by_name("nonexistent")
nil
"""
def get_user_by_name(name) when is_binary(name) do
Repo.get_by(User, name: name)
end
@doc """
Gets a user by email and password.

View file

@ -1,10 +1,8 @@
defmodule PhilomenaWeb.Conversation.MessageController do
use PhilomenaWeb, :controller
alias Philomena.Conversations.{Conversation, Message}
alias Philomena.Conversations.Conversation
alias Philomena.Conversations
alias Philomena.Repo
import Ecto.Query
plug PhilomenaWeb.FilterBannedUsersPlug
plug PhilomenaWeb.CanaryMapPlug, create: :show
@ -15,20 +13,16 @@ defmodule PhilomenaWeb.Conversation.MessageController do
id_field: "slug",
persisted: true
@page_size 25
def create(conn, %{"message" => message_params}) do
conversation = conn.assigns.conversation
user = conn.assigns.current_user
case Conversations.create_message(conversation, user, message_params) do
{:ok, _message} ->
count =
Message
|> where(conversation_id: ^conversation.id)
|> Repo.aggregate(:count, :id)
page =
Float.ceil(count / 25)
|> round()
count = Conversations.count_messages(conversation)
page = div(count + @page_size - 1, @page_size)
conn
|> put_flash(:info, "Message successfully sent.")

View file

@ -4,8 +4,6 @@ defmodule PhilomenaWeb.ConversationController do
alias PhilomenaWeb.NotificationCountPlug
alias Philomena.{Conversations, Conversations.Conversation, Conversations.Message}
alias PhilomenaWeb.MarkdownRenderer
alias Philomena.Repo
import Ecto.Query
plug PhilomenaWeb.FilterBannedUsersPlug when action in [:new, :create]
@ -19,42 +17,17 @@ defmodule PhilomenaWeb.ConversationController do
only: :show,
preload: [:to, :from]
def index(conn, %{"with" => partner}) do
def index(conn, params) do
user = conn.assigns.current_user
Conversation
|> where(
[c],
(c.from_id == ^user.id and c.to_id == ^partner and not c.from_hidden) or
(c.to_id == ^user.id and c.from_id == ^partner and not c.to_hidden)
)
|> load_conversations(conn)
end
def index(conn, _params) do
user = conn.assigns.current_user
Conversation
|> where(
[c],
(c.from_id == ^user.id and not c.from_hidden) or (c.to_id == ^user.id and not c.to_hidden)
)
|> load_conversations(conn)
end
defp load_conversations(queryable, conn) do
conversations =
queryable
|> join(
:inner_lateral,
[c],
_ in fragment("SELECT COUNT(*) FROM messages m WHERE m.conversation_id = ?", c.id),
on: true
)
|> order_by(desc: :last_message_at)
|> preload([:to, :from])
|> select([c, cnt], {c, cnt.count})
|> Repo.paginate(conn.assigns.scrivener)
case params do
%{"with" => partner_id} ->
Conversations.list_conversations_with(partner_id, user, conn.assigns.scrivener)
_ ->
Conversations.list_conversations(user, conn.assigns.scrivener)
end
render(conn, "index.html", title: "Conversations", conversations: conversations)
end
@ -62,27 +35,17 @@ defmodule PhilomenaWeb.ConversationController do
def show(conn, _params) do
conversation = conn.assigns.conversation
user = conn.assigns.current_user
pref = load_direction(user)
messages =
Message
|> where(conversation_id: ^conversation.id)
|> order_by([{^pref, :created_at}])
|> preload([:from])
|> Repo.paginate(conn.assigns.scrivener)
Conversations.list_messages(
conversation,
user,
&MarkdownRenderer.render_collection(&1, conn),
conn.assigns.scrivener
)
rendered =
messages.entries
|> MarkdownRenderer.render_collection(conn)
messages = %{messages | entries: Enum.zip(messages.entries, rendered)}
changeset =
%Message{}
|> Conversations.change_message()
conversation
|> Conversations.mark_conversation_read(user)
changeset = Conversations.change_message(%Message{})
Conversations.mark_conversation_read(conversation, user)
# Update the conversation ticker in the header
conn = NotificationCountPlug.call(conn)
@ -96,9 +59,10 @@ defmodule PhilomenaWeb.ConversationController do
end
def new(conn, params) do
changeset =
conversation =
%Conversation{recipient: params["recipient"], messages: [%Message{}]}
|> Conversations.change_conversation()
changeset = Conversations.change_conversation(conversation)
render(conn, "new.html", title: "New Conversation", changeset: changeset)
end
@ -116,7 +80,4 @@ defmodule PhilomenaWeb.ConversationController do
render(conn, "new.html", changeset: changeset)
end
end
defp load_direction(%{messages_newest_first: false}), do: :asc
defp load_direction(_user), do: :desc
end

View file

@ -20,14 +20,14 @@ h1 My Conversations
th.table--communication-list__stats With
th.table--communication-list__options Options
tbody
= for {c, count} <- @conversations do
= for c <- @conversations do
tr class=conversation_class(@conn.assigns.current_user, c)
td.table--communication-list__name
=> link c.title, to: ~p"/conversations/#{c}"
.small-text.hide-mobile
=> count
= pluralize("message", "messages", count)
=> c.message_count
= pluralize("message", "messages", c.message_count)
' ; started
= pretty_time(c.created_at)
' , last message
@ -36,7 +36,7 @@ h1 My Conversations
td.table--communication-list__stats
= render PhilomenaWeb.UserAttributionView, "_user.html", object: %{user: other_party(@current_user, c)}, conn: @conn
td.table--communication-list__options
=> link "Last message", to: last_message_path(c, count)
=> link "Last message", to: last_message_path(c, c.message_count)
' &bull;
=> link "Hide", to: ~p"/conversations/#{c}/hide", data: [method: "post"], data: [confirm: "Are you really, really sure?"]