Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/desktop/platform/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,22 @@ 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
dispatch_info(message, s)
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)
Expand Down
40 changes: 35 additions & 5 deletions lib/desktop/window.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}

Expand Down
80 changes: 80 additions & 0 deletions test/desktop/window_notification_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading