conversations

This commit is contained in:
byte[] 2019-11-18 11:00:08 -05:00
parent bdec219d23
commit acb50f3efe
12 changed files with 256 additions and 40 deletions

View file

@ -4,23 +4,11 @@ defmodule Philomena.Conversations do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Ecto.Multi
alias Philomena.Repo alias Philomena.Repo
alias Philomena.Conversations.Conversation alias Philomena.Conversations.Conversation
@doc """
Returns the list of conversations.
## Examples
iex> list_conversations()
[%Conversation{}, ...]
"""
def list_conversations do
Repo.all(Conversation)
end
@doc """ @doc """
Gets a single conversation. Gets a single conversation.
@ -49,9 +37,9 @@ defmodule Philomena.Conversations do
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def create_conversation(attrs \\ %{}) do def create_conversation(from, attrs \\ %{}) do
%Conversation{} %Conversation{}
|> Conversation.changeset(attrs) |> Conversation.creation_changeset(from, attrs)
|> Repo.insert() |> Repo.insert()
end end
@ -117,20 +105,19 @@ defmodule Philomena.Conversations do
|> Repo.aggregate(:count, :id) |> Repo.aggregate(:count, :id)
end end
alias Philomena.Conversations.Message def mark_conversation_read(%Conversation{to_id: user_id} = conversation, %{id: user_id}) do
conversation
@doc """ |> Conversation.read_changeset(%{to_read: true})
Returns the list of messages. |> Repo.update()
## Examples
iex> list_messages()
[%Message{}, ...]
"""
def list_messages do
Repo.all(Message)
end end
def mark_conversation_read(%Conversation{from_id: user_id} = conversation, %{id: user_id}) do
conversation
|> Conversation.read_changeset(%{from_read: true})
|> Repo.update()
end
def mark_conversation_read(_conversation, _user), do: {:ok, nil}
alias Philomena.Conversations.Message
@doc """ @doc """
Gets a single message. Gets a single message.
@ -160,10 +147,21 @@ defmodule Philomena.Conversations do
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def create_message(attrs \\ %{}) do def create_message(conversation, user, attrs \\ %{}) do
%Message{} message =
|> Message.changeset(attrs) Ecto.build_assoc(conversation, :messages)
|> Repo.insert() |> Message.creation_changeset(attrs, user)
conversation_query =
Conversation
|> where(id: ^conversation.id)
now = DateTime.utc_now()
Multi.new
|> Multi.insert(:message, message)
|> Multi.update_all(:conversation, conversation_query, set: [from_read: false, to_read: false, last_message_at: now])
|> Repo.isolated_transaction(:serializable)
end end
@doc """ @doc """

View file

@ -3,12 +3,15 @@ defmodule Philomena.Conversations.Conversation do
import Ecto.Changeset import Ecto.Changeset
alias Philomena.Users.User alias Philomena.Users.User
alias Philomena.Conversations.Message
alias Philomena.Repo
@derive {Phoenix.Param, key: :slug} @derive {Phoenix.Param, key: :slug}
schema "conversations" do schema "conversations" do
belongs_to :from, User belongs_to :from, User
belongs_to :to, User belongs_to :to, User
has_many :messages, Message
field :title, :string field :title, :string
field :to_read, :boolean, default: false field :to_read, :boolean, default: false
@ -16,7 +19,8 @@ defmodule Philomena.Conversations.Conversation do
field :to_hidden, :boolean, default: false field :to_hidden, :boolean, default: false
field :from_hidden, :boolean, default: false field :from_hidden, :boolean, default: false
field :slug, :string field :slug, :string
field :last_message_at, :naive_datetime field :last_message_at, :utc_datetime
field :recipient, :string, virtual: true
timestamps(inserted_at: :created_at) timestamps(inserted_at: :created_at)
end end
@ -27,4 +31,41 @@ defmodule Philomena.Conversations.Conversation do
|> cast(attrs, []) |> cast(attrs, [])
|> validate_required([]) |> validate_required([])
end end
def read_changeset(conversation, attrs) do
conversation
|> cast(attrs, [:from_read, :to_read])
end
@doc false
def creation_changeset(conversation, from, attrs) do
conversation
|> cast(attrs, [:title, :recipient])
|> validate_required([:title, :recipient])
|> validate_length(:title, max: 300, count: :bytes)
|> put_assoc(:from, from)
|> put_recipient()
|> set_slug()
|> set_last_message()
|> cast_assoc(:messages, with: {Message, :creation_changeset, [from]})
end
defp set_slug(changeset) do
changeset
|> change(slug: Ecto.UUID.generate())
end
defp set_last_message(changeset) do
changeset
|> change(last_message_at: DateTime.utc_now() |> DateTime.truncate(:second))
end
defp put_recipient(changeset) do
recipient = changeset |> get_field(:recipient)
user = Repo.get_by(User, name: recipient) |> IO.inspect()
changeset
|> put_change(:to, user)
|> validate_required(:to)
end
end end

