From f833791991bf576d225f37d0a499aaba77be904f Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 8 Apr 2026 01:16:15 +0200 Subject: [PATCH] Build Aether against tribes_plugin_api --- assets/package-lock.json | 12 ++ assets/package.json | 8 ++ lib/aether/plugin.ex | 79 +--------- lib/aether_web/live/timeline_live.ex | 199 +++++++++++++------------- mix.exs | 15 +- mix.lock | 13 ++ test/aether/host_integration_test.exs | 38 ++--- test/aether/manifest_test.exs | 2 +- test/contract_test.exs | 2 +- 9 files changed, 164 insertions(+), 204 deletions(-) create mode 100644 assets/package-lock.json create mode 100644 assets/package.json create mode 100644 mix.lock diff --git a/assets/package-lock.json b/assets/package-lock.json new file mode 100644 index 0000000..98452c0 --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "aether-assets", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aether-assets", + "version": "0.1.0" + } + } +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..5478114 --- /dev/null +++ b/assets/package.json @@ -0,0 +1,8 @@ +{ + "name": "aether-assets", + "private": true, + "version": "0.1.0", + "scripts": { + "build": "mkdir -p ../priv/static && cp -r css/. ../priv/static && cp -r js/. ../priv/static" + } +} diff --git a/lib/aether/plugin.ex b/lib/aether/plugin.ex index 0d3ee57..dee32e6 100644 --- a/lib/aether/plugin.ex +++ b/lib/aether/plugin.ex @@ -7,24 +7,12 @@ defmodule Aether.Plugin do during startup. """ - # Once Tribes.Plugin.Base is available, replace the manual implementation - # with: - # - # use Tribes.Plugin.Base, otp_app: :aether - # - # and override only register/1. + use Tribes.Plugin.Base, otp_app: :aether - # @behaviour Tribes.Plugin - - def register(_context) do - manifest = read_manifest() - - %{ - name: manifest["name"], - version: manifest["version"], - provides: manifest["provides"] || [], - requires: manifest["requires"] || [], - enhances_with: manifest["enhances_with"] || [], + @impl true + def register(context) do + super(context) + |> Map.merge(%{ nav_items: [ %{ label: "Aether", @@ -41,61 +29,6 @@ defmodule Aether.Plugin do layout: nil } ], - api_routes: [], - plugs: [], - children: [], - global_js: get_in(manifest, ["assets", "global_js"]) || [], - global_css: get_in(manifest, ["assets", "global_css"]) || [], - migrations_path: migrations_path(manifest), - hooks: %{} - } - end - - defp read_manifest do - manifest_path() - |> File.read!() - |> Jason.decode!() - end - - defp manifest_path do - project_manifest = Path.join(__DIR__, "../../manifest.json") |> Path.expand() - - candidates = - case :code.priv_dir(:aether) do - {:error, :bad_name} -> - [project_manifest] - - priv_dir -> - priv_dir = to_string(priv_dir) - - [ - Path.join(priv_dir, "../manifest.json") |> Path.expand(), - project_manifest - ] - end - - first_existing_path(candidates) || project_manifest - end - - defp migrations_path(manifest) do - if manifest["migrations"] do - candidates = - case :code.priv_dir(:aether) do - {:error, :bad_name} -> - [Path.join(__DIR__, "../../priv/repo/migrations") |> Path.expand()] - - priv_dir -> - [ - Path.join(to_string(priv_dir), "repo/migrations") |> Path.expand(), - Path.join(__DIR__, "../../priv/repo/migrations") |> Path.expand() - ] - end - - first_existing_path(candidates) - end - end - - defp first_existing_path(paths) do - Enum.find(paths, &File.exists?/1) + }) end end diff --git a/lib/aether_web/live/timeline_live.ex b/lib/aether_web/live/timeline_live.ex index 6532023..1b701fb 100644 --- a/lib/aether_web/live/timeline_live.ex +++ b/lib/aether_web/live/timeline_live.ex @@ -1,10 +1,9 @@ defmodule AetherWeb.TimelineLive do - use TribesWeb, :live_view + use Phoenix.LiveView on_mount({TribesWeb.LiveUserAuth, :live_user_optional}) alias Tribes.Communities - alias Tribes.Communities.Tribe alias Tribes.Nostr @impl true @@ -19,8 +18,7 @@ defmodule AetherWeb.TimelineLive do socket = socket - |> assign(:page_title, gettext("Aether")) - |> assign(:current_scope, socket.assigns[:current_scope]) + |> assign(:page_title, "Aether") |> assign(:tribe_id, tribe_id) |> assign(:tribe, scope.tribe) |> assign(:can_view_tribe?, scope.can_view?) @@ -29,8 +27,6 @@ defmodule AetherWeb.TimelineLive do |> assign(:profile_cache, profile_cache) |> assign(:note_ids, note_ids) |> assign(:note_count, MapSet.size(note_ids)) - |> assign(:form, to_form(%{"content" => ""}, as: :note)) - |> assign(:join_form, to_form(%{"invite_code" => ""}, as: :join)) |> assign(:subscription_ref, nil) |> stream_configure(:notes, dom_id: &("note-" <> &1["id"])) |> stream(:notes, notes) @@ -47,10 +43,10 @@ defmodule AetherWeb.TimelineLive do def handle_event("join_tribe", %{"join" => %{"invite_code" => invite_code}}, socket) do cond do is_nil(socket.assigns.current_user) -> - {:noreply, put_flash(socket, :error, gettext("Sign in to join tribes"))} + {:noreply, put_flash(socket, :error, "Sign in to join tribes")} is_nil(socket.assigns.tribe) -> - {:noreply, put_flash(socket, :error, gettext("No tribe selected"))} + {:noreply, put_flash(socket, :error, "No tribe selected")} true -> opts = @@ -61,15 +57,10 @@ defmodule AetherWeb.TimelineLive do case Communities.join_tribe(socket.assigns.current_user, socket.assigns.tribe.id, opts) do {:ok, _membership} -> - socket = refresh_scope(socket) - - {:noreply, - socket - |> put_flash(:info, gettext("Joined tribe")) - |> assign(:join_form, to_form(%{"invite_code" => ""}, as: :join))} + {:noreply, socket |> refresh_scope() |> put_flash(:info, "Joined tribe")} {:error, _reason} -> - {:noreply, put_flash(socket, :error, gettext("Unable to join tribe"))} + {:noreply, put_flash(socket, :error, "Unable to join tribe")} end end end @@ -79,13 +70,13 @@ defmodule AetherWeb.TimelineLive do cond do socket.assigns[:current_user] == nil -> - {:noreply, put_flash(socket, :error, gettext("Sign in to post"))} + {:noreply, put_flash(socket, :error, "Sign in to post")} content == "" -> - {:noreply, put_flash(socket, :error, gettext("Post cannot be empty"))} + {:noreply, put_flash(socket, :error, "Post cannot be empty")} not can_post_to_scope?(socket) -> - {:noreply, put_flash(socket, :error, gettext("Join this tribe to post"))} + {:noreply, put_flash(socket, :error, "Join this tribe to post")} true -> case socket.assigns[:session_privkey] do @@ -97,28 +88,19 @@ defmodule AetherWeb.TimelineLive do {:noreply, socket - |> assign(:form, to_form(%{"content" => ""}, as: :note)) |> assign(:note_ids, note_ids) |> assign(:note_count, MapSet.size(note_ids)) |> stream_insert(:notes, event, at: 0)} else - {:noreply, - socket - |> assign(:form, to_form(%{"content" => ""}, as: :note)) - |> put_flash(:info, gettext("Posted outside current tribe scope"))} + {:noreply, put_flash(socket, :info, "Posted outside current tribe scope")} end {:error, reason} -> - {:noreply, - put_flash( - socket, - :error, - gettext("Failed to publish: %{reason}", reason: inspect(reason)) - )} + {:noreply, put_flash(socket, :error, "Failed to publish: #{inspect(reason)}")} end _other -> - {:noreply, put_flash(socket, :error, gettext("Signing key unavailable in session"))} + {:noreply, put_flash(socket, :error, "Signing key unavailable in session")} end end end @@ -162,84 +144,90 @@ defmodule AetherWeb.TimelineLive do @impl true def render(assigns) do ~H""" - -
-
-

