mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-09 01:06:03 +00:00
feat(Core): Updated map active characters page
This commit is contained in:
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 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
77
priv/posts/2025/05-11-map-active-characters.md
Normal file
77
priv/posts/2025/05-11-map-active-characters.md
Normal 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
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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**
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user