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
# Helper functions for character tracking
def track(_, _, false, _), do: :ok
def track([], _map_id, _is_track_character?, _), do: :ok
def track([character | characters], map_id, true, caller_pid) do
with :ok <- track_character(character, map_id, caller_pid) do
track(characters, map_id, true, caller_pid)
def track([character | characters], map_id, is_track_allowed, caller_pid) do
with :ok <- track_character(character, map_id, is_track_allowed, caller_pid) do
track(characters, map_id, is_track_allowed, caller_pid)
end
end
@@ -176,28 +176,55 @@ defmodule WandererApp.Character.TrackingUtils do
eve_id: eve_id
},
map_id,
is_track_allowed,
caller_pid
) do
with false <- is_nil(caller_pid) do
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{})
)
when not is_nil(caller_pid) do
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
true ->
:ok
_ ->
: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"}
error ->
Logger.error("Failed to update presence: #{inspect(error)}")
{:error, "Failed to update presence"}
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
def untrack(characters, map_id, caller_pid) do
@@ -206,7 +233,10 @@ defmodule WandererApp.Character.TrackingUtils do
characters
|> 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)
WandererApp.Map.Server.untrack_characters(map_id, character_ids)

View File

@@ -17,24 +17,26 @@ defmodule WandererAppWeb.MapCharacters do
|> handle_info_or_assign(assigns)}
end
# attr(:groups, :any, required: true)
# attr(:character_settings, :any, required: true)
@impl true
def render(assigns) do
~H"""
<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}>
<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
:if={character.tracked}
phx-click="untrack"
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
</button>
<span :if={not character.tracked} class="text-white rounded-full px-2">
Viewer
</span>
</div>
</li>
</ul>
@@ -43,31 +45,43 @@ defmodule WandererAppWeb.MapCharacters do
end
attr(:character, :any, required: true)
attr(:character_settings, :any, required: true)
defp character_entry(assigns) do
~H"""
<div class="flex items-center gap-3 text-sm w-[450px]">
<span
:if={is_tracked?(@character.id, @character_settings)}
class="text-green-500 rounded-full px-2 py-1"
>
Tracked
<div class="flex flex-col p-4 items-center gap-2 tooltip tooltip-top" data-tip="Active from">
<span class="text-green-500 rounded-full px-2 py-1 whitespace-nowrap">
<.local_time id={@character.id} at={@character.from} />
</span>
</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 :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">
Online
</span>
<span :if={not is_online?(@character.id)} class="text-red-500 rounded-full px-2 py-1">
Offline
</span>
<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>{@character.name}</span>
<span :if={@character.alliance_ticker}>[{@character.alliance_ticker}]</span>
<span :if={@character.corporation_ticker}>[{@character.corporation_ticker}]</span>
<span :if={@character.tracked} class="text-green-500 rounded-full px-2 py-1">
Tracked
</span>
<span :if={not @character.tracked} class="text-red-500 rounded-full px-2 py-1 whitespace-nowrap">
Not Tracked
</span>
</div>
"""
end
@@ -79,12 +93,6 @@ defmodule WandererAppWeb.MapCharacters do
{:noreply, socket}
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
{:ok, state} = WandererApp.Character.get_character_state(character_id)
state.is_online

View File

@@ -226,7 +226,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
def handle_ui_event(
_event,
_body,
%{assigns: %{has_tracked_characters?: false}} =
%{assigns: %{has_tracked_characters?: false, can_track?: true}} =
socket
) do
Process.send_after(self(), %{event: :show_tracking}, 10)
@@ -242,7 +242,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
def handle_ui_event(
event,
body,
%{assigns: %{main_character_id: main_character_id}} =
%{assigns: %{main_character_id: main_character_id, can_track?: true}} =
socket
)
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)
if map_server_started do
Process.send_after(self(), %{event: :map_server_started}, 10)
Process.send_after(self(), %{event: :map_server_started}, 50)
else
WandererApp.Map.Manager.start_map(map_id)
end
@@ -433,6 +433,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
assigns: %{
current_user: current_user,
map_id: map_id,
main_character_id: main_character_id,
tracked_characters: tracked_characters,
has_tracked_characters?: has_tracked_characters?,
user_permissions:
@@ -454,7 +455,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
end
events =
case not has_tracked_characters? do
case track_character && not has_tracked_characters? do
true ->
events ++ [:empty_tracked_characters]
@@ -462,13 +463,28 @@ defmodule WandererAppWeb.MapCoreEventHandler do
events
end
character_limit_reached? = present_character_ids |> Enum.count() >= characters_limit
events =
case present_character_ids |> Enum.count() < characters_limit do
true ->
cond do
# 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}]
_ ->
track_character && character_limit_reached? ->
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
initial_data =

View File

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

View File

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

View File

@@ -10,19 +10,39 @@ defmodule WandererAppWeb.Presence do
end
def handle_metas(map_id, %{joins: _joins, leaves: _leaves}, presences, state) do
presence_character_ids =
presence_data =
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_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}
end
def presence_character_ids(map_id) do
map_id
|> list()
|> Enum.map(fn {character_id, _} -> character_id 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**
---