mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
conversations
This commit is contained in:
parent
bdec219d23
commit
acb50f3efe
12 changed files with 256 additions and 40 deletions
|
@ -4,23 +4,11 @@ defmodule Philomena.Conversations do
|
|||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Ecto.Multi
|
||||
alias Philomena.Repo
|
||||
|
||||
alias Philomena.Conversations.Conversation
|
||||
|
||||
@doc """
|
||||
Returns the list of conversations.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_conversations()
|
||||
[%Conversation{}, ...]
|
||||
|
||||
"""
|
||||
def list_conversations do
|
||||
Repo.all(Conversation)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single conversation.
|
||||
|
||||
|
@ -49,9 +37,9 @@ defmodule Philomena.Conversations do
|
|||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_conversation(attrs \\ %{}) do
|
||||
def create_conversation(from, attrs \\ %{}) do
|
||||
%Conversation{}
|
||||
|> Conversation.changeset(attrs)
|
||||
|> Conversation.creation_changeset(from, attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
|
@ -117,20 +105,19 @@ defmodule Philomena.Conversations do
|
|||
|> Repo.aggregate(:count, :id)
|
||||
end
|
||||
|
||||
alias Philomena.Conversations.Message
|
||||
|
||||
@doc """
|
||||
Returns the list of messages.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_messages()
|
||||
[%Message{}, ...]
|
||||
|
||||
"""
|
||||
def list_messages do
|
||||
Repo.all(Message)
|
||||
def mark_conversation_read(%Conversation{to_id: user_id} = conversation, %{id: user_id}) do
|
||||
conversation
|
||||
|> Conversation.read_changeset(%{to_read: true})
|
||||
|> Repo.update()
|
||||
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 """
|
||||
Gets a single message.
|
||||
|
@ -160,10 +147,21 @@ defmodule Philomena.Conversations do
|
|||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_message(attrs \\ %{}) do
|
||||
%Message{}
|
||||
|> Message.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
def create_message(conversation, user, attrs \\ %{}) do
|
||||
message =
|
||||
Ecto.build_assoc(conversation, :messages)
|
||||
|> 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
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -3,12 +3,15 @@ defmodule Philomena.Conversations.Conversation do
|
|||
import Ecto.Changeset
|
||||
|
||||
alias Philomena.Users.User
|
||||
alias Philomena.Conversations.Message
|
||||
alias Philomena.Repo
|
||||
|
||||
@derive {Phoenix.Param, key: :slug}
|
||||
|
||||
schema "conversations" do
|
||||
belongs_to :from, User
|
||||
belongs_to :to, User
|
||||
has_many :messages, Message
|
||||
|
||||
field :title, :string
|
||||
field :to_read, :boolean, default: false
|
||||
|
@ -16,7 +19,8 @@ defmodule Philomena.Conversations.Conversation do
|
|||
field :to_hidden, :boolean, default: false
|
||||
field :from_hidden, :boolean, default: false
|
||||
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)
|
||||
end
|
||||
|
@ -27,4 +31,41 @@ defmodule Philomena.Conversations.Conversation do
|
|||
|> cast(attrs, [])
|
||||
|> validate_required([])
|
||||
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
|
||||
|
|
|
@ -20,4 +20,13 @@ defmodule Philomena.Conversations.Message do
|
|||
|> cast(attrs, [])
|
||||
|> validate_required([])
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -1,7 +1,7 @@
|
|||
defmodule PhilomenaWeb.ConversationController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias Philomena.Conversations.{Conversation, Message}
|
||||
alias Philomena.{Conversations, Conversations.Conversation, Conversations.Message}
|
||||
alias Philomena.Textile.Renderer
|
||||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
@ -24,6 +24,7 @@ defmodule PhilomenaWeb.ConversationController do
|
|||
|
||||
def show(conn, _params) do
|
||||
conversation = conn.assigns.conversation
|
||||
user = conn.assigns.current_user
|
||||
|
||||
messages =
|
||||
Message
|
||||
|
@ -39,6 +40,38 @@ defmodule PhilomenaWeb.ConversationController do
|
|||
messages =
|
||||
%{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
|
|
@ -55,7 +55,9 @@ defmodule PhilomenaWeb.Router do
|
|||
pipe_through [:browser, :ensure_totp, :protected]
|
||||
|
||||
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 "/vote", Image.VoteController, only: [:create, :delete], singleton: true
|
||||
resources "/fave", Image.FaveController, only: [:create, :delete], singleton: true
|
||||
|
|
|
@ -5,6 +5,10 @@ elixir:
|
|||
h1 My Conversations
|
||||
.block
|
||||
.block__header
|
||||
a href=Routes.conversation_path(@conn, :new)
|
||||
i.fa.fa-paper-plane>
|
||||
' Create New Conversation
|
||||
|
||||
= pagination
|
||||
|
||||
.block__content
|
||||
|
@ -16,7 +20,7 @@ h1 My Conversations
|
|||
th.table--communication-list__options Options
|
||||
tbody
|
||||
= for c <- @conversations do
|
||||
tr
|
||||
tr class=conversation_class(@conn.assigns.current_user, c)
|
||||
td.table--communication-list__name
|
||||
=> link c.title, to: Routes.conversation_path(@conn, :show, c)
|
||||
|
||||
|
|
|
@ -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..."]
|
41
lib/philomena_web/templates/conversation/new.html.slime
Normal file
41
lib/philomena_web/templates/conversation/new.html.slime
Normal file
|
@ -0,0 +1,41 @@
|
|||
h1 New Conversation
|
||||
.block
|
||||
.block__header
|
||||
=> link "Conversations", to: Routes.conversation_path(@conn, :index)
|
||||
' »
|
||||
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..."]
|
|
@ -1,7 +1,7 @@
|
|||
elixir:
|
||||
route = fn p -> Routes.conversation_path(@conn, :show, @conversation, p) end
|
||||
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
|
||||
.block
|
||||
|
@ -20,3 +20,14 @@ h1 = @conversation.title
|
|||
.block
|
||||
.block__header.block__header--light
|
||||
= 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.
|
||||
|
|
3
lib/philomena_web/views/conversation/message_view.ex
Normal file
3
lib/philomena_web/views/conversation/message_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule PhilomenaWeb.Conversation.MessageView do
|
||||
use PhilomenaWeb, :view
|
||||
end
|
|
@ -1,9 +1,27 @@
|
|||
defmodule PhilomenaWeb.ConversationView do
|
||||
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
|
||||
|
||||
def other_party(_user_id, conversation),
|
||||
def other_party(_user, conversation),
|
||||
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
|
Loading…
Reference in a new issue