feat(Core): Updated map active characters page

This commit is contained in:
Dmitry Popov
2025-05-11 14:03:53 +02:00
parent 8f0ed44b11
commit 1be4ec2b90
10 changed files with 266 additions and 85 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -161,12 +161,12 @@ defmodule WandererApp.Character.TrackingUtils do
end end
# Helper functions for character tracking # Helper functions for character tracking
def track(_, _, false, _), do: :ok
def track([], _map_id, _is_track_character?, _), do: :ok def track([], _map_id, _is_track_character?, _), do: :ok
def track([character | characters], map_id, true, caller_pid) do def track([character | characters], map_id, is_track_allowed, caller_pid) do
with :ok <- track_character(character, map_id, caller_pid) do with :ok <- track_character(character, map_id, is_track_allowed, caller_pid) do
track(characters, map_id, true, caller_pid) track(characters, map_id, is_track_allowed, caller_pid)
end end
end end
@@ -176,28 +176,55 @@ defmodule WandererApp.Character.TrackingUtils do
eve_id: eve_id eve_id: eve_id
}, },
map_id, map_id,
is_track_allowed,
caller_pid caller_pid
) do )
with false <- is_nil(caller_pid) do when not is_nil(caller_pid) do
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{}) WandererAppWeb.Presence.update(caller_pid, map_id, character_id, %{
tracked: is_track_allowed,
from: DateTime.utc_now()
})
|> case do
{:ok, _} ->
:ok
cache_key = "#{inspect(caller_pid)}_map_#{map_id}:character_#{character_id}:tracked" {:error, :nopresence} ->
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{
tracked: is_track_allowed,
from: DateTime.utc_now()
})
case WandererApp.Cache.lookup!(cache_key, false) do error ->
true -> Logger.error("Failed to update presence: #{inspect(error)}")
:ok {:error, "Failed to update presence"}
_ ->
:ok = Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{eve_id}")
:ok = WandererApp.Cache.put(cache_key, true)
end
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
else
true ->
Logger.error("caller_pid is required for tracking characters")
{:error, "caller_pid is required"}
end end
cache_key = "#{inspect(caller_pid)}_map_#{map_id}:character_#{character_id}:tracked"
case WandererApp.Cache.lookup!(cache_key, false) do
true ->
:ok
_ ->
:ok = Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{eve_id}")
:ok = WandererApp.Cache.put(cache_key, true)
end
if is_track_allowed do
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
end
:ok
end
defp track_character(
_character,
_map_id,
_is_track_allowed,
_caller_pid
) do
Logger.error("caller_pid is required for tracking characters")
{:error, "caller_pid is required"}
end end
def untrack(characters, map_id, caller_pid) do def untrack(characters, map_id, caller_pid) do
@@ -206,7 +233,10 @@ defmodule WandererApp.Character.TrackingUtils do
characters characters
|> Enum.each(fn character -> |> Enum.each(fn character ->
WandererAppWeb.Presence.untrack(caller_pid, map_id, character.id) WandererAppWeb.Presence.update(caller_pid, map_id, character.id, %{
tracked: false,
from: DateTime.utc_now()
})
end) end)
WandererApp.Map.Server.untrack_characters(map_id, character_ids) WandererApp.Map.Server.untrack_characters(map_id, character_ids)

View File