View file

@ -20,4 +20,13 @@ defmodule Philomena.Conversations.Message do
|> cast(attrs, []) |> cast(attrs, [])
|> validate_required([]) |> validate_required([])
end end
@doc false
def creation_changeset(message, attrs, user) do
message
|> cast(attrs, [:body])
|> validate_required([:body])
|> put_assoc(:from, user)
|> validate_length(:body, max: 300_000, count: :bytes)
end
end end

View file

@ -0,0 +1,37 @@
defmodule PhilomenaWeb.Conversation.MessageController do
use PhilomenaWeb, :controller
alias Philomena.Conversations.{Conversation, Message}
alias Philomena.Conversations
alias Philomena.Repo
import Ecto.Query
plug PhilomenaWeb.FilterBannedUsersPlug
plug :load_and_authorize_resource, model: Conversation, id_name: "conversation_id", id_field: "slug", persisted: true
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, _result} ->
count =
Message
|> where(conversation_id: ^conversation.id)
|> Repo.aggregate(:count, :id)
page =
Float.ceil(count / 25)
|> round()
conn
|> put_flash(:info, "Message successfully sent.")
|> redirect(to: Routes.conversation_path(conn, :show, conversation, page: page))
_error ->
conn
|> put_flash(:error, "There was an error posting your message")
|> redirect(external: conn.assigns.referrer)
end
end
end

View file

@ -1,7 +1,7 @@
defmodule PhilomenaWeb.ConversationController do defmodule PhilomenaWeb.ConversationController do
use PhilomenaWeb, :controller use PhilomenaWeb, :controller
alias Philomena.Conversations.{Conversation, Message} alias Philomena.{Conversations, Conversations.Conversation, Conversations.Message}
alias Philomena.Textile.Renderer alias Philomena.Textile.Renderer
alias Philomena.Repo alias Philomena.Repo
import Ecto.Query import Ecto.Query
@ -24,6 +24,7 @@ defmodule PhilomenaWeb.ConversationController do
def show(conn, _params) do def show(conn, _params) do
conversation = conn.assigns.conversation conversation = conn.assigns.conversation
user = conn.assigns.current_user
messages = messages =
Message Message
@ -39,6 +40,38 @@ defmodule PhilomenaWeb.ConversationController do
messages = messages =
%{messages | entries: Enum.zip(messages.entries, rendered)} %{messages | entries: Enum.zip(messages.entries, rendered)}
render(conn, "show.html", conversation: conversation, messages: messages) changeset =
%Message{}
|> Conversations.change_message()
conversation
|> Conversations.mark_conversation_read(user)
render(conn, "show.html", conversation: conversation, messages: messages, changeset: changeset)
end
def new(conn, params) do
changeset =
%Conversation{recipient: params["recipient"], messages: [%Message{}]}
|> Conversations.change_conversation()
render(conn, "new.html", changeset: changeset)
end
# Somewhat annoying, cast_assoc has no "limit" validation so we force it
# here to require exactly 1
def create(conn, %{"conversation" => %{"messages" => %{"0" => _message_params}} = conversation_params}) do
user = conn.assigns.current_user
case Conversations.create_conversation(user, conversation_params) do
{:ok, conversation} ->
conn
|> put_flash(:info, "Conversation successfully created.")
|> redirect(to: Routes.conversation_path(conn, :show, conversation))
{:error, changeset} ->
conn
|> render("new.html", changeset: changeset)
end
end end
end end

View file

@ -55,7 +55,9 @@ defmodule PhilomenaWeb.Router do
pipe_through [:browser, :ensure_totp, :protected] pipe_through [:browser, :ensure_totp, :protected]
resources "/notifications", NotificationController, only: [:index, :delete] resources "/notifications", NotificationController, only: [:index, :delete]
resources "/conversations", ConversationController, only: [:index, :show] resources "/conversations", ConversationController, only: [:index, :show, :new, :create] do
resources "/messages", Conversation.MessageController, only: [:create]
end
resources "/images", ImageController, only: [] do resources "/images", ImageController, only: [] do
resources "/vote", Image.VoteController, only: [:create, :delete], singleton: true resources "/vote", Image.VoteController, only: [:create, :delete], singleton: true
resources "/fave", Image.FaveController, only: [:create, :delete], singleton: true resources "/fave", Image.FaveController, only: [:create, :delete], singleton: true

View file

