8.7 KiB · text 5af55d9
defmodule GitGudWeb.IssueLive.Show do
use GitGudWeb, :live_view
alias GitGud.Issues
alias GitGud.Issues.IssueComment
alias GitGud.Labels
alias GitGud.Repositories
alias GitGud.Repositories.Storage
@impl true
def mount(%{"owner" => owner, "name" => name, "number" => number}, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
issue = Issues.get_issue!(repo, String.to_integer(number))
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> assign(:issue, issue)
|> assign(:all_labels, Labels.list_labels(repo))
|> assign_comment_form()
|> assign(:comment_body, "")
|> assign(:page_title, "##{issue.number} #{issue.title}")}
end
defp assign_comment_form(socket),
do: assign(socket, :comment_form, to_form(IssueComment.changeset(%IssueComment{}, %{})))
@impl true
def handle_event("toggle_state", _params, socket) do
new_state = if socket.assigns.issue.state == "open", do: "closed", else: "open"
{:ok, _} = Issues.set_state(socket.assigns.issue, new_state)
{:noreply, reload(socket)}
end
def handle_event("toggle_label", %{"id" => label_id}, socket) do
label = Labels.get_label!(label_id)
issue = socket.assigns.issue
attached? = Enum.any?(issue.labels, &(&1.id == label.id))
if attached?,
do: Issues.detach_label(issue, label),
else: Issues.attach_label(issue, label)
{:noreply, reload(socket)}
end
def handle_event("update_comment_body", %{"issue_comment" => %{"body" => body}}, socket) do
{:noreply, assign(socket, :comment_body, body)}
end
def handle_event("quote_reply", %{"id" => id}, socket) do
case Enum.find(socket.assigns.issue.comments, &(&1.id == String.to_integer(id))) do
nil ->
{:noreply, socket}
comment ->
author = author_name(comment.author)
quoted = GitGud.Markdown.quote_block(comment.body, author)
next = append_with_blank(socket.assigns.comment_body, quoted)
{:noreply, assign(socket, :comment_body, next)}
end
end
def handle_event("add_comment", %{"issue_comment" => attrs}, socket) do
user = socket.assigns.current_scope.user
case Issues.add_comment(socket.assigns.issue, user, attrs) do
{:ok, _} ->
{:noreply, socket |> assign_comment_form() |> assign(:comment_body, "") |> reload()}
{:error, cs} ->
{:noreply, assign(socket, :comment_form, to_form(cs))}
end
end
def handle_event(
"report",
%{"target_type" => type, "target_id" => id, "reason" => reason},
socket
) do
case socket.assigns.current_scope && socket.assigns.current_scope.user do
nil ->
{:noreply, put_flash(socket, :error, "Sign in to report.")}
user ->
case GitGud.Reports.open(user, {type, String.to_integer(id)}, reason) do
{:ok, _} ->
{:noreply, put_flash(socket, :info, "Report submitted. Thanks.")}
{:error, :already_moderated} ->
{:noreply,
put_flash(socket, :info, "This content has already been moderated.")}
{:error, %Ecto.Changeset{}} ->
{:noreply, put_flash(socket, :info, "Already reported.")}
end
end
end
defp reload(socket) do
assign(socket, :issue, Issues.get_issue!(socket.assigns.repo, socket.assigns.issue.number))
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-6">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
/>
<header>
<p class="text-xs opacity-60">
<.link navigate={~p"/r/#{@handle}/#{@repo.name}/issues"} class="link link-hover">
Back to issues
</.link>
</p>
<h1 class="text-2xl font-semibold">
#{@issue.number}{@issue.title}
</h1>
<p class="text-xs opacity-60">
<span class={["badge", state_class(@issue.state)]}>{@issue.state}</span>
opened by {author_name(@issue.author)} on {format_dt(@issue.inserted_at)}
</p>
</header>
<section class="border border-base-300 rounded p-4">
<GitGudWeb.ModerationComponents.moderated_body
target={@issue}
viewer={@current_scope && @current_scope.user}
repo={@repo}
theme={@editor_theme}
/>
</section>
<section>
<h2 class="font-semibold mb-2">Labels</h2>
<div class="flex flex-wrap gap-1">
<button
:for={l <- @all_labels}
type="button"
phx-click="toggle_label"
phx-value-id={l.id}
class={[
"badge",
Enum.any?(@issue.labels, &(&1.id == l.id)) || "opacity-40"
]}
style={"background-color: " <> l.color <> "; color: white"}
>
{l.name}
</button>
</div>
</section>
<section>
<h2 class="font-semibold mb-2">Comments</h2>
<ul class="space-y-3">
<li :for={c <- @issue.comments} class="border border-base-300 rounded p-3">
<div class="flex items-baseline justify-between">
<p class="text-xs opacity-60 mb-1 flex items-center gap-1.5">
<.avatar name={author_name(c.author)} size="xs" alt={author_name(c.author)} />
{author_name(c.author)} · {format_dt(c.inserted_at)}
</p>
<div class="flex gap-1">
<button
:if={@current_scope && @current_scope.user}
type="button"
phx-click="quote_reply"
phx-value-id={c.id}
class="btn btn-xs btn-ghost opacity-60 hover:opacity-100"
title="Quote in reply"
>
<.icon name="hero-chat-bubble-left" class="size-3" /> Quote
</button>
<button
:if={@current_scope && @current_scope.user && not GitGud.Issues.IssueComment.moderated?(c)}
type="button"
phx-click="report"
phx-value-target_type="issue_comment"
phx-value-target_id={c.id}
phx-value-reason="abuse"
class="btn btn-xs btn-ghost opacity-60 hover:opacity-100"
data-confirm="Report this comment for abuse?"
title="Report"
>
<.icon name="hero-flag" class="size-3" />
</button>
</div>
</div>
<GitGudWeb.ModerationComponents.moderated_body
target={c}
viewer={@current_scope && @current_scope.user}
repo={@repo}
theme={@editor_theme}
empty_text="(empty comment)"
/>
</li>
</ul>
<.form
for={@comment_form}
id="comment-form"
phx-change="update_comment_body"
phx-submit="add_comment"
class="mt-4 space-y-2"
>
<label class="label">Add a comment</label>
<textarea
id="issue_comment_body"
name="issue_comment[body]"
rows="6"
class="textarea textarea-bordered w-full font-mono text-sm"
phx-debounce="200"
>{@comment_body}</textarea>
<div class="flex justify-end gap-2">
<button
type="button"
phx-click="toggle_state"
class="btn btn-sm btn-ghost"
>
{if @issue.state == "open", do: "Close", else: "Reopen"}
</button>
<button type="submit" class="btn btn-sm btn-primary">Comment</button>
</div>
</.form>
</section>
</div>
</Layouts.app>
"""
end
defp state_class("open"), do: "badge-success"
defp state_class("closed"), do: "badge-neutral"
defp state_class(_), do: ""
defp author_name(%{email: e}), do: e |> String.split("@") |> hd()
defp author_name(_), do: "anonymous"
defp append_with_blank("", new), do: new
defp append_with_blank(existing, new), do: existing <> "\n\n" <> new
defp format_dt(dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M")
end