fix: A1-17 route bds2://new-post deep links into transform pipeline and draft creation

This commit is contained in:
2026-05-30 08:58:22 +02:00
parent ebf6136d2f
commit 7045b10738
20 changed files with 1128 additions and 607 deletions

View File

@@ -0,0 +1,74 @@
defmodule BDS.Desktop.DeepLink do
@moduledoc """
Receives OS URL-scheme events for the `bds2://` scheme and routes them to the
shell (spec: script.allium `BlogmarkReceived`).
On macOS the app bundle registers `bds2://` as a custom URL scheme (see the
`CFBundleURLTypes` entry in the packaged `Info.plist`). When the browser
bookmarklet navigates to `bds2://new-post?title=&url=`, the OS launches/raises
the app and `Desktop.Env` delivers an `{:open_url, [url]}` event. This
GenServer subscribes to those events and forwards recognised `bds2://` links to
the live shell over PubSub, where `BDS.Blogmark` turns them into draft posts.
The `bds2://` scheme is distinct from the legacy app's `bds://` so the two
installs do not contend for the same registration.
"""
use GenServer
require Logger
alias BDS.CliSync.Watcher
@scheme "bds2://"
def child_spec(opts) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
end
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, __MODULE__))
end
@impl true
def init(opts) do
pubsub = Keyword.get(opts, :pubsub, BDS.PubSub)
topic = Keyword.get(opts, :topic, Watcher.topic())
subscribe_to_env()
{:ok, %{pubsub: pubsub, topic: topic}}
end
# Desktop.Env delivers OS events as {event_name, args} tuples.
@impl true
def handle_info({:open_url, [url | _rest]}, state) when is_binary(url) do
{:noreply, route(url, state)}
end
def handle_info(_message, state), do: {:noreply, state}
defp route(url, state) do
if String.starts_with?(url, @scheme) do
Phoenix.PubSub.broadcast(state.pubsub, state.topic, {:blogmark_deep_link, url})
else
Logger.debug("ignoring non-bds2 deep link: #{inspect(url)}")
end
state
end
# Desktop.Env is only present when the wx desktop adapter is running. Guard the
# subscribe so the GenServer can still start in headless/test configurations.
defp subscribe_to_env do
if Process.whereis(Desktop.Env) do
try do
Desktop.Env.subscribe()
catch
:exit, _reason -> :ok
end
end
:ok
end
end

View File

@@ -5,7 +5,7 @@ defmodule BDS.Desktop.ShellLive do
import Phoenix.HTML
alias BDS.{AI, BoundedAtoms, Metadata}
alias BDS.{AI, Blogmark, BoundedAtoms, Metadata}
alias BDS.CliSync.Watcher
alias BDS.Desktop.{ExternalLinks, FilePicker, FolderPicker, ShellData, UILocale}
@@ -717,6 +717,10 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, socket}
end
def handle_info({:blogmark_deep_link, url}, socket) when is_binary(url) do
{:noreply, handle_blogmark_deep_link(socket, url)}
end
def handle_info(message, socket) do
Bridges.handle_info(message, socket, bridges_callbacks())
end
@@ -1002,6 +1006,56 @@ defmodule BDS.Desktop.ShellLive do
defp create_sidebar_item(socket, kind),
do: SidebarCreate.create(socket, kind, sidebar_create_callbacks())
# Receive a bds2://new-post blogmark deep link: run the transform pipeline,
# create a draft post, open it in the editor, and surface transform toasts.
defp handle_blogmark_deep_link(socket, url) do
title = dgettext("ui", "Blogmark")
case current_project_id(socket) do
project_id when is_binary(project_id) ->
case Blogmark.receive_deep_link(project_id, url) do
{:ok, %{post: post, toasts: toasts, errors: errors}} ->
socket
|> reload_shell(socket.assigns.workbench)
|> open_sidebar_item(
%{
"route" => "post",
"id" => post.id,
"title" => post.title,
"subtitle" => post.slug
},
:pin
)
|> append_blogmark_toasts(title, toasts)
|> append_blogmark_errors(title, errors)
{:error, reason} ->
append_output_entry(socket, title, inspect(reason), url, "error")
end
_ ->
append_output_entry(
socket,
title,
dgettext("ui", "Open a project before importing a blogmark."),
url,
"warning"
)
end
end
defp append_blogmark_toasts(socket, title, toasts) do
Enum.reduce(toasts, socket, fn message, acc ->
append_output_entry(acc, title, message, nil, "info")
end)
end
defp append_blogmark_errors(socket, title, errors) do
Enum.reduce(errors, socket, fn %{slug: slug, reason: reason}, acc ->
append_output_entry(acc, title, inspect(reason), slug, "error")
end)
end
defp handle_file_picker_result(socket, {:ok, _media}),
do: refresh_content(socket, socket.assigns.workbench)