@ -5,6 +5,10 @@ elixir:
h1 My Conversations h1 My Conversations
.block .block
.block__header .block__header
a href=Routes.conversation_path(@conn, :new)
i.fa.fa-paper-plane>
' Create New Conversation
= pagination = pagination
.block__content .block__content
@ -16,7 +20,7 @@ h1 My Conversations
th.table--communication-list__options Options th.table--communication-list__options Options
tbody tbody
= for c <- @conversations do = for c <- @conversations do
tr tr class=conversation_class(@conn.assigns.current_user, c)
td.table--communication-list__name td.table--communication-list__name
=> link c.title, to: Routes.conversation_path(@conn, :show, c) => link c.title, to: Routes.conversation_path(@conn, :show, c)

View file

@ -0,0 +1,19 @@
= form_for @changeset, Routes.conversation_message_path(@conn, :create, @conversation), fn f ->
.block
.block__header.block__header--js-tabbed
a.selected href="#" data-click-tab="write"
i.fa.fa-pencil-alt>
| Reply
a href="#" data-click-tab="preview"
i.fa.fa-eye>
| Preview
.block__tab.communication-edit__tab.selected data-tab="write"
= textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Your message", required: true
.block__tab.communication-edit__tab.hidden data-tab="preview"
| [Loading preview...]
.block__content.communication-edit__actions
= submit "Send", class: "button", autocomplete: "off", data: [disable_with: "Sending..."]

View file

@ -0,0 +1,41 @@
h1 New Conversation
.block
.block__header
=> link "Conversations", to: Routes.conversation_path(@conn, :index)
' &raquo;
span.block__header__title New Conversation
= form_for @changeset, Routes.conversation_path(@conn, :create), fn f ->
= if @changeset.action do
.alert.alert-danger
p Oops, something went wrong! Please check the errors below.
.field
.fieldlabel Specify any user's exact name here, case-sensitive
= text_input f, :recipient, class: "input input--wide", placeholder: "Recipient", required: true
= error_tag f, :to
.field
= text_input f, :title, class: "input input--wide", placeholder: "Title", required: true
= error_tag f, :title
= inputs_for f, :messages, fn fm ->
.block
.block__header.block__header--js-tabbed
a.selected href="#" data-click-tab="write"
i.fa.fa-pencil-alt>
| Reply
a href="#" data-click-tab="preview"
i.fa.fa-eye>
| Preview
.block__tab.communication-edit__tab.selected data-tab="write"
= textarea fm, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Your message", required: true
= error_tag fm, :body
.block__tab.communication-edit__tab.hidden data-tab="preview"
| [Loading preview...]
.block__content.communication-edit__actions
= submit "Send", class: "button", autocomplete: "off", data: [disable_with: "Sending..."]

View file

@ -1,7 +1,7 @@
elixir: elixir:
route = fn p -> Routes.conversation_path(@conn, :show, @conversation, p) end route = fn p -> Routes.conversation_path(@conn, :show, @conversation, p) end
pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @messages, route: route, conn: @conn pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @messages, route: route, conn: @conn
other = other_party(@current_user.id, @conversation) other = other_party(@current_user, @conversation)
h1 = @conversation.title h1 = @conversation.title
.block .block
@ -20,3 +20,14 @@ h1 = @conversation.title
.block .block
.block__header.block__header--light .block__header.block__header--light
= pagination = pagination
= if @messages.total_entries < 1_000 do
= render PhilomenaWeb.Conversation.MessageView, "_form.html", conversation: @conversation, changeset: @changeset, conn: @conn
- else
div
h2 Okay, we're impressed
p You've managed to send over 1,000 messages in this conversation!
p We'd like to ask you to make a new conversation. Don't worry, this one won't go anywhere if you need to refer back to it.
p
=> link "Click here", to: Routes.conversation_path(@conn, :new, receipient: other.name)
' to make a new conversation with this user.

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.Conversation.MessageView do
use PhilomenaWeb, :view
end

View file

@ -1,9 +1,27 @@
defmodule PhilomenaWeb.ConversationView do defmodule PhilomenaWeb.ConversationView do
use PhilomenaWeb, :view use PhilomenaWeb, :view
def other_party(user_id, %{to_id: user_id} = conversation), def other_party(%{id: user_id}, %{to_id: user_id} = conversation),
do: conversation.from do: conversation.from
def other_party(_user_id, conversation), def other_party(_user, conversation),
do: conversation.to do: conversation.to
def read_by?(%{id: user_id}, %{to_id: user_id} = conversation),
do: conversation.to_read
def read_by?(%{id: user_id}, %{from_id: user_id} = conversation),
do: conversation.from_read
def read_by?(_user, _conversation),
do: false
def conversation_class(user, conversation) do
case read_by?(user, conversation) do
false -> "warning"
_ -> nil
end
end
end end