{timeline_title(@tribe)}

-
{gettext("%{count} notes", count: @note_count)}
-
+
+
+

{timeline_title(@tribe)}

+
{@note_count} notes
+
- <%= if @tribe && !@is_tribe_member? do %> -
-
-

- {gettext("You are viewing this tribe as a guest.")} -

- <.form for={@join_form} id="join-tribe-form" phx-submit="join_tribe" class="space-y-3"> - <.input - field={@join_form[:invite_code]} + <%= if @tribe && !@is_tribe_member? do %> +
+
+

+ You are viewing this tribe as a guest. +

+
+
-
- <% end %> - - <%= if @current_user && (@can_view_tribe? || is_nil(@tribe)) do %> - <.form for={@form} id="note-form" phx-submit="post" class="card bg-base-100 shadow-sm"> -
- <.input field={@form[:content]} type="textarea" label={gettext("Post")} rows="3" /> +
-
-
- - <% end %> - - <%= if !@can_view_tribe? && @tribe do %> -
- {gettext("This tribe timeline is private until you join.")} +
- <% end %> - -
- - -
-
-
- <.link navigate={~p"/u/#{note["pubkey"]}"} class="link link-hover font-medium"> - {author_label(@profile_cache, note["pubkey"])} - - - -
-

{note["content"]}

