Compare commits

..

4 Commits

Author SHA1 Message Date
CI
51489c1aa5 chore: release version v1.32.3 2025-01-02 17:23:05 +00:00
Dmitry Popov
25dd6de770 fix(Map): Fix 'Allow only tracked characters' saving 2025-01-02 18:22:32 +01:00
CI
2a825f5a02 chore: release version v1.32.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-02 10:12:04 +00:00
guarzo
908d249eb9 Allow user to follow a specific tracked character (#87)
* add follow character functionality
2025-01-02 14:09:10 +04:00
17 changed files with 293 additions and 91 deletions

View File

@@ -2,6 +2,20 @@
<!-- changelog -->
## [v1.32.3](https://github.com/wanderer-industries/wanderer/compare/v1.32.2...v1.32.3) (2025-01-02)
### Bug Fixes:
* Map: Fix 'Allow only tracked characters' saving
## [v1.32.2](https://github.com/wanderer-industries/wanderer/compare/v1.32.1...v1.32.2) (2025-01-02)
## [v1.32.1](https://github.com/wanderer-industries/wanderer/compare/v1.32.0...v1.32.1) (2024-12-25)

View File

@@ -14,31 +14,24 @@ defmodule WandererApp.Api.MapCharacterSettings do
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:read_by_map,
action: :read_by_map
)
define(:by_map_filtered,
action: :by_map_filtered
)
define(:tracked_by_map_filtered,
action: :tracked_by_map_filtered
)
define(:tracked_by_map_all,
action: :tracked_by_map_all
)
define(:read_by_map, action: :read_by_map)
define(:by_map_filtered, action: :by_map_filtered)
define(:tracked_by_map_filtered, action: :tracked_by_map_filtered)
define(:tracked_by_map_all, action: :tracked_by_map_all)
define(:track, action: :track)
define(:untrack, action: :untrack)
define(:follow, action: :follow)
define(:unfollow, action: :unfollow)
end
actions do
default_accept [
:map_id,
:character_id,
:tracked
:tracked,
:followed
]
defaults [:create, :read, :update, :destroy]
@@ -76,6 +69,14 @@ defmodule WandererApp.Api.MapCharacterSettings do
update :untrack do
change(set_attribute(:tracked, false))
end
update :follow do
change(set_attribute(:followed, true))
end
update :unfollow do
change(set_attribute(:followed, false))
end
end
attributes do
@@ -86,6 +87,11 @@ defmodule WandererApp.Api.MapCharacterSettings do
allow_nil? true
end
attribute :followed, :boolean do
default false
allow_nil? true
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end

View File

@@ -489,7 +489,7 @@ defmodule WandererApp.Character.Tracker do
defp maybe_update_location(
%{
character_id: character_id
character_id: character_id,
} =
state,
location

View File

@@ -15,13 +15,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: track_character
tracked: track_character,
followed: false
}),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
Impl.broadcast!(map_id, :character_added, character)
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
:ok
else
_error ->
@@ -382,7 +381,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:character_location, %{solar_system_id: solar_system_id},
%{solar_system_id: old_solar_system_id}}
]
_ ->
[:skip]
end

View File

@@ -333,7 +333,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
solar_system_id: location.solar_system_id,
})
Impl.broadcast!(map_id, :add_connection, connection)
@@ -346,9 +346,20 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
:ok
{:error, error} ->
Logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end)
:ok
{:error, :already_exists} ->
# Still broadcast location change in case of followed character
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
})
:ok
{:error, error} ->
Logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end)
:ok
end
end

View File

@@ -84,20 +84,22 @@ defmodule WandererApp.Maps do
id: id,
eve_id: eve_id,
corporation_ticker: corporation_ticker,
tracked: false
tracked: false,
followed: false
}
def map_character(
%{name: name, id: id, eve_id: eve_id, corporation_ticker: corporation_ticker} =
_character,
%{tracked: tracked} = _character_settings
%{tracked: tracked, followed: followed} = _character_settings
),
do: %{
name: name,
id: id,
eve_id: eve_id,
corporation_ticker: corporation_ticker,
tracked: tracked
tracked: tracked,
followed: followed
}
@decorate cacheable(

View File

@@ -24,11 +24,31 @@ defmodule WandererApp.MapCharacterSettingsRepo do
def get_tracked_by_map_all(map_id),
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
def get_by_map(map_id, character_id) do
case get_by_map_filtered(map_id, [character_id]) do
{:ok, [setting | _]} ->
{:ok, setting}
{:ok, []} ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
end
end
def track(settings), do: settings |> WandererApp.Api.MapCharacterSettings.track()
def untrack(settings), do: settings |> WandererApp.Api.MapCharacterSettings.untrack()
def track!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.track!()
def untrack!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.untrack!()
def follow(settings), do: settings |> WandererApp.Api.MapCharacterSettings.follow()
def unfollow(settings), do: settings |> WandererApp.Api.MapCharacterSettings.unfollow()
def follow!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.follow!()
def unfollow!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.unfollow!()
def destroy!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.destroy!()
end

View File

@@ -393,6 +393,7 @@ defmodule WandererAppWeb.CoreComponents do
data-pc-name="checkbox"
data-pc-section="root"
>
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
id={@id}
name={@name}

View File

@@ -86,7 +86,8 @@ defmodule WandererAppWeb.CharactersTrackingLive do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: selected_map.id,
tracked: true
tracked: true,
followed: false
})
{:noreply, socket}

