You've already forked tribes-plugin-aether
forked from tribes/tribes-plugin-template
refactor: extract reusable chat panel component
Move Aether chat panel behavior into a LiveComponent so other plugins can embed it without an iframe while ChatLive continues to own standalone routing.
This commit is contained in:
@@ -4,89 +4,39 @@ defmodule AetherWeb.ChatLive do
|
||||
on_mount({Tribes.Plugin.LiveUserAuth, :live_user_optional})
|
||||
|
||||
alias Aether.Chat
|
||||
alias Aether.Chat.{Channel, Message}
|
||||
alias AetherWeb.ChatPanelComponent
|
||||
alias Tribes.Plugin.Layouts
|
||||
|
||||
@component_id "aether-chat-panel-component"
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
%{mode: mode, slug: slug, backend: backend} = channel_request(session)
|
||||
request = channel_request(session)
|
||||
|
||||
case Chat.ensure_channel(%{slug: slug, title: channel_title(slug), backend: backend}) do
|
||||
{:ok, %Channel{} = channel} ->
|
||||
messages = load_messages(channel)
|
||||
message_ids = messages |> Enum.map(& &1.id) |> MapSet.new()
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:page_title, channel.title)
|
||||
|> assign(:channel, channel)
|
||||
|> assign(:embedded?, mode == :embedded)
|
||||
|> assign(:backend, channel.backend)
|
||||
|> assign(:message_ids, message_ids)
|
||||
|> assign(:message_count, MapSet.size(message_ids))
|
||||
|> stream_configure(:messages, dom_id: &("chat-message-" <> &1.id))
|
||||
|> stream(:messages, messages)
|
||||
|
||||
if connected?(socket) do
|
||||
_ = Chat.subscribe_channel(channel)
|
||||
end
|
||||
|
||||
{:ok, socket}
|
||||
|
||||
{:error, reason} ->
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Chat")
|
||||
|> assign(:channel, nil)
|
||||
|> assign(:embedded?, mode == :embedded)
|
||||
|> assign(:backend, backend)
|
||||
|> assign(:message_ids, MapSet.new())
|
||||
|> assign(:message_count, 0)
|
||||
|> assign(:chat_error, inspect(reason))
|
||||
|> stream_configure(:messages, dom_id: &("chat-message-" <> &1.id))
|
||||
|> stream(:messages, [])}
|
||||
end
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, channel_title(request.slug))
|
||||
|> assign(:chat_request, request)
|
||||
|> assign(:component_id, @component_id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("send_message", %{"message" => %{"body" => body}}, socket) do
|
||||
body = String.trim(body)
|
||||
|
||||
cond do
|
||||
socket.assigns.channel == nil ->
|
||||
{:noreply, put_flash(socket, :error, "Chat channel is unavailable")}
|
||||
|
||||
socket.assigns.current_user == nil ->
|
||||
{:noreply, put_flash(socket, :error, "Sign in to chat")}
|
||||
|
||||
body == "" ->
|
||||
{:noreply, put_flash(socket, :error, "Message cannot be empty")}
|
||||
|
||||
true ->
|
||||
post_message(socket, body)
|
||||
end
|
||||
def handle_info({:aether_chat, :message, message}, socket) do
|
||||
send_update(ChatPanelComponent, id: @component_id, incoming_message: message)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:aether_chat, :message, %Message{} = message}, socket) do
|
||||
if socket.assigns.channel && message.channel_id == socket.assigns.channel.id do
|
||||
{:noreply, put_message(socket, message)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(%{embedded?: true} = assigns) do
|
||||
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">
|
||||
<.chat_panel
|
||||
channel={@channel}
|
||||
<.live_component
|
||||
module={ChatPanelComponent}
|
||||
id={@component_id}
|
||||
slug={@chat_request.slug}
|
||||
backend={@chat_request.backend}
|
||||
embedded?={true}
|
||||
current_user={@current_user}
|
||||
embedded?={@embedded?}
|
||||
backend={@backend}
|
||||
message_count={@message_count}
|
||||
streams={@streams}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
@@ -103,169 +53,28 @@ defmodule AetherWeb.ChatLive do
|
||||
Aether chat
|
||||
</p>
|
||||
<h1 class="mt-1 text-3xl font-semibold tracking-tight text-base-content">
|
||||
{if @channel, do: @channel.title, else: "Chat"}
|
||||
{channel_title(@chat_request.slug)}
|
||||
</h1>
|
||||
<p class="mt-2 max-w-2xl text-sm leading-6 text-base-content/70">
|
||||
Public synced group chat. Marmot-backed channels will come later without changing the chat surface.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-full border border-base-300 px-4 py-2 text-sm font-medium text-base-content/70">
|
||||
{@message_count} messages
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<.chat_panel
|
||||
channel={@channel}
|
||||
<.live_component
|
||||
module={ChatPanelComponent}
|
||||
id={@component_id}
|
||||
slug={@chat_request.slug}
|
||||
backend={@chat_request.backend}
|
||||
embedded?={false}
|
||||
current_user={@current_user}
|
||||
embedded?={@embedded?}
|
||||
backend={@backend}
|
||||
message_count={@message_count}
|
||||
streams={@streams}
|
||||
/>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
defp chat_panel(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id="aether-chat-panel"
|
||||
class={[
|
||||
"flex min-h-0 flex-1 flex-col overflow-hidden border border-base-300 bg-base-100 shadow-sm",
|
||||
@embedded? && "h-full rounded-2xl",
|
||||
!@embedded? && "rounded-3xl"
|
||||
]}
|
||||
>
|
||||
<div
|
||||
:if={@embedded?}
|
||||
id="chat-embed-header"
|
||||
class="flex items-center justify-between gap-3 border-b border-base-300 px-4 py-3"
|
||||
>
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-base-content/50">Chat</p>
|
||||
<h2 class="text-base font-semibold text-base-content">{if @channel, do: @channel.title, else: "Chat"}</h2>
|
||||
</div>
|
||||
<span class="text-xs text-base-content/60">{@message_count}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={@backend == :marmot}
|
||||
id="chat-marmot-notice"
|
||||
class="border-b border-warning/30 bg-warning/10 px-4 py-3 text-sm text-warning-content"
|
||||
>
|
||||
Marmot backend scaffold is enabled for this channel. Browser transport, storage, and signing are not active yet.
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="chat-messages"
|
||||
phx-hook="AetherChatScroll"
|
||||
phx-update="stream"
|
||||
class={[
|
||||
"flex-1 space-y-3 overflow-y-auto p-4",
|
||||
!@embedded? && "min-h-[24rem] sm:p-6",
|
||||
@embedded? && "min-h-0"
|
||||
]}
|
||||
>
|
||||
<div
|
||||
id="chat-empty"
|
||||
class="hidden only:block rounded-box border border-dashed border-base-300 p-8 text-center text-sm text-base-content/60"
|
||||
>
|
||||
No messages yet. Start the conversation.
|
||||
</div>
|
||||
|
||||
<article :for={{id, message} <- @streams.messages} id={id} class="aether-chat-message flex gap-3">
|
||||
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
|
||||
{author_initial(message)}
|
||||
</div>
|
||||
<div class="max-w-[min(42rem,85%)] rounded-2xl bg-base-200 px-4 py-3 shadow-sm">
|
||||
<div class="mb-1 flex items-center gap-2 text-xs text-base-content/60">
|
||||
<span class="font-semibold text-base-content/75">{author_label(message)}</span>
|
||||
<span
|
||||
phx-hook="DateTime"
|
||||
id={"chat-time-#{message.id}"}
|
||||
phx-update="ignore"
|
||||
data-timestamp={message_timestamp(message)}
|
||||
data-relative="true"
|
||||
data-format="LLL"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<p class="whitespace-pre-wrap break-words text-sm leading-6 text-base-content">{message.body}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<%= cond do %>
|
||||
<% @backend == :marmot -> %>
|
||||
<div id="chat-marmot-disabled" class="border-t border-base-300 bg-base-100 p-5 text-center text-sm text-base-content/70">
|
||||
Marmot chat is not enabled in the web UI yet.
|
||||
</div>
|
||||
<% @current_user -> %>
|
||||
<form id="chat-message-form" phx-submit="send_message" class="border-t border-base-300 bg-base-100 p-3 sm:p-4">
|
||||
<div class="flex items-end gap-2 rounded-2xl border border-base-300 bg-base-200/60 p-2">
|
||||
<label for="chat-message-body" class="sr-only">Message</label>
|
||||
<textarea
|
||||
id="chat-message-body"
|
||||
name="message[body]"
|
||||
rows="2"
|
||||
class="textarea textarea-ghost min-h-12 flex-1 resize-none bg-transparent focus:outline-none"
|
||||
placeholder={"Message #{if @channel, do: @channel.title, else: "chat"}"}
|
||||
></textarea>
|
||||
<button id="chat-send-button" type="submit" class="btn btn-primary rounded-full">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<% true -> %>
|
||||
<div id="chat-signed-out" class="border-t border-base-300 bg-base-100 p-5 text-center text-sm text-base-content/70">
|
||||
Sign in to join the chat.
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp post_message(socket, body) do
|
||||
user = socket.assigns.current_user
|
||||
|
||||
attrs = %{
|
||||
body: body,
|
||||
author_id: user_attr(user, :id),
|
||||
author_pubkey: user_attr(user, :pubkey_hex) || user_attr(user, :pubkey),
|
||||
metadata: %{"author_name" => user_attr(user, :username)}
|
||||
}
|
||||
|
||||
case Chat.post_message(socket.assigns.channel, attrs) do
|
||||
{:ok, message} ->
|
||||
{:noreply, put_message(socket, message)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to send message: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
defp put_message(socket, %Message{} = message) do
|
||||
if MapSet.member?(socket.assigns.message_ids, message.id) do
|
||||
socket
|
||||
else
|
||||
message_ids = MapSet.put(socket.assigns.message_ids, message.id)
|
||||
|
||||
socket
|
||||
|> assign(:message_ids, message_ids)
|
||||
|> assign(:message_count, MapSet.size(message_ids))
|
||||
|> stream_insert(:messages, message, at: -1)
|
||||
end
|
||||
end
|
||||
|
||||
defp load_messages(%Channel{} = channel) do
|
||||
case Chat.list_messages(channel) do
|
||||
{:ok, messages} -> messages
|
||||
{:error, _reason} -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp channel_request(%{"plugin_path" => path}) when is_binary(path) do
|
||||
case String.split(path, "/", trim: true) do
|
||||
["aether", "chat", "embed", slug | _rest] ->
|
||||
@@ -304,32 +113,4 @@ defmodule AetherWeb.ChatLive do
|
||||
|> String.split(" ", trim: true)
|
||||
|> Enum.map_join(" ", &String.capitalize/1)
|
||||
end
|
||||
|
||||
defp user_attr(nil, _key), do: nil
|
||||
defp user_attr(user, key), do: Map.get(user, key)
|
||||
|
||||
defp author_label(%Message{metadata: %{"author_name" => name}})
|
||||
when is_binary(name) and name != "" do
|
||||
name
|
||||
end
|
||||
|
||||
defp author_label(%Message{author_pubkey: pubkey}) when is_binary(pubkey) and pubkey != "" do
|
||||
String.slice(pubkey, 0, 8) <> "…"
|
||||
end
|
||||
|
||||
defp author_label(_message), do: "Unknown"
|
||||
|
||||
defp author_initial(message) do
|
||||
message
|
||||
|> author_label()
|
||||
|> String.first()
|
||||
|> Kernel.||("?")
|
||||
|> String.upcase()
|
||||
end
|
||||
|
||||
defp message_timestamp(%Message{inserted_at: %DateTime{} = inserted_at}) do
|
||||
DateTime.to_unix(inserted_at, :millisecond)
|
||||
end
|
||||
|
||||
defp message_timestamp(_message), do: DateTime.utc_now() |> DateTime.to_unix(:millisecond)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
defmodule AetherWeb.ChatPanelComponent do
|
||||
@moduledoc """
|
||||
Reusable chat panel component for standalone and embedded Aether chat surfaces.
|
||||
"""
|
||||
|
||||
use Phoenix.LiveComponent
|
||||
|
||||
alias Aether.Chat
|
||||
alias Aether.Chat.{Channel, Message}
|
||||
|
||||
@impl true
|
||||
def update(%{incoming_message: %Message{} = message}, socket) do
|
||||
{:ok, put_message(socket, message)}
|
||||
end
|
||||
|
||||
def update(assigns, socket) do
|
||||
slug = Map.fetch!(assigns, :slug)
|
||||
backend = Map.get(assigns, :backend, :public_sync)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_new(:channel, fn -> nil end)
|
||||
|> assign_new(:messages, fn -> [] end)
|
||||
|> assign_new(:message_ids, fn -> MapSet.new() end)
|
||||
|> assign_new(:message_count, fn -> 0 end)
|
||||
|> assign_new(:subscribed_channel_id, fn -> nil end)
|
||||
|
||||
socket =
|
||||
if reload_channel?(socket, slug, backend) do
|
||||
load_channel(socket, slug, backend)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
{:ok, maybe_subscribe(socket)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("send_message", %{"message" => %{"body" => body}}, socket) do
|
||||
body = String.trim(body)
|
||||
|
||||
cond do
|
||||
socket.assigns.channel == nil ->
|
||||
{:noreply, put_flash(socket, :error, "Chat channel is unavailable")}
|
||||
|
||||
socket.assigns.current_user == nil ->
|
||||
{:noreply, put_flash(socket, :error, "Sign in to chat")}
|
||||
|
||||
body == "" ->
|
||||
{:noreply, put_flash(socket, :error, "Message cannot be empty")}
|
||||
|
||||
true ->
|
||||
post_message(socket, body)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id="aether-chat-panel"
|
||||
class={[
|
||||
"flex min-h-0 flex-1 flex-col overflow-hidden border border-base-300 bg-base-100 shadow-sm",
|
||||
@embedded? && "h-full rounded-2xl",
|
||||
!@embedded? && "rounded-3xl"
|
||||
]}
|
||||
>
|
||||
<div
|
||||
:if={@embedded?}
|
||||
id="chat-embed-header"
|
||||
class="flex items-center justify-between gap-3 border-b border-base-300 px-4 py-3"
|
||||
>
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-base-content/50">Chat</p>
|
||||
<h2 class="text-base font-semibold text-base-content">{if @channel, do: @channel.title, else: "Chat"}</h2>
|
||||
</div>
|
||||
<span class="text-xs text-base-content/60">{@message_count}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={@backend == :marmot}
|
||||
id="chat-marmot-notice"
|
||||
class="border-b border-warning/30 bg-warning/10 px-4 py-3 text-sm text-warning-content"
|
||||
>
|
||||
Marmot backend scaffold is enabled for this channel. Browser transport, storage, and signing are not active yet.
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="chat-messages"
|
||||
phx-hook="AetherChatScroll"
|
||||
class={[
|
||||
"flex-1 space-y-3 overflow-y-auto p-4",
|
||||
!@embedded? && "min-h-[24rem] sm:p-6",
|
||||
@embedded? && "min-h-0"
|
||||
]}
|
||||
>
|
||||
<div
|
||||
:if={@messages == []}
|
||||
id="chat-empty"
|
||||
class="rounded-box border border-dashed border-base-300 p-8 text-center text-sm text-base-content/60"
|
||||
>
|
||||
No messages yet. Start the conversation.
|
||||
</div>
|
||||
|
||||
<article :for={message <- @messages} id={"chat-message-#{message.id}"} class="aether-chat-message flex gap-3">
|
||||
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
|
||||
{author_initial(message)}
|
||||
</div>
|
||||
<div class="max-w-[min(42rem,85%)] rounded-2xl bg-base-200 px-4 py-3 shadow-sm">
|
||||
<div class="mb-1 flex items-center gap-2 text-xs text-base-content/60">
|
||||
<span class="font-semibold text-base-content/75">{author_label(message)}</span>
|
||||
<span
|
||||
phx-hook="DateTime"
|
||||
id={"chat-time-#{message.id}"}
|
||||
phx-update="ignore"
|
||||
data-timestamp={message_timestamp(message)}
|
||||
data-relative="true"
|
||||
data-format="LLL"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<p class="whitespace-pre-wrap break-words text-sm leading-6 text-base-content">{message.body}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<%= cond do %>
|
||||
<% @backend == :marmot -> %>
|
||||
<div id="chat-marmot-disabled" class="border-t border-base-300 bg-base-100 p-5 text-center text-sm text-base-content/70">
|
||||
Marmot chat is not enabled in the web UI yet.
|
||||
</div>
|
||||
<% @current_user -> %>
|
||||
<form id="chat-message-form" phx-submit="send_message" phx-target={@myself} class="border-t border-base-300 bg-base-100 p-3 sm:p-4">
|
||||
<div class="flex items-end gap-2 rounded-2xl border border-base-300 bg-base-200/60 p-2">
|
||||
<label for="chat-message-body" class="sr-only">Message</label>
|
||||
<textarea
|
||||
id="chat-message-body"
|
||||
name="message[body]"
|
||||
rows="2"
|
||||
class="textarea textarea-ghost min-h-12 flex-1 resize-none bg-transparent focus:outline-none"
|
||||
placeholder={"Message #{if @channel, do: @channel.title, else: "chat"}"}
|
||||
></textarea>
|
||||
<button id="chat-send-button" type="submit" class="btn btn-primary rounded-full">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<% true -> %>
|
||||
<div id="chat-signed-out" class="border-t border-base-300 bg-base-100 p-5 text-center text-sm text-base-content/70">
|
||||
Sign in to join the chat.
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp reload_channel?(socket, slug, backend) do
|
||||
channel = socket.assigns.channel
|
||||
channel == nil || channel.slug != slug || channel.backend != backend
|
||||
end
|
||||
|
||||
defp load_channel(socket, slug, backend) do
|
||||
case Chat.ensure_channel(%{slug: slug, title: channel_title(slug), backend: backend}) do
|
||||
{:ok, %Channel{} = channel} ->
|
||||
messages = load_messages(channel)
|
||||
message_ids = messages |> Enum.map(& &1.id) |> MapSet.new()
|
||||
|
||||
socket
|
||||
|> assign(:channel, channel)
|
||||
|> assign(:backend, channel.backend)
|
||||
|> assign(:messages, messages)
|
||||
|> assign(:message_ids, message_ids)
|
||||
|> assign(:message_count, MapSet.size(message_ids))
|
||||
|
||||
{:error, reason} ->
|
||||
socket
|
||||
|> assign(:channel, nil)
|
||||
|> assign(:backend, backend)
|
||||
|> assign(:messages, [])
|
||||
|> assign(:message_ids, MapSet.new())
|
||||
|> assign(:message_count, 0)
|
||||
|> assign(:chat_error, inspect(reason))
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_subscribe(
|
||||
%{assigns: %{channel: %Channel{id: id}, subscribed_channel_id: id}} = socket
|
||||
) do
|
||||
socket
|
||||
end
|
||||
|
||||
defp maybe_subscribe(%{assigns: %{channel: %Channel{} = channel}} = socket) do
|
||||
if connected?(socket) do
|
||||
_ = Chat.subscribe_channel(channel)
|
||||
assign(socket, :subscribed_channel_id, channel.id)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_subscribe(socket), do: socket
|
||||
|
||||
defp post_message(socket, body) do
|
||||
user = socket.assigns.current_user
|
||||
|
||||
attrs = %{
|
||||
body: body,
|
||||
author_id: user_attr(user, :id),
|
||||
author_pubkey: user_attr(user, :pubkey_hex) || user_attr(user, :pubkey),
|
||||
metadata: %{"author_name" => user_attr(user, :username)}
|
||||
}
|
||||
|
||||
case Chat.post_message(socket.assigns.channel, attrs) do
|
||||
{:ok, message} ->
|
||||
{:noreply, put_message(socket, message)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to send message: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
defp put_message(socket, %Message{} = message) do
|
||||
if socket.assigns.channel && message.channel_id == socket.assigns.channel.id do
|
||||
append_message(socket, message)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp append_message(socket, %Message{} = message) do
|
||||
if MapSet.member?(socket.assigns.message_ids, message.id) do
|
||||
socket
|
||||
else
|
||||
message_ids = MapSet.put(socket.assigns.message_ids, message.id)
|
||||
|
||||
socket
|
||||
|> assign(:messages, socket.assigns.messages ++ [message])
|
||||
|> assign(:message_ids, message_ids)
|
||||
|> assign(:message_count, MapSet.size(message_ids))
|
||||
end
|
||||
end
|
||||
|
||||
defp load_messages(%Channel{} = channel) do
|
||||
case Chat.list_messages(channel) do
|
||||
{:ok, messages} -> messages
|
||||
{:error, _reason} -> []
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
defp user_attr(nil, _key), do: nil
|
||||
defp user_attr(user, key), do: Map.get(user, key)
|
||||
|
||||
defp author_label(%Message{metadata: %{"author_name" => name}})
|
||||
when is_binary(name) and name != "" do
|
||||
name
|
||||
end
|
||||
|
||||
defp author_label(%Message{author_pubkey: pubkey}) when is_binary(pubkey) and pubkey != "" do
|
||||
String.slice(pubkey, 0, 8) <> "…"
|
||||
end
|
||||
|
||||
defp author_label(_message), do: "Unknown"
|
||||
|
||||
defp author_initial(message) do
|
||||
message
|
||||
|> author_label()
|
||||
|> String.first()
|
||||
|> Kernel.||("?")
|
||||
|> String.upcase()
|
||||
end
|
||||
|
||||
defp message_timestamp(%Message{inserted_at: %DateTime{} = inserted_at}) do
|
||||
DateTime.to_unix(inserted_at, :millisecond)
|
||||
end
|
||||
|
||||
defp message_timestamp(_message), do: DateTime.utc_now() |> DateTime.to_unix(:millisecond)
|
||||
end
|
||||
Reference in New Issue
Block a user