diff --git a/lib/desktop/platform/server.ex b/lib/desktop/platform/server.ex index 74c061e..107e2f2 100644 --- a/lib/desktop/platform/server.ex +++ b/lib/desktop/platform/server.ex @@ -23,7 +23,9 @@ defmodule Desktop.Platform.Server do @impl true def handle_info(message, s = %__MODULE__{state: state, module: module}) do - if function_exported?(module, :handle_event, 2) and is_tuple(message) do + # Only wx event records go to handle_event/2. Other tuples (e.g. + # {:edw_notification, id, action}) must reach handle_info/2. + if function_exported?(module, :handle_event, 2) and wx_event?(message) do module.handle_event(message, state) |> wrap_result(s) else @@ -31,6 +33,12 @@ defmodule Desktop.Platform.Server do end end + defp wx_event?(message) when is_tuple(message) and tuple_size(message) > 0 do + elem(message, 0) == :wx + end + + defp wx_event?(_), do: false + defp dispatch_info(message, s = %__MODULE__{state: state, module: module}) do if function_exported?(module, :handle_info, 2) do module.handle_info(message, state) diff --git a/lib/desktop/window.ex b/lib/desktop/window.ex index 42754a7..51899a8 100644 --- a/lib/desktop/window.ex +++ b/lib/desktop/window.ex @@ -587,6 +587,27 @@ defmodule Desktop.Window do end end + @doc false + def handle_info({:edw_notification, id, action}, ui) do + notification_by_id(ui, to_string(id), action) + {:noreply, ui} + end + + defp notification_by_id(%Window{notifications: noties}, id, action) do + case Map.get(noties, id) do + nil -> + Logger.error( + "Received unhandled notification event #{inspect(id)}: #{inspect(action)} (#{inspect(noties)})" + ) + + {_ref, nil} -> + :ok + + {_ref, callback} -> + spawn(fn -> callback.(action) end) + end + end + def close_window(wx(userData: pid), inev) do Platform.Window.close_event_veto(inev) GenServer.cast(pid, :close_window) @@ -658,20 +679,29 @@ defmodule Desktop.Window do {:show_notification, message, id, type, title, callback, timeout}, ui = %Window{notifications: noties, title: window_title} ) do + id_key = to_string(id) + {n, _} = - note = - case Map.get(noties, id, nil) do + case Map.get(noties, id_key, nil) do nil -> {Fallback.notification_new(title || window_title, type), callback} {note, _} -> {note, callback} end - Fallback.notification_show(n, message, timeout, title || window_title) - noties = Map.put(noties, id, note) + # Native backends (`{:notification, _, _}`) correlate OS clicks by string id. + # Pass that id into show/close so EventBridge `edw_notification` matches map keys. + {show_handle, store_handle} = + case n do + {:notification, _, _} -> {id_key, id_key} + other -> {other, other} + end + + Fallback.notification_show(show_handle, message, timeout, title || window_title) + noties = Map.put(noties, id_key, {store_handle, callback}) {:noreply, %Window{ui | notifications: noties}} end def handle_cast({:dismiss_notification, id}, ui = %Window{notifications: noties}) do - case Map.pop(noties, id) do + case Map.pop(noties, to_string(id)) do {nil, _noties} -> {:noreply, ui} diff --git a/test/desktop/window_notification_test.exs b/test/desktop/window_notification_test.exs new file mode 100644 index 0000000..848c2fd --- /dev/null +++ b/test/desktop/window_notification_test.exs @@ -0,0 +1,80 @@ +defmodule Desktop.WindowNotificationTest do + use Desktop.Test.DesktopCase, async: true + + alias Desktop.Window + + test "edw_notification click invokes stored callback" do + test = self() + + ui = + minimal_window( + notifications: %{ + "nid" => {"nid", fn action -> send(test, {:cb, action}) end} + } + ) + + assert {:noreply, ^ui} = Window.handle_info({:edw_notification, "nid", :click}, ui) + assert_receive {:cb, :click}, 500 + end + + test "edw_notification dismiss with nil callback is a no-op" do + ui = minimal_window(notifications: %{"nid" => {"nid", nil}}) + + assert {:noreply, ^ui} = Window.handle_info({:edw_notification, "nid", :dismiss}, ui) + end + + test "edw_notification unknown id does not crash" do + ui = minimal_window(notifications: %{}) + + assert {:noreply, ^ui} = Window.handle_info({:edw_notification, "missing", :click}, ui) + end + + test "show_notification stores under string key for native-style handles" do + with_backend(:browser, fn -> + # Browser new/2 returns nil; cast still stores under to_string(id). + ui = minimal_window(notifications: %{}) + test = self() + callback = fn action -> send(test, {:cb, action}) end + + assert {:noreply, ui2} = + Window.handle_cast( + {:show_notification, "hello", :demo, :info, "Title", callback, -1}, + ui + ) + + assert Map.has_key?(ui2.notifications, "demo") + assert {_handle, ^callback} = ui2.notifications["demo"] + + assert {:noreply, _} = Window.handle_info({:edw_notification, "demo", :click}, ui2) + assert_receive {:cb, :click}, 500 + end) + end + + test "dismiss_notification pops string key" do + with_backend(:browser, fn -> + ui = minimal_window(notifications: %{"demo" => {nil, nil}}) + + assert {:noreply, ui2} = Window.handle_cast({:dismiss_notification, :demo}, ui) + assert ui2.notifications == %{} + end) + end + + test "show_notification converts native handle to string id for close correlation" do + ui = + minimal_window( + notifications: %{ + "chat-1" => {{:notification, "Title", :info}, fn _ -> :ok end} + } + ) + + with_backend(:browser, fn -> + assert {:noreply, ui2} = + Window.handle_cast( + {:show_notification, "body", "chat-1", :info, "Title", nil, -1}, + ui + ) + + assert {"chat-1", nil} = ui2.notifications["chat-1"] + end) + end +end