View File

@@ -111,7 +111,8 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: true
tracked: true,
followed: false
})
character = map_character_settings |> Ash.load!(:character) |> Map.get(:character)
@@ -190,6 +191,92 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
)}
end
def handle_ui_event(
"toggle_follow",
%{"character-id" => clicked_char_id},
%{
assigns: %{
map_id: map_id,
current_user: current_user
}
} = socket
) do
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
# Find and filter user's characters
{:ok, user_characters} = get_tracked_map_characters(map_id, current_user)
user_char_ids = Enum.map(user_characters, & &1.id)
my_settings =
all_settings
|> Enum.filter(fn s ->
s.character_id in user_char_ids
end)
existing = Enum.find(my_settings, &(&1.character_id == clicked_char_id))
{:ok, target_setting} =
if existing do
{:ok, existing}
else
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: clicked_char_id,
map_id: map_id,
tracked: true,
followed: true
})
end
# If the target_setting is already followed => unfollow it
if target_setting.followed do
{:ok, updated} = WandererApp.MapCharacterSettingsRepo.unfollow(target_setting)
else
# Only unfollow other rows from the current user
for s <- my_settings, s.id != target_setting.id, s.followed == true do
WandererApp.MapCharacterSettingsRepo.unfollow!(s)
end
# Ensure the new followed char is tracked
if not target_setting.tracked do
WandererApp.MapCharacterSettingsRepo.track!(target_setting)
char = target_setting |> Ash.load!(:character) |> Map.get(:character)
:ok = track_characters([char], map_id, true)
:ok = add_characters([char], map_id, true)
end
{:ok, updated} = WandererApp.MapCharacterSettingsRepo.follow(target_setting)
end
# re-fetch or re-map to confirm final results in UI
%{result: characters} = socket.assigns.characters
{:ok, tracked_characters} = get_tracked_map_characters(map_id, current_user)
user_eve_ids = Enum.map(tracked_characters, & &1.eve_id)
{:ok, final_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
updated_chars =
characters
|> Enum.map(fn c ->
s = Enum.find(final_settings, &(&1.character_id == c.id))
WandererApp.Maps.map_character(c, s)
end)
socket =
socket
|> assign(user_characters: user_eve_ids)
|> assign(has_tracked_characters?: has_tracked_characters?(user_eve_ids))
|> assign_async(:characters, fn ->
{:ok, %{characters: updated_chars}}
end)
|> MapEventHandler.push_map_event("init", %{user_characters: user_eve_ids, reset: false})
{:noreply, socket}
end
def handle_ui_event("hide_tracking", _, socket),
do: {:noreply, socket |> assign(show_tracking?: false)}

View File

@@ -162,6 +162,14 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
)
def handle_ui_event("toggle_follow_" <> character_id, _, socket),
do:
MapCharactersEventHandler.handle_ui_event(
"toggle_follow",
%{"character-id" => character_id},
socket
)
def handle_ui_event(
"get_user_settings",
_,

View File

@@ -21,32 +21,57 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|> MapEventHandler.push_map_event("remove_systems", solar_system_ids)
def handle_server_event(
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{assigns: %{current_user: current_user, map_user_settings: map_user_settings}} = socket
) do
is_user_character =
current_user.characters |> Enum.map(& &1.id) |> Enum.member?(character_id)
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{assigns: %{current_user: current_user, map_id: map_id, map_user_settings: map_user_settings}} = socket
) do
is_select_on_spash =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_user_character =
current_user.characters
|> Enum.map(& &1.id)
|> Enum.member?(character_id)
(is_user_character && is_select_on_spash)
|> case do
true ->
is_select_on_spash =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_followed =
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character_id) do
{:ok, setting} -> setting.followed == true
_ -> false
end
must_select? = is_user_character && (is_select_on_spash || is_followed)
if not must_select? do
socket
|> MapEventHandler.push_map_event("select_system", solar_system_id)
else
# Check if we already selected this exact system for this char:
last_selected =
WandererApp.Cache.lookup!(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
nil
)
false ->
socket
end
if last_selected == solar_system_id do
# same system => skip
socket
else
# new system => update cache + push event
WandererApp.Cache.put(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
solar_system_id
)
socket
|> MapEventHandler.push_map_event("select_system", solar_system_id)
end
end
end
def handle_server_event(%{event: :kills_updated, payload: kills}, socket) do

