diff --git a/lib/philomena/conversations.ex b/lib/philomena/conversations.ex index 9f91a3b3..7b5d4e9d 100644 --- a/lib/philomena/conversations.ex +++ b/lib/philomena/conversations.ex @@ -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 """ diff --git a/lib/philomena/conversations/conversation.ex b/lib/philomena/conversations/conversation.ex index 5e82630b..62ddbed2 100644 --- a/lib/philomena/conversations/conversation.ex +++ b/lib/philomena/conversations/conversation.ex @@ -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 diff --git a/lib/philomena/conversations/message.ex b/lib/philomena/conversations/message.ex index 7ee9c008..80ac7487 100644 --- a/lib/philomena/conversations/message.ex +++ b/lib/philomena/conversations/message.ex @@ -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 diff --git a/lib/philomena_web/controllers/conversation/message_controller.ex b/lib/philomena_web/controllers/conversation/message_controller.ex new file mode 100644 index 00000000..95145c44 --- /dev/null +++ b/lib/philomena_web/controllers/conversation/message_controller.ex @@ -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 \ No newline at end of file diff --git a/lib/philomena_web/controllers/conversation_controller.ex b/lib/philomena_web/controllers/conversation_controller.ex index d4ffb4d9..8750180d 100644 --- a/lib/philomena_web/controllers/conversation_controller.ex +++ b/lib/philomena_web/controllers/conversation_controller.ex @@ -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 \ No newline at end of file diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 4c512638..5773bfc1 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -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 diff --git a/lib/philomena_web/templates/conversation/index.html.slime b/lib/philomena_web/templates/conversation/index.html.slime index d64bf3e8..0cbc991c 100644 --- a/lib/philomena_web/templates/conversation/index.html.slime +++ b/lib/philomena_web/templates/conversation/index.html.slime @@ -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) diff --git a/lib/philomena_web/templates/conversation/message/_form.html.slime b/lib/philomena_web/templates/conversation/message/_form.html.slime new file mode 100644 index 00000000..d8de7f2a --- /dev/null +++ b/lib/philomena_web/templates/conversation/message/_form.html.slime @@ -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..."] diff --git a/lib/philomena_web/templates/conversation/new.html.slime b/lib/philomena_web/templates/conversation/new.html.slime new file mode 100644 index 00000000..3da0a3da --- /dev/null +++ b/lib/philomena_web/templates/conversation/new.html.slime @@ -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..."] \ No newline at end of file diff --git a/lib/philomena_web/templates/conversation/show.html.slime b/lib/philomena_web/templates/conversation/show.html.slime index c9902bd0..3863e07a 100644 --- a/lib/philomena_web/templates/conversation/show.html.slime +++ b/lib/philomena_web/templates/conversation/show.html.slime @@ -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. diff --git a/lib/philomena_web/views/conversation/message_view.ex b/lib/philomena_web/views/conversation/message_view.ex new file mode 100644 index 00000000..6dc4e279 --- /dev/null +++ b/lib/philomena_web/views/conversation/message_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.Conversation.MessageView do + use PhilomenaWeb, :view +end diff --git a/lib/philomena_web/views/conversation_view.ex b/lib/philomena_web/views/conversation_view.ex index b4c2c1d7..973656de 100644 --- a/lib/philomena_web/views/conversation_view.ex +++ b/lib/philomena_web/views/conversation_view.ex @@ -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 \ No newline at end of file