@@ -17,24 +17,26 @@ defmodule WandererAppWeb.MapCharacters do
|> handle_info_or_assign(assigns)} |> handle_info_or_assign(assigns)}
end end
# attr(:groups, :any, required: true)
# attr(:character_settings, :any, required: true)
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div id={@id}> <div id={@id}>
<ul :for={group <- @groups} class="space-y-4 border-t border-b border-gray-200 py-4"> <ul :for={group <- @groups} class="border-t border-b border-gray-200 py-0">
<li :for={character <- group.characters}> <li :for={character <- group.characters}>
<div class="flex items-center justify-between w-full space-x-2 p-1 hover:bg-gray-900"> <div class="flex items-center justify-between w-full space-x-2 p-1 hover:bg-gray-900">
<.character_entry character={character} character_settings={@character_settings} /> <.character_entry character={character} />
<button <button
:if={character.tracked}
phx-click="untrack" phx-click="untrack"
phx-value-event-data={character.id} phx-value-event-data={character.id}
class="btn btn-sm btn-icon" class="btn btn-sm btn-icon py-1"
> >
<.icon name="hero-eye-slash" class="h-5 w-5" /> Untrack <.icon name="hero-eye-slash" class="h-5 w-5" /> Untrack
</button> </button>
<span :if={not character.tracked} class="text-white rounded-full px-2">
Viewer
</span>
</div> </div>
</li> </li>
</ul> </ul>
@@ -43,31 +45,43 @@ defmodule WandererAppWeb.MapCharacters do
end end
attr(:character, :any, required: true) attr(:character, :any, required: true)
attr(:character_settings, :any, required: true)
defp character_entry(assigns) do defp character_entry(assigns) do
~H""" ~H"""
<div class="flex items-center gap-3 text-sm w-[450px]"> <div class="flex items-center gap-3 text-sm w-[450px]">
<span <div class="flex flex-col p-4 items-center gap-2 tooltip tooltip-top" data-tip="Active from">
:if={is_tracked?(@character.id, @character_settings)} <span class="text-green-500 rounded-full px-2 py-1 whitespace-nowrap">
class="text-green-500 rounded-full px-2 py-1" <.local_time id={@character.id} at={@character.from} />
> </span>
Tracked </div>
<div class="avatar">
<div class="rounded-md w-8 h-8">
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
</div>
</div>
<span class="whitespace-nowrap">{@character.name}</span>
<span :if={@character.alliance_ticker} class="whitespace-nowrap">
[{@character.alliance_ticker}]
</span> </span>
<span :if={@character.corporation_ticker} class="whitespace-nowrap">
[{@character.corporation_ticker}]
</span>
<span :if={is_online?(@character.id)} class="text-green-500 rounded-full px-2 py-1"> <span :if={is_online?(@character.id)} class="text-green-500 rounded-full px-2 py-1">
Online Online
</span> </span>
<span :if={not is_online?(@character.id)} class="text-red-500 rounded-full px-2 py-1"> <span :if={not is_online?(@character.id)} class="text-red-500 rounded-full px-2 py-1">
Offline Offline
</span> </span>
<div class="avatar">
<div class="rounded-md w-8 h-8"> <span :if={@character.tracked} class="text-green-500 rounded-full px-2 py-1">
<img src={member_icon_url(@character.eve_id)} alt={@character.name} /> Tracked
</div> </span>
</div>
<span>{@character.name}</span> <span :if={not @character.tracked} class="text-red-500 rounded-full px-2 py-1 whitespace-nowrap">
<span :if={@character.alliance_ticker}>[{@character.alliance_ticker}]</span> Not Tracked
<span :if={@character.corporation_ticker}>[{@character.corporation_ticker}]</span> </span>
</div> </div>
""" """
end end
@@ -79,12 +93,6 @@ defmodule WandererAppWeb.MapCharacters do
{:noreply, socket} {:noreply, socket}
end end
defp is_tracked?(character_id, character_settings) do
Enum.any?(character_settings, fn setting ->
setting.character_id == character_id && setting.tracked
end)
end
defp is_online?(character_id) do defp is_online?(character_id) do
{:ok, state} = WandererApp.Character.get_character_state(character_id) {:ok, state} = WandererApp.Character.get_character_state(character_id)
state.is_online state.is_online

View File

