From 38ff2fa2b20891bc9c2669e1cc2c71541b80d802 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Mon, 25 May 2026 05:44:06 +0200 Subject: [PATCH] feat: embed optional stream chat Declare chat@1 as an optional enhancement and show an Aether chat iframe beside the Sender player when a chat provider is installed. --- lib/sender/chat_integration.ex | 36 +++++++++++++++++++++++++++++ lib/sender_web/live/home_live.ex | 39 +++++++++++++++++++++++++------- manifest.json | 2 +- test/sender/home_page_test.exs | 32 ++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 lib/sender/chat_integration.ex diff --git a/lib/sender/chat_integration.ex b/lib/sender/chat_integration.ex new file mode 100644 index 0000000..727142b --- /dev/null +++ b/lib/sender/chat_integration.ex @@ -0,0 +1,36 @@ +defmodule Sender.ChatIntegration do + @moduledoc false + + @chat_capability "chat@1" + @aether_embed_prefix "/aether/chat/embed/" + + def embed_path(stream_id \\ "default") when is_binary(stream_id) do + if chat_available?() do + {:ok, @aether_embed_prefix <> channel_slug(stream_id)} + else + :unavailable + end + end + + def chat_available? do + Code.ensure_loaded?(Tribes.PluginRegistry) && + Tribes.PluginRegistry.provides?(@chat_capability) + rescue + _error -> false + end + + defp channel_slug(stream_id) do + "sender-stream-" <> slugify(stream_id) + end + + defp slugify(value) do + value + |> String.downcase() + |> String.replace(~r/[^a-z0-9]+/u, "-") + |> String.trim("-") + |> case do + "" -> "default" + slug -> slug + end + end +end diff --git a/lib/sender_web/live/home_live.ex b/lib/sender_web/live/home_live.ex index be7a223..751d089 100644 --- a/lib/sender_web/live/home_live.ex +++ b/lib/sender_web/live/home_live.ex @@ -14,10 +14,17 @@ defmodule SenderWeb.HomeLive do on_mount({Tribes.Plugin.LiveUserAuth, :live_user_optional}) + alias Sender.ChatIntegration alias Tribes.Plugin.Layouts def mount(_params, _session, socket) do - {:ok, assign(socket, :page_title, "Streaming")} + chat_embed_path = + case ChatIntegration.embed_path() do + {:ok, path} -> path + :unavailable -> nil + end + + {:ok, socket |> assign(:page_title, "Streaming") |> assign(:chat_embed_path, chat_embed_path)} end def render(assigns) do @@ -34,13 +41,29 @@ defmodule SenderWeb.HomeLive do

-
-

No live stream selected.

-
+
+
+

No live stream selected.

+
+ +
+ +
+
""" diff --git a/manifest.json b/manifest.json index e7b1cbc..957dbaf 100644 --- a/manifest.json +++ b/manifest.json @@ -7,7 +7,7 @@ "otp_app": "sender", "provides": ["sender@1"], "requires": ["ui@1"], - "enhances_with": [], + "enhances_with": ["chat@1"], "assets": { "global_js": ["sender.js"], "global_css": ["sender.css"] diff --git a/test/sender/home_page_test.exs b/test/sender/home_page_test.exs index ed67367..45447b2 100644 --- a/test/sender/home_page_test.exs +++ b/test/sender/home_page_test.exs @@ -9,9 +9,41 @@ defmodule Sender.HomePageTest do assert has_element?(view, "#plugin-nav-streaming", "Streaming") end + test "embeds stream chat when a chat provider is installed", %{conn: conn} do + register_chat_provider!() + + {:ok, view, _html} = live(conn, "/sender") + + assert has_element?(view, "#sender-stream-chat") + + assert has_element?( + view, + ~s|#sender-chat-frame[src="/aether/chat/embed/sender-stream-default"]| + ) + end + test "dispatches top-level subpaths to the plugin page", %{conn: conn} do {:ok, _view, html} = live(conn, "/sender/example") assert html =~ "Streaming" end + + defp register_chat_provider! do + name = "sender_chat_test" + _ = Tribes.PluginRegistry.unregister_plugin(name) + + :ok = + Tribes.PluginRegistry.register_plugin( + name, + %{ + name: name, + version: "0.1.0", + provides: ["chat@1"], + provider_priority: -100 + }, + File.cwd!() + ) + + on_exit(fn -> _ = Tribes.PluginRegistry.unregister_plugin(name) end) + end end