Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/claper/presentations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,31 @@ defmodule Claper.Presentations do
|> broadcast(:state_updated)
end

@doc """
Returns true when the event only accepts messages from authenticated users.

Always reads the current state from the database, so a caller holding a
presentation state loaded earlier cannot accept a message with a stale value
after the moderator turned the setting on.

## Examples

iex> authenticated_chat_only?(event_id)
false

"""
def authenticated_chat_only?(event_id) do
query =
from(ps in PresentationState,
join: pf in PresentationFile,
on: ps.presentation_file_id == pf.id,
where: pf.event_id == ^event_id,
select: ps.authenticated_chat_only
)

Repo.one(query) == true
end

@interaction_schemas [
Claper.Polls.Poll,
Claper.Forms.Form,
Expand Down
3 changes: 3 additions & 0 deletions lib/claper/presentations/presentation_state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Claper.Presentations.PresentationState do
join_screen_visible: boolean() | nil,
chat_enabled: boolean() | nil,
anonymous_chat_enabled: boolean() | nil,
authenticated_chat_only: boolean() | nil,
message_reaction_enabled: boolean() | nil,
banned: [String.t()] | nil,
show_only_pinned: boolean() | nil,
Expand All @@ -26,6 +27,7 @@ defmodule Claper.Presentations.PresentationState do
field :join_screen_visible, :boolean
field :chat_enabled, :boolean
field :anonymous_chat_enabled, :boolean
field :authenticated_chat_only, :boolean, default: false
field :message_reaction_enabled, :boolean, default: true
field :banned, {:array, :string}, default: []
field :show_only_pinned, :boolean, default: false
Expand All @@ -48,6 +50,7 @@ defmodule Claper.Presentations.PresentationState do
:presentation_file_id,
:chat_enabled,
:anonymous_chat_enabled,
:authenticated_chat_only,
:show_only_pinned,
:show_attendee_count,
:message_reaction_enabled
Expand Down
13 changes: 11 additions & 2 deletions lib/claper_web/controllers/post_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,17 @@ defmodule ClaperWeb.PostController do
def create(conn, %{"event_id" => event_id, "body" => body}) do
try do
with event <- Claper.Events.get_event!(event_id) do
case Claper.Posts.create_post(event, %{body: body}) do
{:ok, post} -> render(conn, "post.json", post: post)
# Posts created here carry no user, so an event that only accepts
# messages from authenticated attendees cannot accept them.
if Claper.Presentations.authenticated_chat_only?(event.id) do
conn
|> put_status(:forbidden)
|> put_view(ClaperWeb.ErrorView)
|> render(:"403")
else
case Claper.Posts.create_post(event, %{body: body}) do
{:ok, post} -> render(conn, "post.json", post: post)
end
end
end
rescue
Expand Down
17 changes: 17 additions & 0 deletions lib/claper_web/live/event_live/manage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,23 @@ defmodule ClaperWeb.EventLive.Manage do
{:noreply, socket |> assign(:state, new_state)}
end

@impl true
def handle_event(
"checked",
%{"key" => "authenticated_chat_only", "value" => value},
%{assigns: %{state: state}} = socket
) do
{:ok, new_state} =
Claper.Presentations.update_presentation_state(
state,
%{
:authenticated_chat_only => value
}
)

{:noreply, socket |> assign(:state, new_state)}
end

@impl true
def handle_event(
"checked",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,33 @@ defmodule ClaperWeb.EventLive.ManageAttendeesOptionsComponent do
</svg>
</:icon>
</.toggle_row>
<.toggle_row
label={
if @state.authenticated_chat_only,
do: gettext("Allow messages without login"),
else: gettext("Require login to post")
}
checked={@state.authenticated_chat_only}
key={:authenticated_chat_only}
shortcut={if @create == nil, do: "F", else: nil}
disabled={!@state.chat_enabled}
show_shortcut={@show_shortcut}
>
<:icon>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z"
clip-rule="evenodd"
/>
</svg>
</:icon>
</.toggle_row>
<.toggle_row
label={
if @state.message_reaction_enabled,
Expand Down
11 changes: 11 additions & 0 deletions lib/claper_web/live/event_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,17 @@ defmodule ClaperWeb.EventLive.Show do
{:noreply, socket}
end

@impl true
def handle_event(
"save",
_params,
%{assigns: %{state: %{authenticated_chat_only: true}, current_user: current_user}} =
socket
)
when not is_map(current_user) do
{:noreply, socket |> put_flash(:error, gettext("You must be logged in to post a message"))}
end

@impl true
def handle_event(
"save",
Expand Down
106 changes: 60 additions & 46 deletions lib/claper_web/live/event_live/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -484,55 +484,69 @@
id="room-composer"
class="absolute inset-x-0 bottom-0 z-30 bg-transparent px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-2"
>
<%= if @state.chat_enabled do %>
<.form
:let={f}
for={@post_changeset}
id="post-form"
class="attendee-composer flex items-center gap-1.5 rounded-2xl border border-white/20 px-1.5 py-1 shadow-2xl"
phx-hook="PostForm"
data-nickname={@nickname}
data-storage-key={"post-draft:#{@event.uuid}"}
phx-submit="save"
>
<button
id="composer-identity-button"
type="button"
aria-label={gettext("Choose identity")}
phx-click={JS.toggle(to: "#identity-menu")}
class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-black/60 text-xs font-bold text-white"
<%= cond do %>
<% !@state.chat_enabled -> %>
<div class="attendee-composer flex min-h-10 items-center rounded-2xl border border-white/20 px-3 text-sm text-white/60 shadow-2xl">
{gettext("Messages deactivated")}
</div>
<% @state.authenticated_chat_only && is_nil(@current_user) -> %>
<div
id="login-required-composer"
class="attendee-composer flex min-h-10 items-center justify-between gap-3 rounded-2xl border border-white/20 px-3 py-1.5 text-sm text-white/60 shadow-2xl"
>
{if @nickname in [nil, ""], do: "A", else: String.first(@nickname)}
</button>
<span>{gettext("Log in to post a message")}</span>
<a
href={~p"/users/log_in"}
class="shrink-0 rounded-full bg-primary-500 px-3 py-1.5 text-xs font-bold text-white"
>
{gettext("Log in")}
</a>
</div>
<% true -> %>
<.form
:let={f}
for={@post_changeset}
id="post-form"
class="attendee-composer flex items-center gap-1.5 rounded-2xl border border-white/20 px-1.5 py-1 shadow-2xl"
phx-hook="PostForm"
data-nickname={@nickname}
data-storage-key={"post-draft:#{@event.uuid}"}
phx-submit="save"
>
<button
id="composer-identity-button"
type="button"
aria-label={gettext("Choose identity")}
phx-click={JS.toggle(to: "#identity-menu")}
class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-black/60 text-xs font-bold text-white"
>
{if @nickname in [nil, ""], do: "A", else: String.first(@nickname)}
</button>

{textarea(f, :body,
id: "postFormTA",
disabled: !@state.anonymous_chat_enabled && @nickname in [nil, ""],
rows: 1,
maxlength: 255,
class:
"min-h-9 max-h-16 min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2 text-base leading-5 text-white outline-hidden placeholder:text-white/70 focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
placeholder:
if(!@state.anonymous_chat_enabled && @nickname in [nil, ""],
do: gettext("Set your nickname to participate"),
else: gettext("Ask, comment...")
)
)}
{textarea(f, :body,
id: "postFormTA",
disabled: !@state.anonymous_chat_enabled && @nickname in [nil, ""],
rows: 1,
maxlength: 255,
class:
"min-h-9 max-h-16 min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2 text-base leading-5 text-white outline-hidden placeholder:text-white/70 focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
placeholder:
if(!@state.anonymous_chat_enabled && @nickname in [nil, ""],
do: gettext("Set your nickname to participate"),
else: gettext("Ask, comment...")
)
)}

<button
type="submit"
id="submitBtn"
disabled
aria-label={gettext("Send message")}
class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-black/20 text-white opacity-50 transition disabled:cursor-not-allowed"
>
<img src="/images/icons/send.svg" class="h-5 w-5" />
</button>
</.form>
<% else %>
<div class="attendee-composer flex min-h-10 items-center rounded-2xl border border-white/20 px-3 text-sm text-white/60 shadow-2xl">
{gettext("Messages deactivated")}
</div>
<button
type="submit"
id="submitBtn"
disabled
aria-label={gettext("Send message")}
class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-black/20 text-white opacity-50 transition disabled:cursor-not-allowed"
>
<img src="/images/icons/send.svg" class="h-5 w-5" />
</button>
</.form>
<% end %>
</footer>
</main>
Expand Down
Loading
Loading