mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-13 10:58:59 +00:00
♻️ refactor: extract useFriendsGrouped, rename useAction, dedupe friend count
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { ContentFilterKey } from "../../../../../shared/types/settings";
|
import type { ContentFilterKey } from "../../../../../shared/types/settings";
|
||||||
import { api } from "../../../lib/api";
|
import { api } from "../../../lib/api";
|
||||||
import { useI18n } from "../../../lib/i18n";
|
import { useI18n } from "../../../lib/i18n";
|
||||||
import { Notice, Section, ToggleRow, useAsync, type SectionProps } from "../ui";
|
import { Notice, Section, ToggleRow, useAction, type SectionProps } from "../ui";
|
||||||
|
|
||||||
const FILTER_ORDER: ContentFilterKey[] = [
|
const FILTER_ORDER: ContentFilterKey[] = [
|
||||||
"content_sex",
|
"content_sex",
|
||||||
@@ -13,7 +13,7 @@ const FILTER_ORDER: ContentFilterKey[] = [
|
|||||||
|
|
||||||
export function ContentGatingSection({ settings, onChange }: SectionProps) {
|
export function ContentGatingSection({ settings, onChange }: SectionProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { busy, error, run } = useAsync();
|
const { busy, error, run } = useAction();
|
||||||
const active = new Set(settings.contentFilters);
|
const active = new Set(settings.contentFilters);
|
||||||
|
|
||||||
function toggle(key: ContentFilterKey, on: boolean) {
|
function toggle(key: ContentFilterKey, on: boolean) {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { Trans } from "react-i18next";
|
|||||||
import { api } from "../../../lib/api";
|
import { api } from "../../../lib/api";
|
||||||
import { useI18n } from "../../../lib/i18n";
|
import { useI18n } from "../../../lib/i18n";
|
||||||
import { Button, Field } from "../../../components/ui";
|
import { Button, Field } from "../../../components/ui";
|
||||||
import { Notice, Section, useAsync, type SectionProps } from "../ui";
|
import { Notice, Section, useAction, type SectionProps } from "../ui";
|
||||||
|
|
||||||
export function DangerZoneSection({ settings, onChange }: SectionProps) {
|
export function DangerZoneSection({ settings, onChange }: SectionProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { busy, error, run } = useAsync();
|
const { busy, error, run } = useAction();
|
||||||
const [confirmText, setConfirmText] = useState("");
|
const [confirmText, setConfirmText] = useState("");
|
||||||
const canDelete = confirmText.trim().toUpperCase() === "DELETE";
|
const canDelete = confirmText.trim().toUpperCase() === "DELETE";
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
addDays,
|
addDays,
|
||||||
daysSince,
|
daysSince,
|
||||||
lastChangedLabel,
|
lastChangedLabel,
|
||||||
useAsync,
|
useAction,
|
||||||
type SectionProps,
|
type SectionProps,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export function DisplayNameSection({ settings, onChange }: SectionProps) {
|
|||||||
const [name, setName] = useState(settings.displayName);
|
const [name, setName] = useState(settings.displayName);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [confirmRevert, setConfirmRevert] = useState(false);
|
const [confirmRevert, setConfirmRevert] = useState(false);
|
||||||
const { busy, error, ok, run } = useAsync();
|
const { busy, error, ok, run } = useAction();
|
||||||
|
|
||||||
const cooldownDays = settings.supporter ? 30 : 90;
|
const cooldownDays = settings.supporter ? 30 : 90;
|
||||||
const changedDaysAgo = daysSince(settings.displayNameChangedAt);
|
const changedDaysAgo = daysSince(settings.displayNameChangedAt);
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { useState } from "react";
|
|||||||
import { api } from "../../../lib/api";
|
import { api } from "../../../lib/api";
|
||||||
import { useI18n } from "../../../lib/i18n";
|
import { useI18n } from "../../../lib/i18n";
|
||||||
import { Button, Field } from "../../../components/ui";
|
import { Button, Field } from "../../../components/ui";
|
||||||
import { Notice, Section, useAsync, type SectionProps } from "../ui";
|
import { Notice, Section, useAction, type SectionProps } from "../ui";
|
||||||
|
|
||||||
export function EmailSection({ settings, onChange }: SectionProps) {
|
export function EmailSection({ settings, onChange }: SectionProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const { busy, error, ok, run } = useAsync();
|
const { busy, error, ok, run } = useAction();
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
await run(api.settings.email(email.trim(), password), {
|
await run(api.settings.email(email.trim(), password), {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
Notice,
|
Notice,
|
||||||
Section,
|
Section,
|
||||||
WEBSITE_ACCOUNT,
|
WEBSITE_ACCOUNT,
|
||||||
useAsync,
|
useAction,
|
||||||
type SectionProps,
|
type SectionProps,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export function PasswordSection({ settings, onChange }: SectionProps) {
|
|||||||
const [current, setCurrent] = useState("");
|
const [current, setCurrent] = useState("");
|
||||||
const [next, setNext] = useState("");
|
const [next, setNext] = useState("");
|
||||||
const [confirm, setConfirm] = useState("");
|
const [confirm, setConfirm] = useState("");
|
||||||
const { busy, error, ok, run } = useAsync();
|
const { busy, error, ok, run } = useAction();
|
||||||
const mismatch = confirm.length > 0 && next !== confirm;
|
const mismatch = confirm.length > 0 && next !== confirm;
|
||||||
const valid = current.length > 0 && next.length >= 8 && next === confirm;
|
const valid = current.length > 0 && next.length >= 8 && next === confirm;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { api } from "../../../lib/api";
|
import { api } from "../../../lib/api";
|
||||||
import { useI18n } from "../../../lib/i18n";
|
import { useI18n } from "../../../lib/i18n";
|
||||||
import { Notice, Section, ToggleRow, useAsync, type SectionProps } from "../ui";
|
import { Notice, Section, ToggleRow, useAction, type SectionProps } from "../ui";
|
||||||
|
|
||||||
export function PrivacySection({ settings, onChange }: SectionProps) {
|
export function PrivacySection({ settings, onChange }: SectionProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { busy, error, run } = useAsync();
|
const { busy, error, run } = useAction();
|
||||||
|
|
||||||
function setShared(show: boolean) {
|
function setShared(show: boolean) {
|
||||||
void run(api.settings.privacy({ sharedConnectionsHidden: !show }), { onOk: onChange });
|
void run(api.settings.privacy({ sharedConnectionsHidden: !show }), { onOk: onChange });
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useI18n } from "../../../lib/i18n";
|
|||||||
import { Badge, Button, Field, Modal } from "../../../components/ui";
|
import { Badge, Button, Field, Modal } from "../../../components/ui";
|
||||||
import { useStepUp } from "../../auth/useStepUp";
|
import { useStepUp } from "../../auth/useStepUp";
|
||||||
import { TwoFactorPrompt } from "../../auth/TwoFactorPrompt";
|
import { TwoFactorPrompt } from "../../auth/TwoFactorPrompt";
|
||||||
import { Notice, Section, useAsync, type SectionProps } from "../ui";
|
import { Notice, Section, useAction, type SectionProps } from "../ui";
|
||||||
|
|
||||||
export function TwoFactorSection({ settings, onChange }: SectionProps) {
|
export function TwoFactorSection({ settings, onChange }: SectionProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -16,8 +16,8 @@ export function TwoFactorSection({ settings, onChange }: SectionProps) {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [codes, setCodes] = useState<RecoveryCode[] | null>(null);
|
const [codes, setCodes] = useState<RecoveryCode[] | null>(null);
|
||||||
const setup = useAsync();
|
const setup = useAction();
|
||||||
const verify = useAsync();
|
const verify = useAction();
|
||||||
const [confirmDisable, setConfirmDisable] = useState(false);
|
const [confirmDisable, setConfirmDisable] = useState(false);
|
||||||
const [disabledOk, setDisabledOk] = useState(false);
|
const [disabledOk, setDisabledOk] = useState(false);
|
||||||
const stepUp = useStepUp();
|
const stepUp = useStepUp();
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { useState } from "react";
|
|||||||
import { api } from "../../../lib/api";
|
import { api } from "../../../lib/api";
|
||||||
import { useI18n } from "../../../lib/i18n";
|
import { useI18n } from "../../../lib/i18n";
|
||||||
import { Button } from "../../../components/ui";
|
import { Button } from "../../../components/ui";
|
||||||
import { Notice, Section, useAsync } from "../ui";
|
import { Notice, Section, useAction } from "../ui";
|
||||||
|
|
||||||
export function UserDataSection() {
|
export function UserDataSection() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { busy, error, ok, run } = useAsync();
|
const { busy, error, ok, run } = useAction();
|
||||||
const [armed, setArmed] = useState(false);
|
const [armed, setArmed] = useState(false);
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function Section({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAsync() {
|
export function useAction() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSocial } from "../../store/social";
|
import { useFriends } from "../../store/social";
|
||||||
import { useWorlds } from "../../store/worlds";
|
import { useWorlds } from "../../store/worlds";
|
||||||
import { useGroups } from "../../store/groups";
|
import { useGroups } from "../../store/groups";
|
||||||
import { useCopied, useDebug } from "./useDebug";
|
import { useCopied, useDebug } from "./useDebug";
|
||||||
@@ -19,7 +19,7 @@ export function DebugPanel() {
|
|||||||
const { cache, stats, logs, ws, repoStats, invalidate, clear, clearLogs, clearWs } = useDebug();
|
const { cache, stats, logs, ws, repoStats, invalidate, clear, clearLogs, clearWs } = useDebug();
|
||||||
const [tab, setTab] = useState<Tab>("cache");
|
const [tab, setTab] = useState<Tab>("cache");
|
||||||
|
|
||||||
const friendCount = useSocial((s) => Object.values(s.users).filter((u) => u.isFriend).length);
|
const friendCount = useFriends().length;
|
||||||
const worldCount = useWorlds((s) => Object.keys(s.worlds).length);
|
const worldCount = useWorlds((s) => Object.keys(s.worlds).length);
|
||||||
const groupCount = useGroups((s) => Object.keys(s.groups).length);
|
const groupCount = useGroups((s) => Object.keys(s.groups).length);
|
||||||
const repoCount = repoStats.reduce((sum, r) => sum + r.count, 0);
|
const repoCount = repoStats.reduce((sum, r) => sum + r.count, 0);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState, useCallback, type ReactNode } from "react";
|
import { useState, useCallback, type ReactNode } from "react";
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import { isOnline, locationLabel, presenceOf } from "../../lib/vrchat";
|
import { isOnline, locationLabel, presenceOf } from "../../lib/vrchat";
|
||||||
import { Badge, PresenceAvatar, ContextMenu } from "../../components/ui";
|
import { Badge, PresenceAvatar, ContextMenu } from "../../components/ui";
|
||||||
@@ -7,13 +7,7 @@ import { useWorldName } from "../../store/worlds";
|
|||||||
import { parseLocation, type UserProfile } from "../../../../shared/types/user";
|
import { parseLocation, type UserProfile } from "../../../../shared/types/user";
|
||||||
import { useT } from "../../lib/i18n";
|
import { useT } from "../../lib/i18n";
|
||||||
import { useUserMenu } from "./useUserMenu";
|
import { useUserMenu } from "./useUserMenu";
|
||||||
|
import { useFriendsGrouped } from "./useFriendsGrouped";
|
||||||
interface InstanceSection {
|
|
||||||
key: string;
|
|
||||||
worldId: string;
|
|
||||||
instanceId: string;
|
|
||||||
members: UserProfile[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextMenuState {
|
interface ContextMenuState {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -27,55 +21,7 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
|||||||
const self = useSelf();
|
const self = useSelf();
|
||||||
const selfId = self?.id;
|
const selfId = self?.id;
|
||||||
|
|
||||||
const sorted = useMemo(
|
const { online, offline, instances, alone } = useFriendsGrouped(friends, self);
|
||||||
() =>
|
|
||||||
[...friends].sort(
|
|
||||||
(a, b) =>
|
|
||||||
Number(isOnline(b)) - Number(isOnline(a)) || a.displayName.localeCompare(b.displayName),
|
|
||||||
),
|
|
||||||
[friends],
|
|
||||||
);
|
|
||||||
|
|
||||||
const online = useMemo(() => sorted.filter(isOnline), [sorted]);
|
|
||||||
const offline = useMemo(() => sorted.filter((f) => !isOnline(f)), [sorted]);
|
|
||||||
|
|
||||||
const { instances, alone } = useMemo(() => {
|
|
||||||
const byInstance = new Map<string, UserProfile[]>();
|
|
||||||
const alone: UserProfile[] = [];
|
|
||||||
const people = self && isOnline(self) ? [self, ...online] : online;
|
|
||||||
for (const f of people) {
|
|
||||||
const parsed = parseLocation(f.location);
|
|
||||||
if (!parsed) {
|
|
||||||
if (f.id !== selfId) alone.push(f);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const key = `${parsed.worldId}:${parsed.instanceId}`;
|
|
||||||
const list = byInstance.get(key);
|
|
||||||
if (list) list.push(f);
|
|
||||||
else byInstance.set(key, [f]);
|
|
||||||
}
|
|
||||||
const instances: InstanceSection[] = [];
|
|
||||||
for (const [key, members] of byInstance) {
|
|
||||||
if (members.length < 2) {
|
|
||||||
if (members[0].id !== selfId) alone.push(members[0]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
members.sort(
|
|
||||||
(a, b) =>
|
|
||||||
Number(b.id === selfId) - Number(a.id === selfId) ||
|
|
||||||
a.displayName.localeCompare(b.displayName),
|
|
||||||
);
|
|
||||||
const [worldId, instanceId] = key.split(":");
|
|
||||||
instances.push({ key, worldId, instanceId, members });
|
|
||||||
}
|
|
||||||
instances.sort(
|
|
||||||
(a, b) =>
|
|
||||||
Number(b.members.some((m) => m.id === selfId)) -
|
|
||||||
Number(a.members.some((m) => m.id === selfId)) || b.members.length - a.members.length,
|
|
||||||
);
|
|
||||||
alone.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
||||||
return { instances, alone };
|
|
||||||
}, [online, self, selfId]);
|
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set(["offline"]));
|
const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set(["offline"]));
|
||||||
const toggle = (id: string) =>
|
const toggle = (id: string) =>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { parseLocation, type UserProfile } from "../../../../shared/types/user";
|
||||||
|
import { isOnline } from "../../lib/vrchat";
|
||||||
|
|
||||||
|
export interface InstanceSection {
|
||||||
|
key: string;
|
||||||
|
worldId: string;
|
||||||
|
instanceId: string;
|
||||||
|
members: UserProfile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FriendsGrouped {
|
||||||
|
online: UserProfile[];
|
||||||
|
offline: UserProfile[];
|
||||||
|
instances: InstanceSection[];
|
||||||
|
alone: UserProfile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// groups online friends (plus self) by shared instance; everyone in an instance
|
||||||
|
// of one stays in `alone`. self is pinned first within each instance.
|
||||||
|
export function useFriendsGrouped(
|
||||||
|
friends: UserProfile[],
|
||||||
|
self: UserProfile | undefined,
|
||||||
|
): FriendsGrouped {
|
||||||
|
const selfId = self?.id;
|
||||||
|
|
||||||
|
const sorted = useMemo(
|
||||||
|
() =>
|
||||||
|
[...friends].sort(
|
||||||
|
(a, b) =>
|
||||||
|
Number(isOnline(b)) - Number(isOnline(a)) || a.displayName.localeCompare(b.displayName),
|
||||||
|
),
|
||||||
|
[friends],
|
||||||
|
);
|
||||||
|
|
||||||
|
const online = useMemo(() => sorted.filter(isOnline), [sorted]);
|
||||||
|
const offline = useMemo(() => sorted.filter((f) => !isOnline(f)), [sorted]);
|
||||||
|
|
||||||
|
const { instances, alone } = useMemo(() => {
|
||||||
|
const byInstance = new Map<string, UserProfile[]>();
|
||||||
|
const alone: UserProfile[] = [];
|
||||||
|
const people = self && isOnline(self) ? [self, ...online] : online;
|
||||||
|
for (const f of people) {
|
||||||
|
const parsed = parseLocation(f.location);
|
||||||
|
if (!parsed) {
|
||||||
|
if (f.id !== selfId) alone.push(f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = `${parsed.worldId}:${parsed.instanceId}`;
|
||||||
|
const list = byInstance.get(key);
|
||||||
|
if (list) list.push(f);
|
||||||
|
else byInstance.set(key, [f]);
|
||||||
|
}
|
||||||
|
const instances: InstanceSection[] = [];
|
||||||
|
for (const [key, members] of byInstance) {
|
||||||
|
if (members.length < 2) {
|
||||||
|
if (members[0].id !== selfId) alone.push(members[0]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
members.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Number(b.id === selfId) - Number(a.id === selfId) ||
|
||||||
|
a.displayName.localeCompare(b.displayName),
|
||||||
|
);
|
||||||
|
const [worldId, instanceId] = key.split(":");
|
||||||
|
instances.push({ key, worldId, instanceId, members });
|
||||||
|
}
|
||||||
|
instances.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Number(b.members.some((m) => m.id === selfId)) -
|
||||||
|
Number(a.members.some((m) => m.id === selfId)) || b.members.length - a.members.length,
|
||||||
|
);
|
||||||
|
alone.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||||
|
return { instances, alone };
|
||||||
|
}, [online, self, selfId]);
|
||||||
|
|
||||||
|
return { online, offline, instances, alone };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user