-
-
+ <% end %> + + <%= if @current_user && (@can_view_tribe? || is_nil(@tribe)) do %> +
+
+ +
+ +
+
+
+ <% end %> + + <%= if !@can_view_tribe? && @tribe do %> +
+ This tribe timeline is private until you join. +
+ <% end %> + +
+ + +
- +
""" end @@ -266,7 +254,7 @@ defmodule AetherWeb.TimelineLive do defp parse_tribe_id_from_plugin_path(_path), do: nil defp load_scope(current_user, tribe_id) when is_binary(tribe_id) do - with {:ok, %Tribe{} = tribe} <- Communities.get_tribe(tribe_id), + with {:ok, tribe} when is_map(tribe) <- Communities.get_tribe(tribe_id), {:ok, members} <- Communities.list_tribe_members_for_timeline(tribe.id) do member_pubkeys = members |> Enum.map(& &1.user.pubkey_hex) |> MapSet.new() @@ -276,7 +264,7 @@ defmodule AetherWeb.TimelineLive do user -> Enum.any?(members, &(&1.user_id == user.id)) end - can_view? = tribe.visibility == :public || is_member + can_view? = Map.get(tribe, :visibility) == :public || is_member %{tribe: tribe, member_pubkeys: member_pubkeys, member?: is_member, can_view?: can_view?} else @@ -361,6 +349,11 @@ defmodule AetherWeb.TimelineLive do profile["name"] || "#{String.slice(pubkey, 0, 8)}…" end - defp timeline_title(nil), do: gettext("Global timeline") - defp timeline_title(%Tribe{name: name}), do: gettext("%{name} timeline", name: name) + defp timeline_title(nil), do: "Global timeline" + + defp timeline_title(%{name: name}) when is_binary(name) do + "#{name} timeline" + end + + defp timeline_title(_tribe), do: "Tribe timeline" end diff --git a/mix.exs b/mix.exs index 1a3c3bc..c767e58 100644 --- a/mix.exs +++ b/mix.exs @@ -26,15 +26,14 @@ defmodule Aether.MixProject do defp deps do [ - # Host dependency — provides Tribes.Plugin behaviour, types, and test helpers. + # Plugin API dependency for local development alongside a tribes checkout. # - # For local development alongside a tribes checkout: - {:tribes, path: "../tribes", runtime: false}, - # - # For CI or standalone development (when not co-located with tribes): - # {:tribes, github: "your-org/tribes", branch: "master", only: [:dev, :test]}, - - {:jason, "~> 1.2"} + # For CI or standalone development, this can be replaced with a published + # package once tribes_plugin_api is released. + {:tribes_plugin_api, path: "../tribes/tribes_plugin_api", runtime: false}, + {:phoenix, "~> 1.8"}, + {:phoenix_html, "~> 4.1"}, + {:phoenix_live_view, "~> 1.1.0"} ] end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..a6efb60 --- /dev/null +++ b/mix.lock @@ -0,0 +1,13 @@ +%{ + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.28", "8a8e123d018025f756605a2fb02a4854f0d3cd7b207f710fef1fd5d9d72d0254", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24faad535b65089642c3a7d84088109dc58f49c1f1c5a978659855d643466353"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, +} diff --git a/test/aether/host_integration_test.exs b/test/aether/host_integration_test.exs index 0a14859..c2ca236 100644 --- a/test/aether/host_integration_test.exs +++ b/test/aether/host_integration_test.exs @@ -1,27 +1,29 @@ -defmodule Aether.HostIntegrationTest do - use ExUnit.Case, async: false +if Code.ensure_loaded?(Tribes.PluginRegistry) and Code.ensure_loaded?(TribesWeb.Navigation) do + defmodule Aether.HostIntegrationTest do + use ExUnit.Case, async: false - setup do - start_supervised!({Tribes.PluginRegistry, []}) + setup do + start_supervised!({Tribes.PluginRegistry, []}) - spec = Tribes.Plugins.Aether.Plugin.register(%{pubsub: nil, repo: nil}) - :ok = Tribes.PluginRegistry.register_plugin(spec.name, spec, "/tmp/aether") + spec = Tribes.Plugins.Aether.Plugin.register(%{pubsub: nil, repo: nil}) + :ok = Tribes.PluginRegistry.register_plugin(spec.name, spec, "/tmp/aether") - on_exit(fn -> - :ok = Tribes.PluginRegistry.unregister_plugin(spec.name) - end) + on_exit(fn -> + :ok = Tribes.PluginRegistry.unregister_plugin(spec.name) + end) - %{spec: spec} - end + %{spec: spec} + end - test "registers timeline capability and /aether route with host", %{spec: spec} do - assert spec.name == "aether" - assert %{name: "aether"} = Tribes.PluginRegistry.provider!("timeline_ui@1") + test "registers timeline capability and /aether route with host", %{spec: spec} do + assert spec.name == "aether" + assert %{name: "aether"} = Tribes.PluginRegistry.provider!("timeline_ui@1") - assert {:ok, "aether", %{path: "/aether"}} = - Tribes.PluginRegistry.page_for_path("/aether/tribe-123") + assert {:ok, "aether", %{path: "/aether"}} = + Tribes.PluginRegistry.page_for_path("/aether/tribe-123") - assert TribesWeb.Navigation.timeline_base_path() == "/aether" - assert TribesWeb.Navigation.timeline_path("tribe-123") == "/aether/tribe-123" + assert TribesWeb.Navigation.timeline_base_path() == "/aether" + assert TribesWeb.Navigation.timeline_path("tribe-123") == "/aether/tribe-123" + end end end diff --git a/test/aether/manifest_test.exs b/test/aether/manifest_test.exs index 4d1a890..c892315 100644 --- a/test/aether/manifest_test.exs +++ b/test/aether/manifest_test.exs @@ -5,7 +5,7 @@ defmodule Aether.ManifestTest do setup_all do content = File.read!(@manifest_path) - manifest = Jason.decode!(content) + manifest = JSON.decode!(content) %{manifest: manifest} end diff --git a/test/contract_test.exs b/test/contract_test.exs index db231ef..27d387b 100644 --- a/test/contract_test.exs +++ b/test/contract_test.exs @@ -21,7 +21,7 @@ defmodule Aether.ContractTest do @manifest_path Path.join(__DIR__, "../manifest.json") |> Path.expand() setup_all do - manifest = @manifest_path |> File.read!() |> Jason.decode!() + manifest = @manifest_path |> File.read!() |> JSON.decode!() spec = @plugin.register(%{pubsub: nil, repo: nil}) %{manifest: manifest, spec: spec} end