mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-13 10:58:59 +00:00
✨ feat: add demo mode anonymization
This commit is contained in:
@@ -14,6 +14,7 @@ const DEFAULT_PREFERENCES: AppPreferences = {
|
||||
accent: null,
|
||||
locale: DEFAULT_LOCALE,
|
||||
showDebugNav: false,
|
||||
demoMode: false,
|
||||
};
|
||||
const DEFAULTS: StoredConfig = {
|
||||
gamePath: null,
|
||||
@@ -30,6 +31,7 @@ function normalizePreferences(raw: Partial<AppPreferences> | undefined): AppPref
|
||||
accent: accent && /^#[\da-f]{6}$/i.test(accent) ? accent : null,
|
||||
locale: isAppLocale(locale) ? locale : DEFAULT_LOCALE,
|
||||
showDebugNav: raw?.showDebugNav === true,
|
||||
demoMode: raw?.demoMode === true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Avatar, StatusDot } from "../../components/ui";
|
||||
import { useSelf } from "../../store/social";
|
||||
import { statusMeta } from "../../lib/vrchat";
|
||||
import { api } from "../../lib/api";
|
||||
import { useDemoMode } from "../../lib/debugSettings";
|
||||
import { demoName } from "../../lib/demoMode";
|
||||
import type { UserStatus } from "../../../../shared/types/user";
|
||||
|
||||
const STATUS_CHOICES: UserStatus[] = ["join me", "active", "ask me", "busy"];
|
||||
@@ -15,12 +17,16 @@ const MENU_ROW =
|
||||
export function AccountSwitcher() {
|
||||
const { accounts, switchAccount, removeAccount, beginAddAccount, logout } = useAuth();
|
||||
const self = useSelf();
|
||||
const demoMode = useDemoMode();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [showAccounts, setShowAccounts] = useState(false);
|
||||
|
||||
const active = accounts.accounts.find((a) => a.id === accounts.activeId);
|
||||
const others = accounts.accounts.filter((a) => a.id !== accounts.activeId);
|
||||
const status = self?.status ?? "offline";
|
||||
const activeName = active ? (demoMode ? demoName(active.id) : active.displayName) : "Account";
|
||||
const activeIcon = demoMode ? undefined : active?.userIcon;
|
||||
const activeSub = demoMode ? statusMeta[status].label : self?.statusDescription || statusMeta[status].label;
|
||||
|
||||
function close() {
|
||||
setOpen(false);
|
||||
@@ -39,7 +45,7 @@ export function AccountSwitcher() {
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className="acct__current relative shrink-0 leading-[0]">
|
||||
<Avatar src={active?.userIcon} name={active?.displayName} size={32} />
|
||||
<Avatar src={activeIcon} name={activeName} size={32} />
|
||||
<StatusDot
|
||||
color={statusMeta[status].color}
|
||||
ring="var(--surface)"
|
||||
@@ -49,10 +55,10 @@ export function AccountSwitcher() {
|
||||
</span>
|
||||
<span className="acct__current-text flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate text-left text-[13.5px] font-semibold leading-[1.25]">
|
||||
{active?.displayName ?? "Account"}
|
||||
{activeName}
|
||||
</span>
|
||||
<span className="truncate text-left text-[11px] font-medium text-faint">
|
||||
{self?.statusDescription || statusMeta[status].label}
|
||||
{activeSub}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
@@ -73,7 +79,9 @@ export function AccountSwitcher() {
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-px">
|
||||
{others.map((a) => (
|
||||
{others.map((a) => {
|
||||
const name = demoMode ? demoName(a.id) : a.displayName;
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
className="group/row flex items-center rounded-sm transition-colors hover:bg-surface-2"
|
||||
@@ -85,19 +93,20 @@ export function AccountSwitcher() {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<Avatar src={a.userIcon} name={a.displayName} size={28} />
|
||||
<span className="truncate text-[13px] font-semibold">{a.displayName}</span>
|
||||
<Avatar src={demoMode ? undefined : a.userIcon} name={name} size={28} />
|
||||
<span className="truncate text-[13px] font-semibold">{name}</span>
|
||||
</button>
|
||||
<button
|
||||
className="grid w-[30px] shrink-0 self-stretch place-items-center rounded-sm text-faint opacity-0 transition-[opacity,color,background] duration-[var(--dur)] ease-[var(--ease)] hover:bg-[color-mix(in_srgb,var(--danger)_12%,transparent)] hover:text-danger group-hover/row:opacity-100"
|
||||
title="Remove account"
|
||||
aria-label={`Remove ${a.displayName}`}
|
||||
aria-label={`Remove ${name}`}
|
||||
onClick={() => void removeAccount(a.id)}
|
||||
>
|
||||
<X size={15} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useFriends, useSelf } from "../../store/social";
|
||||
import { useWorldName } from "../../store/worlds";
|
||||
import { parseLocation, type UserProfile } from "../../../../shared/types/user";
|
||||
import { useT } from "../../lib/i18n";
|
||||
import { useDemoMode } from "../../lib/debugSettings";
|
||||
import { anonymizeUserForDemo } from "../../lib/demoMode";
|
||||
import { useUserMenu } from "./useUserMenu";
|
||||
import { useFriendsGrouped } from "./useFriendsGrouped";
|
||||
|
||||
@@ -19,6 +21,7 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
||||
const t = useT();
|
||||
const friends = useFriends();
|
||||
const self = useSelf();
|
||||
const demoMode = useDemoMode();
|
||||
const selfId = self?.id;
|
||||
|
||||
const { online, offline, instances, alone } = useFriendsGrouped(friends, self);
|
||||
@@ -40,8 +43,8 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
||||
const openContextMenu = useCallback((e: React.MouseEvent, friend: UserProfile) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, friend });
|
||||
}, []);
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, friend: demoMode ? anonymizeUserForDemo(friend) : friend });
|
||||
}, [demoMode]);
|
||||
|
||||
return (
|
||||
<aside className="friendsbar flex h-full flex-col overflow-hidden border-l border-border bg-surface">
|
||||
@@ -53,7 +56,7 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{selfAlone ? <FriendRow friend={self} onOpen={onOpen} isSelf selfChrome /> : null}
|
||||
{selfAlone ? <FriendRow friend={self} onOpen={onOpen} isSelf selfChrome demoMode={demoMode} /> : null}
|
||||
|
||||
{friends.length === 0 ? (
|
||||
<p className="p-3 text-[13px] text-faint">{t("nav:friends.noFriendsOnline")}</p>
|
||||
@@ -74,6 +77,7 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
||||
onOpen={onOpen}
|
||||
hideLocation
|
||||
isSelf={f.id === selfId}
|
||||
demoMode={demoMode}
|
||||
onContextMenu={f.id !== selfId ? openContextMenu : undefined}
|
||||
/>
|
||||
))}
|
||||
@@ -92,6 +96,7 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
||||
key={f.id}
|
||||
friend={f}
|
||||
onOpen={onOpen}
|
||||
demoMode={demoMode}
|
||||
onContextMenu={openContextMenu}
|
||||
/>
|
||||
))}
|
||||
@@ -111,6 +116,7 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
||||
friend={f}
|
||||
onOpen={onOpen}
|
||||
dim
|
||||
demoMode={demoMode}
|
||||
onContextMenu={openContextMenu}
|
||||
/>
|
||||
))}
|
||||
@@ -199,6 +205,7 @@ function FriendRow({
|
||||
selfChrome,
|
||||
hideLocation,
|
||||
onContextMenu,
|
||||
demoMode,
|
||||
}: {
|
||||
friend: UserProfile;
|
||||
onOpen: (id: string) => void;
|
||||
@@ -207,20 +214,22 @@ function FriendRow({
|
||||
selfChrome?: boolean;
|
||||
hideLocation?: boolean;
|
||||
onContextMenu?: (e: React.MouseEvent, friend: UserProfile) => void;
|
||||
demoMode?: boolean;
|
||||
}) {
|
||||
const t = useT();
|
||||
const presence = presenceOf({ ...friend, isSelf });
|
||||
const sub = friend.statusDescription || presence.effective.label;
|
||||
const parsed = parseLocation(friend.location);
|
||||
const displayFriend = demoMode ? anonymizeUserForDemo(friend) : friend;
|
||||
const presence = presenceOf({ ...displayFriend, isSelf });
|
||||
const sub = displayFriend.statusDescription || presence.effective.label;
|
||||
const parsed = parseLocation(displayFriend.location);
|
||||
const worldName = useWorldName(parsed?.worldId);
|
||||
const location = parsed
|
||||
? `${worldName ? `in ${worldName}` : t("nav:friends.inAWorld")} (#${parsed.instanceId})`
|
||||
: locationLabel(friend.location);
|
||||
: locationLabel(displayFriend.location);
|
||||
return (
|
||||
<button
|
||||
onClick={() => onOpen(isSelf ? "me" : friend.id)}
|
||||
onContextMenu={onContextMenu ? (e) => onContextMenu(e, friend) : undefined}
|
||||
title={friend.displayName}
|
||||
title={displayFriend.displayName}
|
||||
className={[
|
||||
"friend-row flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-surface-2",
|
||||
dim ? "opacity-55 hover:opacity-100" : "",
|
||||
@@ -229,13 +238,13 @@ function FriendRow({
|
||||
: "",
|
||||
].join(" ")}
|
||||
>
|
||||
<PresenceAvatar user={friend} size={32} />
|
||||
<PresenceAvatar user={displayFriend} size={32} />
|
||||
|
||||
<span className="friend-row__text flex min-w-0 flex-col">
|
||||
<span
|
||||
className={`flex min-w-0 items-center gap-1.5 text-[13.5px] ${isSelf ? "font-semibold" : "font-medium"}`}
|
||||
>
|
||||
<span className="truncate">{friend.displayName}</span>
|
||||
<span className="truncate">{displayFriend.displayName}</span>
|
||||
{isSelf ? <Badge tone="accent">You</Badge> : null}
|
||||
</span>
|
||||
<span className="truncate text-[12px] text-muted">{sub}</span>
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
trustMeta,
|
||||
} from "../../lib/vrchat";
|
||||
import { useProfile } from "./useProfile";
|
||||
import { useDemoMode } from "../../lib/debugSettings";
|
||||
import { anonymizeUserForDemo } from "../../lib/demoMode";
|
||||
import { WorldsSection, FavoriteWorldsSection, WorldSearch } from "./WorldsSection";
|
||||
import { GroupsSection } from "./GroupsSection";
|
||||
import { LocationSection } from "./LocationSection";
|
||||
@@ -48,22 +50,24 @@ export function ProfileView({ target }: { target: "me" | string }) {
|
||||
|
||||
function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
const t = useT();
|
||||
const demoMode = useDemoMode();
|
||||
const displayProfile = demoMode ? anonymizeUserForDemo(profile) : profile;
|
||||
const [tab, setTab] = useViewState<TabId>(`profile:${profile.id}:tab`, "overview");
|
||||
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
const canAdd = !profile.isSelf && !profile.isFriend;
|
||||
const addFriend = useAddFriend(profile.id);
|
||||
const { buildItems, modal } = useUserMenu();
|
||||
const trust = trustMeta[profile.trustRank];
|
||||
const presence = presenceOf(profile);
|
||||
const trust = trustMeta[displayProfile.trustRank];
|
||||
const presence = presenceOf(displayProfile);
|
||||
const statusLabel =
|
||||
!presence.online && profile.status !== "offline"
|
||||
!presence.online && displayProfile.status !== "offline"
|
||||
? t("profile:status.offlineWas", { status: presence.status.label })
|
||||
: presence.effective.label;
|
||||
const avatar = avatarOf(profile);
|
||||
const banner = bannerOf(profile);
|
||||
const devLabel = profile.developerType ? developerLabels[profile.developerType] : undefined;
|
||||
const bioLinks = (profile.bioLinks ?? []).filter(Boolean);
|
||||
const showcasedBadges = (profile.badges ?? []).filter((b) => b.showcased);
|
||||
const avatar = avatarOf(displayProfile);
|
||||
const banner = bannerOf(displayProfile);
|
||||
const devLabel = displayProfile.developerType ? developerLabels[displayProfile.developerType] : undefined;
|
||||
const bioLinks = (displayProfile.bioLinks ?? []).filter(Boolean);
|
||||
const showcasedBadges = (displayProfile.badges ?? []).filter((b) => b.showcased);
|
||||
|
||||
return (
|
||||
<HeroHeader
|
||||
@@ -72,7 +76,7 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
overlap={-72}
|
||||
media={
|
||||
<div className="profile__avatar" style={{ "--ring": trust.color } as React.CSSProperties}>
|
||||
<Avatar src={avatar} name={profile.displayName} size={116} />
|
||||
<Avatar src={avatar} name={displayProfile.displayName} size={116} />
|
||||
<span
|
||||
className="profile__status-dot"
|
||||
style={{ background: presence.color }}
|
||||
@@ -83,21 +87,21 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
body={
|
||||
<div className="min-w-0 flex-1 pb-2">
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<h2 className="text-[32px] font-bold tracking-[-0.6px]">{profile.displayName}</h2>
|
||||
<h2 className="text-[32px] font-bold tracking-[-0.6px]">{displayProfile.displayName}</h2>
|
||||
{profile.isSelf ? <Tag color="var(--accent)">{t("profile:badge.you")}</Tag> : null}
|
||||
{profile.isFriend && !profile.isSelf ? (
|
||||
<Tag color="var(--status-join)">{t("profile:badge.friend")}</Tag>
|
||||
) : null}
|
||||
{devLabel ? <Tag color="var(--accent)">{devLabel}</Tag> : null}
|
||||
{profile.ageVerified ? (
|
||||
{displayProfile.ageVerified ? (
|
||||
<Tag color="var(--trust-trusted)">{t("profile:badge.ageVerified")}</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2.5 flex flex-wrap items-center gap-3">
|
||||
<Tag color={trust.color}>{trust.label}</Tag>
|
||||
<PresenceLabel label={statusLabel} color={presence.color} />
|
||||
{profile.pronouns ? (
|
||||
<span className="text-[13px] text-muted">{profile.pronouns}</span>
|
||||
{displayProfile.pronouns ? (
|
||||
<span className="text-[13px] text-muted">{displayProfile.pronouns}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{showcasedBadges.length ? (
|
||||
@@ -135,7 +139,7 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<LocationSection location={profile.location} />
|
||||
<LocationSection location={displayProfile.location} />
|
||||
|
||||
<Tabs
|
||||
tabs={[
|
||||
@@ -150,22 +154,22 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
|
||||
{tab === "overview" ? (
|
||||
<div className="flex flex-col gap-5 rise-in">
|
||||
{profile.note ? (
|
||||
{displayProfile.note ? (
|
||||
<Section title={t("profile:sections.note")}>
|
||||
<p className="text-[14px] leading-relaxed whitespace-pre-wrap text-text">
|
||||
{profile.note}
|
||||
{displayProfile.note}
|
||||
</p>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{profile.statusDescription || profile.bio || bioLinks.length ? (
|
||||
{displayProfile.statusDescription || displayProfile.bio || bioLinks.length ? (
|
||||
<Section title={t("profile:sections.about")}>
|
||||
{profile.statusDescription ? (
|
||||
<p className="text-[15px] italic text-text">“{profile.statusDescription}”</p>
|
||||
{displayProfile.statusDescription ? (
|
||||
<p className="text-[15px] italic text-text">“{displayProfile.statusDescription}”</p>
|
||||
) : null}
|
||||
{profile.bio ? (
|
||||
{displayProfile.bio ? (
|
||||
<p className="text-[14px] leading-relaxed whitespace-pre-wrap text-muted">
|
||||
{profile.bio}
|
||||
{displayProfile.bio}
|
||||
</p>
|
||||
) : null}
|
||||
{bioLinks.length ? (
|
||||
@@ -183,61 +187,61 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
<div className="grid grid-cols-1 items-start gap-5 lg:grid-cols-2">
|
||||
<Section title={t("profile:sections.details")}>
|
||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3">
|
||||
{profile.dateJoined ? (
|
||||
{displayProfile.dateJoined ? (
|
||||
<Fact
|
||||
label={t("profile:facts.joined")}
|
||||
value={formatDate(profile.dateJoined)}
|
||||
value={formatDate(displayProfile.dateJoined)}
|
||||
/>
|
||||
) : null}
|
||||
{profile.lastLogin ? (
|
||||
{displayProfile.lastLogin ? (
|
||||
<Fact
|
||||
label={t("profile:facts.lastLogin")}
|
||||
value={formatDateTime(profile.lastLogin)}
|
||||
value={formatDateTime(displayProfile.lastLogin)}
|
||||
/>
|
||||
) : null}
|
||||
{profile.lastActivity ? (
|
||||
{displayProfile.lastActivity ? (
|
||||
<Fact
|
||||
label={t("profile:facts.lastActivity")}
|
||||
value={formatDateTime(profile.lastActivity)}
|
||||
value={formatDateTime(displayProfile.lastActivity)}
|
||||
/>
|
||||
) : null}
|
||||
{profile.lastPlatform ? (
|
||||
{displayProfile.lastPlatform ? (
|
||||
<Fact
|
||||
label={t("profile:facts.platform")}
|
||||
value={platformLabel(profile.lastPlatform)}
|
||||
value={platformLabel(displayProfile.lastPlatform)}
|
||||
/>
|
||||
) : null}
|
||||
{profile.state ? (
|
||||
<Fact label={t("profile:facts.state")} value={stateLabel(profile.state, t)} />
|
||||
{displayProfile.state ? (
|
||||
<Fact label={t("profile:facts.state")} value={stateLabel(displayProfile.state, t)} />
|
||||
) : null}
|
||||
<Fact label={t("profile:facts.userId")} value={profile.id} mono />
|
||||
{demoMode ? null : <Fact label={t("profile:facts.userId")} value={profile.id} mono />}
|
||||
</dl>
|
||||
</Section>
|
||||
|
||||
{profile.languages?.length ? (
|
||||
{displayProfile.languages?.length ? (
|
||||
<Section title={t("profile:sections.languages")}>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{profile.languages.map((code) => (
|
||||
{displayProfile.languages.map((code) => (
|
||||
<Tag key={code}>{languageLabel(code)}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{profile.currentAvatarTags?.length ? (
|
||||
{displayProfile.currentAvatarTags?.length ? (
|
||||
<Section title={t("profile:sections.avatarTags")}>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{profile.currentAvatarTags.map((t) => (
|
||||
{displayProfile.currentAvatarTags.map((t) => (
|
||||
<Tag key={t}>{prettyTag(t, "content_")}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{profile.badges?.length ? (
|
||||
{displayProfile.badges?.length ? (
|
||||
<Section title={t("profile:sections.badges")}>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{[...profile.badges]
|
||||
{[...displayProfile.badges]
|
||||
.sort((a, b) => Number(b.showcased) - Number(a.showcased))
|
||||
.map((b) => (
|
||||
<div
|
||||
@@ -260,10 +264,10 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{profile.pastDisplayNames?.length ? (
|
||||
{displayProfile.pastDisplayNames?.length ? (
|
||||
<Section title={t("profile:sections.formerNames")} collapsible>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{profile.pastDisplayNames.map((p) => (
|
||||
{displayProfile.pastDisplayNames.map((p) => (
|
||||
<span
|
||||
key={`${p.displayName}-${p.updatedAt}`}
|
||||
className="rounded-lg border border-border bg-surface px-2.5 py-1 text-[13px] text-text"
|
||||
@@ -309,7 +313,7 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
<ContextMenu
|
||||
x={menu.x}
|
||||
y={menu.y}
|
||||
items={buildItems(profile)}
|
||||
items={buildItems(displayProfile)}
|
||||
onClose={() => setMenu(null)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -27,7 +27,12 @@ import type { AppConfig, PreferredRegion, RegionPing } from "../../../../shared/
|
||||
import type { UnityStatus } from "../../../../shared/types/unity";
|
||||
import { api, errorMessage } from "../../lib/api";
|
||||
import { useAsync } from "../../lib/useAsync";
|
||||
import { useSetDebugNavVisible, useDebugNavVisible } from "../../lib/debugSettings";
|
||||
import {
|
||||
useDemoMode,
|
||||
useSetDemoMode,
|
||||
useSetDebugNavVisible,
|
||||
useDebugNavVisible,
|
||||
} from "../../lib/debugSettings";
|
||||
import { useAppConfig } from "../../lib/AppConfigContext";
|
||||
import { regionFlag, regionLabel } from "../../lib/vrchat";
|
||||
import {
|
||||
@@ -107,6 +112,8 @@ function DebugSection() {
|
||||
const { t } = useI18n();
|
||||
const showDebugNav = useDebugNavVisible();
|
||||
const setDebugNavVisible = useSetDebugNavVisible();
|
||||
const demoMode = useDemoMode();
|
||||
const setDemoMode = useSetDemoMode();
|
||||
return (
|
||||
<Section
|
||||
title={t("settings:debug.title")}
|
||||
@@ -120,6 +127,13 @@ function DebugSection() {
|
||||
</div>
|
||||
<Toggle checked={showDebugNav} onChange={setDebugNavVisible} />
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between gap-4 rounded-lg border border-border bg-surface-2 p-3">
|
||||
<div>
|
||||
<div className="text-[13px] font-semibold text-text">{t("settings:debug.demoMode")}</div>
|
||||
<div className="mt-0.5 text-[12px] text-muted">{t("settings:debug.demoModeHint")}</div>
|
||||
</div>
|
||||
<Toggle checked={demoMode} onChange={setDemoMode} />
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ export function useDebugNavVisible(): boolean {
|
||||
return useAppConfig().config?.preferences.showDebugNav ?? false;
|
||||
}
|
||||
|
||||
export function useDemoMode(): boolean {
|
||||
return useAppConfig().config?.preferences.demoMode ?? false;
|
||||
}
|
||||
|
||||
export function useSetDebugNavVisible(): (visible: boolean) => void {
|
||||
const { setPreferences } = useAppConfig();
|
||||
return useCallback(
|
||||
@@ -14,3 +18,13 @@ export function useSetDebugNavVisible(): (visible: boolean) => void {
|
||||
[setPreferences],
|
||||
);
|
||||
}
|
||||
|
||||
export function useSetDemoMode(): (enabled: boolean) => void {
|
||||
const { setPreferences } = useAppConfig();
|
||||
return useCallback(
|
||||
(enabled: boolean) => {
|
||||
void setPreferences({ demoMode: enabled });
|
||||
},
|
||||
[setPreferences],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { UserProfile } from "../../../shared/types/user";
|
||||
|
||||
const GIVEN = [
|
||||
"Aoi",
|
||||
"Hana",
|
||||
"Haruka",
|
||||
"Hikari",
|
||||
"Kaede",
|
||||
"Koharu",
|
||||
"Mika",
|
||||
"Natsuki",
|
||||
"Ren",
|
||||
"Rin",
|
||||
"Sora",
|
||||
"Yui",
|
||||
];
|
||||
const FAMILY = [
|
||||
"Amamiya",
|
||||
"Fujimoto",
|
||||
"Hoshino",
|
||||
"Kisaragi",
|
||||
"Kobayashi",
|
||||
"Minazuki",
|
||||
"Mizuno",
|
||||
"Sakuraba",
|
||||
"Shirakawa",
|
||||
"Tachibana",
|
||||
"Tsukino",
|
||||
"Yamabuki",
|
||||
];
|
||||
|
||||
function hashId(id: string): number {
|
||||
let hash = 2166136261;
|
||||
for (let i = 0; i < id.length; i++) {
|
||||
hash ^= id.charCodeAt(i);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
export function demoName(id: string): string {
|
||||
const hash = hashId(id);
|
||||
return `${FAMILY[hash % FAMILY.length]} ${GIVEN[(hash >>> 4) % GIVEN.length]}`;
|
||||
}
|
||||
|
||||
export function anonymizeUserForDemo(user: UserProfile): UserProfile {
|
||||
return {
|
||||
...user,
|
||||
displayName: demoName(user.id),
|
||||
bio: "",
|
||||
bioLinks: [],
|
||||
statusDescription: "",
|
||||
userIcon: "",
|
||||
profilePicOverride: "",
|
||||
profilePicOverrideThumbnail: "",
|
||||
currentAvatarImageUrl: "",
|
||||
currentAvatarThumbnailImageUrl: "",
|
||||
location: user.location === "offline" ? "offline" : "private",
|
||||
note: undefined,
|
||||
pronouns: undefined,
|
||||
pastDisplayNames: [],
|
||||
badges: [],
|
||||
};
|
||||
}
|
||||
@@ -88,7 +88,9 @@
|
||||
"title": "Debug",
|
||||
"description": "Developer and troubleshooting options.",
|
||||
"sidebar": "Show Debug in the sidebar",
|
||||
"sidebarHint": "Adds the Debug shortcut back to the left navigation."
|
||||
"sidebarHint": "Adds the Debug shortcut back to the left navigation.",
|
||||
"demoMode": "Demo mode",
|
||||
"demoModeHint": "Anonymize user-facing details for screenshots."
|
||||
},
|
||||
"region": {
|
||||
"title": "Instance Region",
|
||||
|
||||
@@ -88,7 +88,9 @@
|
||||
"title": "デバッグ",
|
||||
"description": "開発者向けとトラブルシューティング用の設定です。",
|
||||
"sidebar": "サイドバーにデバッグを表示",
|
||||
"sidebarHint": "左のナビゲーションにデバッグのショートカットを表示します。"
|
||||
"sidebarHint": "左のナビゲーションにデバッグのショートカットを表示します。",
|
||||
"demoMode": "デモモード",
|
||||
"demoModeHint": "スクリーンショット用にユーザー情報を匿名化します。"
|
||||
},
|
||||
"region": {
|
||||
"title": "インスタンスのリージョン",
|
||||
|
||||
@@ -88,7 +88,9 @@
|
||||
"title": "ดีบัก",
|
||||
"description": "ตัวเลือกสำหรับนักพัฒนาและการแก้ปัญหา",
|
||||
"sidebar": "แสดงดีบักในแถบด้านข้าง",
|
||||
"sidebarHint": "เพิ่มทางลัดดีบักกลับไปที่เมนูนำทางด้านซ้าย"
|
||||
"sidebarHint": "เพิ่มทางลัดดีบักกลับไปที่เมนูนำทางด้านซ้าย",
|
||||
"demoMode": "โหมดเดโม",
|
||||
"demoModeHint": "ซ่อนข้อมูลผู้ใช้สำหรับภาพหน้าจอ"
|
||||
},
|
||||
"region": {
|
||||
"title": "ภูมิภาคของอินสแตนซ์",
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface AppPreferences {
|
||||
accent: string | null;
|
||||
locale: AppLocale;
|
||||
showDebugNav: boolean;
|
||||
demoMode: boolean;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
|
||||
Reference in New Issue
Block a user