mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +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
|
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 """
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
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
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
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.
|
||||||
|
|
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
|
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
|
Loading…
Reference in a new issue