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