mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
Implement "Everyone is friends" toggle: add global friends functionality with server settings and UI integration
This commit is contained in:
@@ -24,6 +24,7 @@ public class MessagesController(
|
|||||||
PrivateInstanceService privateInstances,
|
PrivateInstanceService privateInstances,
|
||||||
OnlinePresenceService onlinePresence,
|
OnlinePresenceService onlinePresence,
|
||||||
PlayerPresenceService presence,
|
PlayerPresenceService presence,
|
||||||
|
ServerSettingsService serverSettings,
|
||||||
ILogger<MessagesController> logger) : ControllerBase
|
ILogger<MessagesController> logger) : ControllerBase
|
||||||
{
|
{
|
||||||
private long Me => this.RequireCurrentPlayerId();
|
private long Me => this.RequireCurrentPlayerId();
|
||||||
@@ -534,11 +535,8 @@ public class MessagesController(
|
|||||||
[HttpGet("/api/messages/v1/onlineStatus")]
|
[HttpGet("/api/messages/v1/onlineStatus")]
|
||||||
public async Task<ActionResult> FavoriteFriendOnlineStatus()
|
public async Task<ActionResult> FavoriteFriendOnlineStatus()
|
||||||
{
|
{
|
||||||
var friendIds = await db.Relationships
|
// Honors the global-friends toggle (everyone is a friend when on).
|
||||||
.Where(r => r.Status == RelationshipStatus.Friend &&
|
var friendIds = await RelationshipQueries.EffectiveFriendIdsAsync(db, serverSettings, Me);
|
||||||
(r.RequesterId == Me || r.TargetId == Me))
|
|
||||||
.Select(r => r.RequesterId == Me ? r.TargetId : r.RequesterId)
|
|
||||||
.ToListAsync();
|
|
||||||
var online = onlinePresence.OnlinePlayerIds().ToHashSet();
|
var online = onlinePresence.OnlinePlayerIds().ToHashSet();
|
||||||
return Ok(friendIds.Select(id => new
|
return Ok(friendIds.Select(id => new
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace DorkNet.Server.Controllers.API.Relationships.V2;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class RelationshipsController(DorkNetDbContext db, NotificationService notifications) : ControllerBase
|
public class RelationshipsController(DorkNetDbContext db, NotificationService notifications, ServerSettingsService settings) : ControllerBase
|
||||||
{
|
{
|
||||||
// RelationshipType — int values match Relationship.RelationshipType
|
// RelationshipType — int values match Relationship.RelationshipType
|
||||||
// in the patched dump.cs.
|
// in the patched dump.cs.
|
||||||
@@ -72,6 +72,13 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
|
|||||||
public async Task<ActionResult> GetMyRelationships()
|
public async Task<ActionResult> GetMyRelationships()
|
||||||
{
|
{
|
||||||
var me = CurrentPlayerId;
|
var me = CurrentPlayerId;
|
||||||
|
|
||||||
|
// Global-friends mode (admin toggle): synthesize a Friend row for
|
||||||
|
// every other account WITHOUT persisting anything. Real rows are
|
||||||
|
// overlaid so mute/favorite still apply and Blocked stays non-friend.
|
||||||
|
if (await settings.IsGlobalFriendsEnabledAsync())
|
||||||
|
return Ok(await BuildGlobalFriendListAsync(me));
|
||||||
|
|
||||||
var rels = await db.Relationships
|
var rels = await db.Relationships
|
||||||
.Where(r => r.RequesterId == me || r.TargetId == me)
|
.Where(r => r.RequesterId == me || r.TargetId == me)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -79,6 +86,55 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
|
|||||||
return Ok(wire);
|
return Ok(wire);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Synthesize the caller's friend list as "everyone" for the
|
||||||
|
/// global-friends toggle. One Friend entry per other account; real rows
|
||||||
|
/// are overlaid so a Block stays None and mute/favorite carry through.</summary>
|
||||||
|
private async Task<List<Dictionary<string, object>>> BuildGlobalFriendListAsync(long me)
|
||||||
|
{
|
||||||
|
var realRows = await db.Relationships
|
||||||
|
.Where(r => r.RequesterId == me || r.TargetId == me)
|
||||||
|
.ToListAsync();
|
||||||
|
var realByOther = new Dictionary<long, RelationshipEntity>();
|
||||||
|
foreach (var r in realRows)
|
||||||
|
realByOther[r.RequesterId == me ? r.TargetId : r.RequesterId] = r;
|
||||||
|
|
||||||
|
var others = await db.Players
|
||||||
|
.Where(p => p.Id != me && p.Id != RelationshipQueries.SystemAccountId)
|
||||||
|
.Select(p => p.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var wire = new List<Dictionary<string, object>>(others.Count);
|
||||||
|
foreach (var otherId in others)
|
||||||
|
{
|
||||||
|
if (realByOther.TryGetValue(otherId, out var row))
|
||||||
|
{
|
||||||
|
// A real block wins — that pair is genuinely not friends.
|
||||||
|
if (row.Status == EntityStatus.Blocked) { wire.Add(EmptyRelationship(otherId)); continue; }
|
||||||
|
wire.Add(SynthFriend(otherId, row, me));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wire.Add(SynthFriend(otherId, null, me));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wire;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A synthetic Friend relationship for global-friends mode,
|
||||||
|
/// carrying mute/ignore/favorite from the caller's real row when present.</summary>
|
||||||
|
private static Dictionary<string, object> SynthFriend(long other, RelationshipEntity? real, long me)
|
||||||
|
{
|
||||||
|
var mineDir = real is not null && real.RequesterId == me;
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
["PlayerID"] = (int)other,
|
||||||
|
["RelationshipType"] = RT_Friend,
|
||||||
|
["Muted"] = mineDir && real!.Muted ? 1 : 0,
|
||||||
|
["Ignored"] = mineDir && real!.Ignored ? 1 : 0,
|
||||||
|
["Favorited"] = mineDir && real!.Favorited ? 1 : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>GET <c>api/relationships/v2/personaldetails/{playerId}</c>
|
/// <summary>GET <c>api/relationships/v2/personaldetails/{playerId}</c>
|
||||||
/// — single relationship row from the caller's perspective. Used
|
/// — single relationship row from the caller's perspective. Used
|
||||||
/// by the watch's profile-card view to determine whether to show
|
/// by the watch's profile-card view to determine whether to show
|
||||||
@@ -90,6 +146,14 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
|
|||||||
var me = CurrentPlayerId;
|
var me = CurrentPlayerId;
|
||||||
if (playerId <= 0 || playerId == me) return Ok(EmptyRelationship(playerId));
|
if (playerId <= 0 || playerId == me) return Ok(EmptyRelationship(playerId));
|
||||||
var row = await FindAsync(me, playerId);
|
var row = await FindAsync(me, playerId);
|
||||||
|
|
||||||
|
// Global-friends mode: report Friend for any real account that isn't
|
||||||
|
// the system account and isn't explicitly blocked.
|
||||||
|
if (playerId != RelationshipQueries.SystemAccountId
|
||||||
|
&& (row is null || row.Status != EntityStatus.Blocked)
|
||||||
|
&& await settings.IsGlobalFriendsEnabledAsync())
|
||||||
|
return Ok(SynthFriend(playerId, row, me));
|
||||||
|
|
||||||
return Ok(row is null ? EmptyRelationship(playerId) : BuildRelationship(row, me));
|
return Ok(row is null ? EmptyRelationship(playerId) : BuildRelationship(row, me));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1390,12 +1390,41 @@ public class AdminController(
|
|||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
row.SignupsDisabled,
|
row.SignupsDisabled,
|
||||||
|
row.GlobalFriendsEnabled,
|
||||||
row.WeeklyChallengesCompletedRequired,
|
row.WeeklyChallengesCompletedRequired,
|
||||||
row.UpdatedAt,
|
row.UpdatedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record SignupsToggleRequest(bool Disabled);
|
public sealed record SignupsToggleRequest(bool Disabled);
|
||||||
|
public sealed record GlobalFriendsToggleRequest(bool Enabled);
|
||||||
|
|
||||||
|
/// <summary>POST <c>api/admin/v1/settings/global-friends</c> — flip the
|
||||||
|
/// "everyone is friends" toggle. No relationship rows are written; the
|
||||||
|
/// friend graph is synthesized at read time (see
|
||||||
|
/// <see cref="RelationshipQueries"/>). After flipping, broadcast
|
||||||
|
/// <c>RelationshipsInvalid</c> to every connected watch so it re-fetches
|
||||||
|
/// <c>api/relationships/v2/get</c> and shows the new state live — no
|
||||||
|
/// relog (the 2020 watch handles that push via
|
||||||
|
/// <c>Relationships.RefreshList</c>).</summary>
|
||||||
|
[HttpPost("settings/global-friends")]
|
||||||
|
public async Task<ActionResult> SetGlobalFriends([FromBody] GlobalFriendsToggleRequest body)
|
||||||
|
{
|
||||||
|
var row = await serverSettings.SetGlobalFriendsEnabledAsync(body.Enabled);
|
||||||
|
await LogAsync(body.Enabled ? "global_friends_enabled" : "global_friends_disabled", "system", 0, "");
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Live refresh: every connected watch re-pulls its relationship list.
|
||||||
|
await notifications.BroadcastAsync(DorkNet.Models.Notification.PushNotificationId.RelationshipsInvalid);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
row.SignupsDisabled,
|
||||||
|
row.GlobalFriendsEnabled,
|
||||||
|
row.WeeklyChallengesCompletedRequired,
|
||||||
|
row.UpdatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>POST <c>api/admin/v1/settings/signups</c> — flip the
|
/// <summary>POST <c>api/admin/v1/settings/signups</c> — flip the
|
||||||
/// server-wide account-creation kill switch. While disabled, both
|
/// server-wide account-creation kill switch. While disabled, both
|
||||||
@@ -1412,6 +1441,7 @@ public class AdminController(
|
|||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
row.SignupsDisabled,
|
row.SignupsDisabled,
|
||||||
|
row.GlobalFriendsEnabled,
|
||||||
row.WeeklyChallengesCompletedRequired,
|
row.WeeklyChallengesCompletedRequired,
|
||||||
row.UpdatedAt,
|
row.UpdatedAt,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public class GoToController(
|
|||||||
DorkNet.Server.Data.DorkNetDbContext db,
|
DorkNet.Server.Data.DorkNetDbContext db,
|
||||||
NotificationService notifications,
|
NotificationService notifications,
|
||||||
JoinTimeoutService joinTimeouts,
|
JoinTimeoutService joinTimeouts,
|
||||||
|
ServerSettingsService serverSettings,
|
||||||
ILogger<GoToController> logger) : ControllerBase
|
ILogger<GoToController> logger) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpPost("/goto/room/{roomName}")]
|
[HttpPost("/goto/room/{roomName}")]
|
||||||
@@ -1005,11 +1006,10 @@ public class GoToController(
|
|||||||
|
|
||||||
private async Task NotifyFriendsOfMoveAsync(long playerId, RoomInstanceDto room)
|
private async Task NotifyFriendsOfMoveAsync(long playerId, RoomInstanceDto room)
|
||||||
{
|
{
|
||||||
var friendIds = await db.Relationships
|
// Honors the global-friends toggle: with it on, every other player is
|
||||||
.Where(r => r.Status == DorkNet.Server.Data.Entities.RelationshipStatus.Friend &&
|
// a "friend" and gets the presence ping (so the whole small server
|
||||||
(r.RequesterId == playerId || r.TargetId == playerId))
|
// sees each other's room moves).
|
||||||
.Select(r => r.RequesterId == playerId ? r.TargetId : r.RequesterId)
|
var friendIds = await RelationshipQueries.EffectiveFriendIdsAsync(db, serverSettings, playerId);
|
||||||
.ToListAsync();
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[goto-fanout] player={PlayerId} moved to room={RoomId}/{RoomName} → notifying {FriendCount} friend(s)",
|
"[goto-fanout] player={PlayerId} moved to room={RoomId}/{RoomName} → notifying {FriendCount} friend(s)",
|
||||||
playerId, room.RoomId, room.Name, friendIds.Count);
|
playerId, room.RoomId, room.Name, friendIds.Count);
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ public class ServerSettingsEntity
|
|||||||
/// brand-new account requests are blocked.</summary>
|
/// brand-new account requests are blocked.</summary>
|
||||||
public bool SignupsDisabled { get; set; }
|
public bool SignupsDisabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>When true, every account on the server is treated as a
|
||||||
|
/// friend of every other account — the relationships list, friend
|
||||||
|
/// online-status HUD, and room-move fan-out all behave as if everyone
|
||||||
|
/// is mutually friended. Built for small private servers where
|
||||||
|
/// searching + manually friend-requesting each other is friction; no
|
||||||
|
/// rows are written, so flipping it off instantly reverts to the real
|
||||||
|
/// relationship graph. Blocked relationships still suppress the pairing,
|
||||||
|
/// and the system/coach account (Id=1) is excluded.</summary>
|
||||||
|
public bool GlobalFriendsEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>When true, the challenge map tells the 2020 watch that
|
/// <summary>When true, the challenge map tells the 2020 watch that
|
||||||
/// weekly challenge completion is required for the weekly reward flow.
|
/// weekly challenge completion is required for the weekly reward flow.
|
||||||
/// Individual player challenge rows still default to incomplete until
|
/// Individual player challenge rows still default to incomplete until
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using DorkNet.Server.Data;
|
||||||
|
using DorkNet.Server.Data.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Effective-friend resolution that honors the
|
||||||
|
/// <see cref="ServerSettingsEntity.GlobalFriendsEnabled"/> toggle.
|
||||||
|
///
|
||||||
|
/// When the toggle is ON, every account on the server is treated as a
|
||||||
|
/// friend of every other account WITHOUT writing any relationship rows —
|
||||||
|
/// the friend graph is synthesized at read time. This keeps a small private
|
||||||
|
/// server frictionless (no searching + manual friend requests) and reverts
|
||||||
|
/// instantly when the toggle flips off, since nothing was persisted.
|
||||||
|
///
|
||||||
|
/// Blocked relationships still suppress the pairing, and the system/coach
|
||||||
|
/// account (<see cref="SystemAccountId"/>) is never offered as a friend.
|
||||||
|
///
|
||||||
|
/// Live propagation: flipping the toggle broadcasts
|
||||||
|
/// <c>PushNotificationId.RelationshipsInvalid</c>, which the 2020 watch
|
||||||
|
/// handles by calling <c>Relationships.RefreshList</c> — a full re-fetch of
|
||||||
|
/// <c>api/relationships/v2/get</c> — so connected players see the change
|
||||||
|
/// without relogging (verified against the readable March decompile:
|
||||||
|
/// <c>RecNet/Relationships.txt → OnRelationshipsInvalid → RefreshList</c>).
|
||||||
|
/// </summary>
|
||||||
|
public static class RelationshipQueries
|
||||||
|
{
|
||||||
|
/// <summary>Coach/system account seeded at startup (Player.Id = 1).
|
||||||
|
/// Excluded from the global-friends set so it never shows up as a
|
||||||
|
/// friend in anyone's list.</summary>
|
||||||
|
public const long SystemAccountId = 1;
|
||||||
|
|
||||||
|
/// <summary>The effective friend ids for <paramref name="me"/>. With the
|
||||||
|
/// global-friends toggle off this is the real Friend graph; with it on
|
||||||
|
/// it's every other (non-system, non-blocked) account.</summary>
|
||||||
|
public static async Task<List<long>> EffectiveFriendIdsAsync(
|
||||||
|
DorkNetDbContext db, ServerSettingsService settings, long me)
|
||||||
|
{
|
||||||
|
if (!await settings.IsGlobalFriendsEnabledAsync())
|
||||||
|
{
|
||||||
|
return await db.Relationships
|
||||||
|
.Where(r => r.Status == RelationshipStatus.Friend &&
|
||||||
|
(r.RequesterId == me || r.TargetId == me))
|
||||||
|
.Select(r => r.RequesterId == me ? r.TargetId : r.RequesterId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global friends: everyone except myself, the system account, and
|
||||||
|
// anyone in a Blocked relationship with me (either direction).
|
||||||
|
var blocked = await db.Relationships
|
||||||
|
.Where(r => r.Status == RelationshipStatus.Blocked &&
|
||||||
|
(r.RequesterId == me || r.TargetId == me))
|
||||||
|
.Select(r => r.RequesterId == me ? r.TargetId : r.RequesterId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return await db.Players
|
||||||
|
.Where(p => p.Id != me && p.Id != SystemAccountId && !blocked.Contains(p.Id))
|
||||||
|
.Select(p => p.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,26 @@ public class ServerSettingsService(DorkNetDbContext db)
|
|||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>When true, every account is treated as a friend of every
|
||||||
|
/// other account (synthesized at read time — no relationship rows are
|
||||||
|
/// written). See <see cref="ServerSettingsEntity.GlobalFriendsEnabled"/>
|
||||||
|
/// and <see cref="RelationshipQueries"/>.</summary>
|
||||||
|
public async Task<bool> IsGlobalFriendsEnabledAsync()
|
||||||
|
{
|
||||||
|
var row = await db.ServerSettings.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(s => s.Id == RowId);
|
||||||
|
return row?.GlobalFriendsEnabled ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServerSettingsEntity> SetGlobalFriendsEnabledAsync(bool enabled)
|
||||||
|
{
|
||||||
|
var existing = await GetTrackedRowAsync();
|
||||||
|
existing.GlobalFriendsEnabled = enabled;
|
||||||
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WeeklyChallengeSettings> GetWeeklyChallengesAsync()
|
public async Task<WeeklyChallengeSettings> GetWeeklyChallengesAsync()
|
||||||
{
|
{
|
||||||
var row = await GetAsync();
|
var row = await GetAsync();
|
||||||
|
|||||||
@@ -248,6 +248,8 @@ public static class DatabaseBootstrap
|
|||||||
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""RecCenterDoorsJson"" text NOT NULL DEFAULT '';");
|
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""RecCenterDoorsJson"" text NOT NULL DEFAULT '';");
|
||||||
await db.Database.ExecuteSqlRawAsync(
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""DiscoveredGameConfigsJson"" text NOT NULL DEFAULT '';");
|
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""DiscoveredGameConfigsJson"" text NOT NULL DEFAULT '';");
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""GlobalFriendsEnabled"" boolean NOT NULL DEFAULT false;");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +288,18 @@ public static class DatabaseBootstrap
|
|||||||
// ignore the duplicate-column error; any other schema problem
|
// ignore the duplicate-column error; any other schema problem
|
||||||
// will surface when EF reads ServerSettings.
|
// will surface when EF reads ServerSettings.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
@"ALTER TABLE ""ServerSettings"" ADD COLUMN ""GlobalFriendsEnabled"" INTEGER NOT NULL DEFAULT 0;");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// SQLite has no ADD COLUMN IF NOT EXISTS. If it already exists,
|
||||||
|
// ignore the duplicate-column error; any other schema problem
|
||||||
|
// will surface when EF reads ServerSettings.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>SQLite migration-history reconciliation. Handles two
|
/// <summary>SQLite migration-history reconciliation. Handles two
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Plus, RefreshCw, Trash } from '../components/Icons';
|
|||||||
|
|
||||||
interface ServerSettings {
|
interface ServerSettings {
|
||||||
signupsDisabled: boolean;
|
signupsDisabled: boolean;
|
||||||
|
globalFriendsEnabled: boolean;
|
||||||
weeklyChallengesCompletedRequired: boolean;
|
weeklyChallengesCompletedRequired: boolean;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -190,6 +191,30 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleGlobalFriends = async () => {
|
||||||
|
if (!settings) return;
|
||||||
|
const next = !settings.globalFriendsEnabled;
|
||||||
|
const verb = next ? 'make everyone friends' : 'turn off all-friends';
|
||||||
|
if (!confirm(
|
||||||
|
next
|
||||||
|
? 'Make every account friends with every other account? No friend rows are written — it\'s synthesized live, and connected players refresh instantly. Toggle off to revert.'
|
||||||
|
: `Really ${verb}? Players will revert to their real friends list.`,
|
||||||
|
)) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const updated = await api<ServerSettings>('/settings/global-friends', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { Enabled: next },
|
||||||
|
});
|
||||||
|
setSettings(updated);
|
||||||
|
toast.push(next ? 'Everyone is now friends — players refreshing live.' : 'All-friends turned off.', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
toast.push((e as Error).message, 'error');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateWeeklyChallenge = (index: number, patch: Partial<WeeklyChallenge>) => {
|
const updateWeeklyChallenge = (index: number, patch: Partial<WeeklyChallenge>) => {
|
||||||
setWeekly((current) => {
|
setWeekly((current) => {
|
||||||
if (!current) return current;
|
if (!current) return current;
|
||||||
@@ -428,6 +453,38 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{settings && (
|
||||||
|
<div className="card !p-5 max-w-2xl mt-5">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h2 className="text-sm font-semibold text-ink-50">Everyone is friends</h2>
|
||||||
|
<p className="mt-1 text-xs text-ink-400">
|
||||||
|
When on, every account is treated as a friend of every other account, so players
|
||||||
|
don't have to search and friend-request each other on a small server. Nothing is
|
||||||
|
written to the database — it's synthesized live, and connected players get a refresh
|
||||||
|
signal so their friends list updates without relogging. Turn it off to instantly
|
||||||
|
revert to real friend lists. Blocks are still respected.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[11px] text-ink-500">
|
||||||
|
Last changed {new Date(settings.updatedAt).toLocaleString()}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={toggleGlobalFriends}
|
||||||
|
disabled={busy}
|
||||||
|
className={(settings.globalFriendsEnabled ? 'btn-danger' : 'btn-primary') + ' text-xs shrink-0'}
|
||||||
|
>
|
||||||
|
{busy ? 'Working…' : settings.globalFriendsEnabled ? 'Turn off' : 'Make everyone friends'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-xs">
|
||||||
|
{settings.globalFriendsEnabled
|
||||||
|
? <span className="badge-online">Everyone is friends</span>
|
||||||
|
: <span className="badge-neutral">Normal friend lists</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{gameConfigs && (
|
{gameConfigs && (
|
||||||
<DiscoveredGameConfigsPanel
|
<DiscoveredGameConfigsPanel
|
||||||
settings={gameConfigs}
|
settings={gameConfigs}
|
||||||
|
|||||||
@@ -176,6 +176,21 @@ hand it to the player, and they redeem it on the site's **`/join`** page —
|
|||||||
which creates their account bound to the device their game client
|
which creates their account bound to the device their game client
|
||||||
reported, so the next launch logs straight in.
|
reported, so the next launch logs straight in.
|
||||||
|
|
||||||
|
### Everyone-is-friends toggle
|
||||||
|
|
||||||
|
For small servers where searching + friend-requesting each other is
|
||||||
|
friction, admin **Settings → Server → "Everyone is friends"** makes every
|
||||||
|
account a friend of every other account. It writes **no** relationship
|
||||||
|
rows — the friend graph is synthesized at read time
|
||||||
|
(`RelationshipQueries.EffectiveFriendIdsAsync`, gated on
|
||||||
|
`ServerSettings.GlobalFriendsEnabled`), so flipping it off reverts
|
||||||
|
instantly. Blocks are still honored and the system/coach account is
|
||||||
|
excluded. Flipping it broadcasts `RelationshipsInvalid` to every connected
|
||||||
|
watch, which calls `Relationships.RefreshList` and re-pulls
|
||||||
|
`api/relationships/v2/get` — so players see everyone appear (or disappear)
|
||||||
|
**without relogging**. It covers the friends list, the friends-online HUD,
|
||||||
|
and room-move presence fan-out.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
Reference in New Issue
Block a user