@@ -226,7 +226,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
def handle_ui_event( def handle_ui_event(
_event, _event,
_body, _body,
%{assigns: %{has_tracked_characters?: false}} = %{assigns: %{has_tracked_characters?: false, can_track?: true}} =
socket socket
) do ) do
Process.send_after(self(), %{event: :show_tracking}, 10) Process.send_after(self(), %{event: :show_tracking}, 10)
@@ -242,7 +242,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
def handle_ui_event( def handle_ui_event(
event, event,
body, body,
%{assigns: %{main_character_id: main_character_id}} = %{assigns: %{main_character_id: main_character_id, can_track?: true}} =
socket socket
) )
when is_nil(main_character_id) do when is_nil(main_character_id) do
@@ -260,7 +260,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
{:ok, map_server_started} = WandererApp.Cache.lookup("map_#{map_id}:started", false) {:ok, map_server_started} = WandererApp.Cache.lookup("map_#{map_id}:started", false)
if map_server_started do if map_server_started do
Process.send_after(self(), %{event: :map_server_started}, 10) Process.send_after(self(), %{event: :map_server_started}, 50)
else else
WandererApp.Map.Manager.start_map(map_id) WandererApp.Map.Manager.start_map(map_id)
end end
@@ -433,6 +433,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
assigns: %{ assigns: %{
current_user: current_user, current_user: current_user,
map_id: map_id, map_id: map_id,
main_character_id: main_character_id,
tracked_characters: tracked_characters, tracked_characters: tracked_characters,
has_tracked_characters?: has_tracked_characters?, has_tracked_characters?: has_tracked_characters?,
user_permissions: user_permissions:
@@ -454,7 +455,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
end end
events = events =
case not has_tracked_characters? do case track_character && not has_tracked_characters? do
true -> true ->
events ++ [:empty_tracked_characters] events ++ [:empty_tracked_characters]
@@ -462,13 +463,28 @@ defmodule WandererAppWeb.MapCoreEventHandler do
events events
end end
character_limit_reached? = present_character_ids |> Enum.count() >= characters_limit
events = events =
case present_character_ids |> Enum.count() < characters_limit do cond do
true -> # in case user has not tracked any character track his main character as viewer
track_character && not has_tracked_characters? ->
main_character = Enum.find(current_user.characters, &(&1.id == main_character_id))
events ++ [{:track_characters, [main_character], false}]
track_character && not character_limit_reached? ->
events ++ [{:track_characters, tracked_characters, track_character}] events ++ [{:track_characters, tracked_characters, track_character}]
_ -> track_character && character_limit_reached? ->
events ++ [:map_character_limit] events ++ [:map_character_limit]
# in case user has view only permissions track his main character as viewer
not track_character ->
main_character = Enum.find(current_user.characters, &(&1.id == main_character_id))
events ++ [{:track_characters, [main_character], track_character}]
true ->
events
end end
initial_data = initial_data =

View File

@@ -5,6 +5,8 @@ defmodule WandererAppWeb.MapCharactersLive do
alias WandererAppWeb.MapCharacters alias WandererAppWeb.MapCharacters
@refresh_interval :timer.seconds(30)
def mount( def mount(
%{"slug" => map_slug} = _params, %{"slug" => map_slug} = _params,
_session, _session,
@@ -44,6 +46,15 @@ defmodule WandererAppWeb.MapCharactersLive do
{:noreply, apply_action(socket, socket.assigns.live_action, params)} {:noreply, apply_action(socket, socket.assigns.live_action, params)}
end end
@impl true
def handle_info(
:refresh_tracking_data,
socket
) do
Process.send_after(self(), :refresh_tracking_data, @refresh_interval)
{:noreply, socket |> load_characters()}
end
@impl true @impl true
def handle_info( def handle_info(
_event, _event,
@@ -101,17 +112,35 @@ defmodule WandererAppWeb.MapCharactersLive do
end end
defp apply_action(socket, :index, _params) do defp apply_action(socket, :index, _params) do
Process.send_after(self(), :refresh_tracking_data, @refresh_interval)
socket socket
|> assign(:active_page, :map_characters) |> assign(:active_page, :map_characters)
|> assign(:page_title, "Map - Characters") |> assign(:page_title, "Map - Characters")
|> load_characters() |> load_characters()
end end
defp get_all_characters(map_id) do
{:ok, present_characters} =
WandererApp.Cache.lookup(
"map_#{map_id}:presence_data",
[]
)
present_characters =
present_characters
|> Enum.map(fn character ->
character |> Map.merge(WandererApp.Character.get_character!(character.character_id))
end)
present_characters
end
defp load_characters(%{assigns: %{map_id: map_id}} = socket) do defp load_characters(%{assigns: %{map_id: map_id}} = socket) do
map_characters = map_characters =
map_id map_id
|> WandererApp.Map.list_characters() |> get_all_characters()
|> Enum.map(&map_ui_character/1) |> Enum.map(fn character -> map_ui_character(map_id, character) end)
groups = groups =
map_characters map_characters
@@ -132,20 +161,22 @@ defmodule WandererAppWeb.MapCharactersLive do
|> assign(:groups, groups) |> assign(:groups, groups)
end end
defp map_ui_character(character), defp map_ui_character(map_id, character) do
do: character
character |> Map.take([
|> Map.take([ :id,
:id, :user_id,
:user_id, :eve_id,
:eve_id, :name,
:name, :online,
:online, :corporation_id,
:corporation_id, :corporation_name,
:corporation_name, :corporation_ticker,
:corporation_ticker, :alliance_id,
:alliance_id, :alliance_name,
:alliance_name, :alliance_ticker,
:alliance_ticker :from,
]) :tracked
])
end
end end

View File

@@ -3,7 +3,7 @@
<.link navigate={~p"/#{@map_slug}"} class="text-neutral-100"> <.link navigate={~p"/#{@map_slug}"} class="text-neutral-100">
<%= @map_name %> <%= @map_name %>
</.link> </.link>
- Characters [<%= @characters_count %>] - Active Characters [<%= @characters_count %>]
</span> </span>
</nav> </nav>
<main <main
@@ -24,7 +24,6 @@
id="map-characters" id="map-characters"
notify_to={self()} notify_to={self()}
groups={@groups} groups={@groups}
character_settings={@character_settings}
event_name="character_event" event_name="character_event"
/> />
</div> </div>

View File

@@ -10,19 +10,39 @@ defmodule WandererAppWeb.Presence do
end end
def handle_metas(map_id, %{joins: _joins, leaves: _leaves}, presences, state) do def handle_metas(map_id, %{joins: _joins, leaves: _leaves}, presences, state) do
presence_character_ids = presence_data =
presences presences
|> Enum.map(fn {character_id, _} -> character_id end) |> Enum.map(fn {character_id, meta} ->
from =
meta
|> Enum.map(& &1.from)
|> Enum.sort(&(DateTime.compare(&1, &2) != :gt))
|> List.first()
any_tracked = Enum.any?(meta, fn %{tracked: tracked} -> tracked end)
%{character_id: character_id, tracked: any_tracked, from: from}
end)
presence_tracked_character_ids =
presence_data
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|> Enum.map(fn %{character_id: character_id} ->
character_id
end)
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true) WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
WandererApp.Cache.insert("map_#{map_id}:presence_character_ids", presence_character_ids)
WandererApp.Cache.insert(
"map_#{map_id}:presence_character_ids",
presence_tracked_character_ids
)
WandererApp.Cache.insert(
"map_#{map_id}:presence_data",
presence_data
)
{:ok, state} {:ok, state}
end end
def presence_character_ids(map_id) do
map_id
|> list()
|> Enum.map(fn {character_id, _} -> character_id end)
end
end end

View File

@@ -0,0 +1,77 @@
%{
title: "Map Active Characters Page — Interface Guide",
author: "Wanderer Team",
cover_image_uri: "/images/news/2025/05-11-map-active-characters/cover.png",
tags: ~w(characters interface guide map security),
description: "This interface is essential for managing access and tracking behavior on shared maps, especially in large organizations."
}
---
### Introduction
![Maps Icon](/images/news/2025/05-11-map-active-characters/cover.png "Maps Icon")
![Map Icon](/images/news/2025/05-11-map-active-characters/map.png "Map Icon")
This page displays **only currently active characters** — those who have the map page open in an active browser tab or window.
### Key Use Cases:
- Identify active pilots on your map
- Monitor user activity and access level
- Manage tracked status to stay within subscription limits
---
## 👤 Character Grouping by User
Each user may have multiple EVE Online characters authorized. On this page:
- Characters are **grouped under their owning user**
- Admins can easily see which characters belong to the same person
- Useful for distinguishing between multiboxers or corp mates sharing access
---
## 📋 Character Info Displayed
![Page](/images/news/2025/05-11-map-active-characters/page.png "Page")
Each tracked character on this page includes:
| Field | Description |
|--------------------|-----------------------------------------------------------------------------|
| **Active From** | Timestamp indicating when the character opened the map (based on real-time browser tab activity) |
| **Character Info** | Character Name, Corporation, and Alliance |
| **Tracked Status** | Whether the character is being actively tracked on the map |
| **Online Status** | Online/offline status (in-game) |
---
## 🔧 Admin Actions
Map **owners and administrators** can:
-**See viewer-only access**: Identify characters who can view but are not being tracked.
- 🚫 **Force Untrack** characters:
- Stop tracking & remove characters from map.
- Useful to stay within your character limit or reset tracking manually.
- Note: The user should re-enable tracking later if needed manually in map tracking settings.
---
## 🛑 Notes & Recommendations
- A character is **counted toward the Characters Limit** of the map's subscription **only when tracked**.
- Tracking begins **as soon as the character opens the map page** in any tab/browser.
- Closing all tabs with the map **automatically stops tracking** for that character (after a small period about 15 minutes).
- This system ensures you always have a live (tracking data automatically updated every 30 seconds), accurate picture of map usage across your team.
---
By using the **Map Active Characters** page, admins can efficiently manage map activity, maintain security, and optimize performance across their team or alliance.
---
Fly safe,
**The Wanderer Team**
---