Files
self 80101b7e78 fix: tolerate Parrhesia EOSE hints
Handle the NIP-67 EOSE tuple shape emitted by newer Parrhesia while keeping compatibility with the previous message format.
2026-06-08 21:53:24 +02:00

187 lines
6.2 KiB
Elixir

defmodule TribeOne.TribesPlugin.AetherWeb.ChatLive do
use Phoenix.LiveView
on_mount({Tribes.Plugin.LiveUserAuth, :live_user_optional})
alias TribeOne.TribesPlugin.Aether.Chat
alias TribeOne.TribesPlugin.AetherWeb.{ChatPanelComponent, ChatRecipientPickerComponent}
alias Tribes.Plugin.Layouts
@component_id "aether-chat-panel-component"
@impl true
def mount(_params, session, socket) do
request = channel_request(session)
{:ok,
socket
|> assign(:page_title, channel_title(request.slug))
|> assign(:chat_request, request)
|> assign(:component_id, @component_id)}
end
@impl true
def handle_info({:aether_chat, :message, message}, socket) do
send_update(ChatPanelComponent, id: @component_id, incoming_message: message)
{:noreply, socket}
end
def handle_info({:aether_chat, :recipient_selected, recipient}, socket) do
current_user = socket.assigns.current_user
if current_user && is_binary(recipient.pubkey_hex) do
attrs = %{
title: recipient_label(recipient),
metadata: %{"recipient_username" => recipient.username}
}
case Chat.ensure_direct_conversation(current_user.pubkey_hex, recipient.pubkey_hex, attrs,
session_privkey: socket.assigns[:session_privkey]
) do
{:ok, channel} ->
{:noreply, push_navigate(socket, to: Chat.standalone_path(channel))}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Could not open conversation: #{inspect(reason)}")}
end
else
{:noreply, put_flash(socket, :error, "Sign in to start a direct message")}
end
end
def handle_info({:parrhesia, :events, _ref, _subscription_id, events}, socket)
when is_list(events) do
Enum.each(events, &send_unwrapped_message(socket, &1))
{:noreply, socket}
end
def handle_info({:parrhesia, :event, _ref, _subscription_id, event}, socket) do
send_unwrapped_message(socket, event)
{:noreply, socket}
end
def handle_info({:parrhesia, :eose, _ref, _subscription_id, _hints}, socket),
do: {:noreply, socket}
def handle_info({:parrhesia, :eose, _ref, _subscription_id}, socket), do: {:noreply, socket}
@impl true
def render(%{chat_request: %{mode: :embedded}} = assigns) do
~H"""
<div class="aether aether-chat h-full min-h-[28rem] p-2" id="aether-chat-embed">
<.live_component
module={ChatPanelComponent}
id={@component_id}
slug={@chat_request.slug}
backend={@chat_request.backend}
embedded?={true}
current_user={@current_user}
session_privkey={@session_privkey}
/>
</div>
"""
end
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="aether aether-chat flex min-h-[calc(100vh-8rem)] flex-col p-4 sm:p-6" id="aether-chat-page">
<section class="mb-4 rounded-3xl border border-base-300 bg-base-100/95 p-5 shadow-sm">
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.3em] text-base-content/50">
Aether chat
</p>
<h1 class="mt-1 text-3xl font-semibold tracking-tight text-base-content">
{channel_title(@chat_request.slug)}
</h1>
<p class="mt-2 max-w-2xl text-sm leading-6 text-base-content/70">
Public synced rooms and NIP-17 direct messages share this chat surface.
</p>
</div>
<.live_component
:if={@current_user}
module={ChatRecipientPickerComponent}
id="aether-chat-recipient-picker"
current_user={@current_user}
/>
</div>
</section>
<.live_component
module={ChatPanelComponent}
id={@component_id}
slug={@chat_request.slug}
backend={@chat_request.backend}
embedded?={false}
current_user={@current_user}
session_privkey={@session_privkey}
/>
</div>
</Layouts.app>
"""
end
defp channel_request(%{"plugin_path" => path}) when is_binary(path) do
case String.split(path, "/", trim: true) do
["aether", "chat", "embed", slug | _rest] ->
request(:embedded, slug, :public_sync)
["plugins", "aether", "chat", "embed", slug | _rest] ->
request(:embedded, slug, :public_sync)
["aether", "chat", "marmot", slug | _rest] ->
request(:standalone, slug, :marmot)
["plugins", "aether", "chat", "marmot", slug | _rest] ->
request(:standalone, slug, :marmot)
["aether", "chat", slug | _rest] ->
request(:standalone, slug, :public_sync)
["plugins", "aether", "chat", slug | _rest] ->
request(:standalone, slug, :public_sync)
_other ->
request(:standalone, Chat.default_channel_slug(), :public_sync)
end
end
defp channel_request(_session),
do: request(:standalone, Chat.default_channel_slug(), :public_sync)
defp request(mode, slug, backend), do: %{mode: mode, slug: slug, backend: backend}
defp send_unwrapped_message(socket, event) do
request = socket.assigns.chat_request
with {:ok, %TribeOne.TribesPlugin.Aether.Chat.Channel{} = channel} <-
Chat.get_channel_by_slug(request.slug, Chat.ash_opts()),
backend <- Chat.backend_module(channel.backend),
true <- function_exported?(backend, :unwrap_event, 3),
{:ok, message} <-
backend.unwrap_event(channel, event, session_privkey: socket.assigns[:session_privkey]) do
send_update(ChatPanelComponent, id: @component_id, incoming_message: message)
end
end
defp recipient_label(%{display_name: display_name})
when is_binary(display_name) and display_name != "" do
display_name
end
defp recipient_label(%{username: username}) when is_binary(username) and username != "",
do: username
defp recipient_label(%{pubkey_hex: pubkey}), do: String.slice(pubkey, 0, 8) <> ""
defp channel_title("general"), do: "General Chat"
defp channel_title(slug) do
slug
|> String.replace(["-", "_"], " ")
|> String.split(" ", trim: true)
|> Enum.map_join(" ", &String.capitalize/1)
end
end