View File

@@ -24,6 +24,7 @@ defmodule WandererAppWeb.MapEventHandler do
@map_characters_ui_events [
"add_character",
"toggle_track",
"toggle_follow",
"hide_tracking"
]

View File

@@ -31,39 +31,20 @@
</.link>
</div>
<.modal
:if={assigns |> Map.get(:show_activity?, false)}
id="map-activity-modal"
title="Activity of Characters"
class="!w-[500px]"
show
on_cancel={JS.push("hide_activity")}
>
<.table
:if={not (assigns |> Map.get(:character_activity) |> is_nil())}
class="!max-h-[80vh] !overflow-y-auto"
id="activity-tbl"
rows={@character_activity.jumps}
>
<:col :let={activity} label="Character">
<.character_item character={activity.character} />
</:col>
<:col :let={activity} label="Passages">
<%= activity.count %>
</:col>
</.table>
</.modal>
<.modal
:if={assigns |> Map.get(:show_tracking?, false)}
id="map-tracking-modal"
title="Track Characters"
title="Track and Follow Characters"
show
on_cancel={JS.push("hide_tracking")}
>
<.async_result :let={characters} assign={@characters}>
<:loading><span class="loading loading-dots loading-xs" /></:loading>
<:failed :let={reason}><%= reason %></:failed>
<:loading>
<span class="loading loading-dots loading-xs" />
</:loading>
<:failed :let={reason}>
<%= reason %>
</:failed>
<.table
:if={characters}
@@ -72,7 +53,7 @@
rows={characters}
>
<:col :let={character} label="Track">
<label class="flex items-center gap-3">
<label class="flex items-center gap-2 justify-center">
<input
type="checkbox"
class="checkbox"
@@ -81,17 +62,34 @@
id={"character-track-#{character.id}"}
checked={character.tracked}
/>
<div class="flex items-center gap-3">
<.avatar url={member_icon_url(character.eve_id)} label={character.name} />
<div>
<div class="font-bold">
<%= character.name %><span class="ml-1 text-gray-400">[<%= character.corporation_ticker %>]</span>
</div>
<div class="text-sm opacity-50"></div>
</div>
</div>
</label>
</:col>
<:col :let={character} label="Follow">
<label class="flex items-center gap-2 justify-center">
<input
type="radio"
name="followed_character"
class="radio"
phx-click="toggle_follow"
phx-value-character-id={character.id}
checked={Map.get(character, :followed, false)}
/>
</label>
</:col>
<:col :let={character} label="Character">
<div class="flex items-center gap-3">
<.avatar url={member_icon_url(character.eve_id)} label={character.name} />
<div>
<div class="font-bold">
<%= character.name %>
<span class="ml-1 text-gray-400">
[<%= character.corporation_ticker %>]
</span>
</div>
<div class="text-sm opacity-50"></div>
</div>
</div>
</:col>
</.table>
</.async_result>
</.modal>

View File

@@ -203,7 +203,10 @@ defmodule WandererAppWeb.MapsLive do
socket.assigns.form,
form
|> Map.put("acls", form["acls"] || [])
|> Map.put("only_tracked_characters", form["only_tracked_characters"] || false)
|> Map.put(
"only_tracked_characters",
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
)
)
{:noreply, socket |> assign(form: form)}
@@ -593,7 +596,13 @@ defmodule WandererAppWeb.MapsLive do
scope -> scope
end
form = form |> Map.put("scope", scope)
form =
form
|> Map.put("scope", scope)
|> Map.put(
"only_tracked_characters",
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
)
map
|> WandererApp.Api.Map.update(form)

View File

@@ -2,7 +2,7 @@ defmodule WandererApp.MixProject do
use Mix.Project
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.32.1"
@version "1.32.3"
def project do
[

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.MigrateResources1 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:map_character_settings_v1) do
add :followed, :boolean, default: false
end
end
def down do
alter table(:map_character_settings_v1) do
remove :followed
end
end
end