Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/components/hosts/HostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default function HostCard({
if (!isTeamVault || pinSource === "none" || pinSource === "personal") return undefined;
const updatedBy = (connection as { updated_by?: string }).updated_by;
const member = updatedBy ? teamMembers.find((m) => m.user_id === updatedBy) : undefined;
return member?.display_name ?? t("hosts.card.teamMemberFallback");
return member?.handle ?? t("hosts.card.teamMemberFallback");
})();
const handlePinClick = () => {
if (!isTeamVault) {
Expand Down Expand Up @@ -119,12 +119,12 @@ export default function HostCard({
const presence = useConnectionPresence(connection);
const presenceTitle = presence
? presence.overflow > 0
? t("hosts.card.inUseByOverflow", { name: presence.primary.displayName, count: presence.overflow })
: t("hosts.card.inUseBy", { name: presence.primary.displayName })
? t("hosts.card.inUseByOverflow", { name: presence.primary.handle, count: presence.overflow })
: t("hosts.card.inUseBy", { name: presence.primary.handle })
: "";
const presenceAvatar = presence && (
<span className="flex items-center" title={presenceTitle}>
<MiniAvatar name={presence.primary.displayName} size={18} />
<MiniAvatar name={presence.primary.handle} size={18} />
{presence.overflow > 0 && (
<span className="ml-1 text-[10px] font-semibold px-1 rounded-full bg-(--t-bg-elevated) text-(--t-text-dim)">
+{presence.overflow}
Expand Down
19 changes: 8 additions & 11 deletions src/components/hosts/TeamSessions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface TeamState {
activeSessions: unknown[];
fetchActiveSessions: ReturnType<typeof vi.fn>;
joinSession: ReturnType<typeof vi.fn>;
connections: Record<string, { multiplayerSessionId: string; participants?: { display_name: string }[] }>;
connections: Record<string, { multiplayerSessionId: string; participants?: { handle: string }[] }>;
}
interface SessionState {
sessions: unknown[];
Expand Down Expand Up @@ -65,25 +65,23 @@ const h = vi.hoisted(() => {
uiState,
useUIStore,
getMyUserId: vi.fn(async () => "me" as string | null),
getCurrentUserEmail: vi.fn(async () => "me@x" as string | null),
accessibleVaultIds: vi.fn(() => ["team-1"] as string[]),
};
});

vi.mock("@/services/teamService", () => ({ getMyUserId: () => h.getMyUserId() }));
vi.mock("@/services/account", () => ({ getCurrentUserEmail: () => h.getCurrentUserEmail() }));
vi.mock("@/hooks/useAccessibleVaultIds", () => ({ useAccessibleVaultIds: () => h.accessibleVaultIds() }));
vi.mock("@/stores/teamSessionStore", () => ({ useTeamSessionStore: h.useTeamSessionStore }));
vi.mock("@/stores/sessionStore", () => ({ useSessionStore: h.useSessionStore }));
vi.mock("@/stores/uiStore", () => ({ useUIStore: h.useUIStore }));

const { teamState, sessionState, uiState, getMyUserId, getCurrentUserEmail, accessibleVaultIds } = h;
const { teamState, sessionState, uiState, getMyUserId, accessibleVaultIds } = h;

import { TeamSessions } from "./TeamSessions";

const active = (o: Partial<{
id: string; connection_name: string; host_user_id: string;
participant_count: number; participants: { user_id: string; display_name: string }[]; vault_ids: string[];
participant_count: number; participants: { user_id: string; handle: string }[]; vault_ids: string[];
}> = {}) => ({
id: o.id ?? "sess-1",
connection_name: o.connection_name ?? "Prod DB",
Expand All @@ -107,7 +105,6 @@ beforeEach(() => {
uiState.homeView = true;
accessibleVaultIds.mockReturnValue(["team-1"]);
getMyUserId.mockReset().mockResolvedValue("me");
getCurrentUserEmail.mockReset().mockResolvedValue("me@x");
});
afterEach(() => cleanup());

Expand Down Expand Up @@ -184,7 +181,7 @@ test("valid code calls joinSession with sessionId + token", async () => {
fireEvent.change(input, { target: { value: "sess-9:tok-9" } });
fireEvent.click(screen.getByText("hosts.teamSessions.join"));
await waitFor(() =>
expect(teamState.joinSession).toHaveBeenCalledWith("sess-9", expect.any(String), expect.any(Function), "tok-9"),
expect(teamState.joinSession).toHaveBeenCalledWith("sess-9", expect.any(Function), "tok-9"),
);
});

Expand Down Expand Up @@ -234,12 +231,12 @@ test("renders exactly one join affordance and an empty-state hint when no sessio
test("participant list prefers live WS connection participants over server participants", () => {
uiState.homeView = true;
teamState.activeSessions = [
active({ id: "s1", participants: [{ user_id: "u1", display_name: "ServerName" }] }),
active({ id: "s1", participants: [{ user_id: "u1", handle: "ServerName" }] }),
];
teamState.connections = {
"local-1": {
multiplayerSessionId: "s1",
participants: [{ display_name: "LiveA" }, { display_name: "LiveB" }],
participants: [{ handle: "LiveA" }, { handle: "LiveB" }],
},
};
render(<TeamSessions />);
Expand All @@ -252,8 +249,8 @@ test("falls back to server participants when not in the session", () => {
active({
id: "s1",
participants: [
{ user_id: "u1", display_name: "A" },
{ user_id: "u2", display_name: "B" },
{ user_id: "u1", handle: "A" },
{ user_id: "u2", handle: "B" },
],
}),
];
Expand Down
5 changes: 1 addition & 4 deletions src/components/hosts/TeamSessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Icon } from "@iconify/react";
import { useTeamSessionStore } from "@/stores/teamSessionStore";
import { useSessionStore } from "@/stores/sessionStore";
import { useTeamSessionStore as useMpStore } from "@/stores/teamSessionStore";
import { getCurrentUserEmail } from "@/services/account";
import { getMyUserId } from "@/services/teamService";
import { useUIStore } from "@/stores/uiStore";
import { useAccessibleVaultIds } from "@/hooks/useAccessibleVaultIds";
Expand Down Expand Up @@ -82,10 +81,8 @@ export function TeamSessions() {
);

const doJoinSession = async (sessionId: string, inviteToken?: string) => {
const displayName = (await getCurrentUserEmail()) ?? t("hosts.teamSessions.meFallback");
await joinTeamSessionAndOpenTab({
sessionId,
displayName,
// Session not found (not yet loaded) collapses to the same redacted state as a null name.
connectionName: sessionDisplayName({
connection_name: activeSessions.find((a) => a.id === sessionId)?.connection_name ?? null,
Expand Down Expand Up @@ -239,7 +236,7 @@ export function TeamSessions() {
? useMpStore.getState().connections[liveLocalId]?.participants
: undefined;
const participants = (liveParticipants ?? session.participants)?.map((p) => ({
name: p.display_name,
name: p.handle,
}));

return (
Expand Down
12 changes: 3 additions & 9 deletions src/components/layout/SidebarAccountButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import { useUIStore } from "@/stores/uiStore";
import { useThemeStore } from "@/stores/themeStore";
import { useRipple } from "@/hooks/useRipple";
import { getAccountMode, getMe, lockVaultSession, logout } from "@/services/account";
import { getAccountMode, getMyHandle, lockVaultSession, logout } from "@/services/account";
import { getSavedAccounts, saveCurrentAccount, switchToAccount, removeSavedAccount, type SavedAccount } from "@/services/savedAccounts";
import { DropdownMenuItem } from "@/components/shared/DropdownMenuItem";
import { useCopyHandle } from "@/hooks/useCopyHandle";
Expand All @@ -28,21 +28,15 @@ export function SidebarAccountButton() {

const refreshAccountInfo = async () => {
const { invoke: inv } = await import("@tauri-apps/api/core");
const [mode, email, accountId, handle] = await Promise.all([
const [mode, email, accountId] = await Promise.all([
getAccountMode().catch(() => null),
inv<string | null>("keychain_get", { key: "email" }).catch(() => null),
inv<string | null>("keychain_get", { key: "account_id" }).catch(() => null),
// Cached by getMe(); an account that signed in before handles existed has
// none yet, so fall back to the server rather than hiding the row forever.
inv<string | null>("keychain_get", { key: "handle" }).catch(() => null),
]);
setAccountMode(mode);
setAccountEmail(email);
setCurrentAccountId(accountId);
setAccountHandle(handle);
if (!handle && mode === "server") {
getMe().then((me) => setAccountHandle(me?.handle ?? null)).catch(() => {});
}
void getMyHandle().then((handle) => setAccountHandle(handle || null)).catch(() => {});
};

useEffect(() => { refreshAccountInfo(); }, []);
Expand Down
8 changes: 4 additions & 4 deletions src/components/layout/VaultHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function OnlineMembersStack({ members, roles, onInviteClick }: { members: TeamMe
{visible.map((m, i) => (
<div
key={m.user_id}
title={m.display_name}
title={m.handle}
style={{
marginLeft: i === 0 ? 0 : -9,
zIndex: MAX_STACK - i,
Expand All @@ -60,7 +60,7 @@ function OnlineMembersStack({ members, roles, onInviteClick }: { members: TeamMe
transition: "border-color 0.2s, opacity 0.2s",
}}
>
<MiniAvatar name={m.display_name} size={24} />
<MiniAvatar name={m.handle} size={24} />
</div>
))}
{overflow > 0 && (
Expand Down Expand Up @@ -98,11 +98,11 @@ function OnlineMembersStack({ members, roles, onInviteClick }: { members: TeamMe
return (
<div key={m.user_id} className="flex items-center gap-2.5 px-3 py-2" style={{ opacity: m.is_online ? 1 : 0.5 }}>
<div className="relative shrink-0">
<MiniAvatar name={m.display_name} size={22} />
<MiniAvatar name={m.handle} size={22} />
{m.is_online && <StatusDot color="var(--t-status-connected)" size={7} />}
</div>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-xs truncate" style={{ color: "var(--t-text-primary)" }}>{m.display_name}</span>
<span className="text-xs truncate" style={{ color: "var(--t-text-primary)" }}>{m.handle}</span>
{memberRoles.length > 0 && (
<div className="flex items-center gap-1 flex-wrap mt-0.5">
{memberRoles.map((r) => {
Expand Down
12 changes: 6 additions & 6 deletions src/components/members/MembersPage.BulkActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ContextMenuItem } from "@/components/shared/ContextMenu";

const h = vi.hoisted(() => ({
getMyUserId: vi.fn(),
getMyEmail: vi.fn(),
getMyHandle: vi.fn(),
loadTeams: vi.fn(),
loadMembers: vi.fn(),
loadRoles: vi.fn(),
Expand All @@ -20,9 +20,9 @@ const h = vi.hoisted(() => ({
{ id: "r-ed", team_id: "t1", name: "editor", is_builtin: false, permissions: 0, position: 2, created_at: "" },
],
members: [
{ team_id: "t1", user_id: "me", invited_by_display_name: null, joined_at: "2024-01-01T00:00:00Z", display_name: "Me", public_key: "pk", role_ids: ["r-mem"] },
{ team_id: "t1", user_id: "u1", invited_by_display_name: null, joined_at: "2024-01-02T00:00:00Z", display_name: "Ann", public_key: "pk", role_ids: ["r-mem", "r-ed"] },
{ team_id: "t1", user_id: "u2", invited_by_display_name: null, joined_at: "2024-01-03T00:00:00Z", display_name: "Bob", public_key: "pk", role_ids: ["r-mem"] },
{ team_id: "t1", user_id: "me", invited_by_display_name: null, joined_at: "2024-01-01T00:00:00Z", handle: "merry-quartz-2597", public_key: "pk", role_ids: ["r-mem"] },
{ team_id: "t1", user_id: "u1", invited_by_display_name: null, joined_at: "2024-01-02T00:00:00Z", handle: "amber-lynx-4410", public_key: "pk", role_ids: ["r-mem", "r-ed"] },
{ team_id: "t1", user_id: "u2", invited_by_display_name: null, joined_at: "2024-01-03T00:00:00Z", handle: "brisk-otter-8823", public_key: "pk", role_ids: ["r-mem"] },
],
}));

Expand Down Expand Up @@ -97,10 +97,10 @@ vi.mock("@/hooks/usePermission", () => ({
vi.mock("@/services/teamService", () => ({
searchUsers: vi.fn(),
getMyUserId: h.getMyUserId,
getMyEmail: h.getMyEmail,
inviteByEmail: vi.fn(),
revokePendingInvitation: vi.fn(),
}));
vi.mock("@/services/account", () => ({ getMyHandle: h.getMyHandle }));
vi.mock("@/services/teamActionFeedback", () => ({
runTeamAction: async (o: { run: () => Promise<unknown> }) => o.run(),
}));
Expand Down Expand Up @@ -186,7 +186,7 @@ import MembersPage from "./MembersPage";
beforeEach(() => {
Object.values(h).forEach((v) => { if (typeof v === "function" && "mockReset" in v) (v as ReturnType<typeof vi.fn>).mockReset(); });
h.getMyUserId.mockResolvedValue("me");
h.getMyEmail.mockResolvedValue("me@x.com");
h.getMyHandle.mockResolvedValue("merry-quartz-2597");
h.loadTeams.mockResolvedValue(undefined);
h.loadMembers.mockResolvedValue(undefined);
h.loadRoles.mockResolvedValue(undefined);
Expand Down
23 changes: 12 additions & 11 deletions src/components/members/MembersPage.InvitePanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const h = vi.hoisted(() => ({
add: vi.fn(),
assign: vi.fn(),
reload: vi.fn(),
getMyHandle: vi.fn(async () => "merry-quartz-2597"),
usedSeats: 2,
totalSeats: 3,
}));
Expand All @@ -26,10 +27,10 @@ vi.mock("@/components/shared/Panel", () => ({
vi.mock("@/services/teamService", () => ({
searchUsers: h.searchUsers,
getMyUserId: vi.fn(),
getMyEmail: vi.fn(),
inviteByEmail: h.inviteByEmail,
revokePendingInvitation: vi.fn(),
}));
vi.mock("@/services/account", () => ({ getMyHandle: h.getMyHandle }));
vi.mock("@/services/teamActionFeedback", () => ({
runTeamAction: async (o: { run: () => Promise<unknown> }) => o.run(),
}));
Expand Down Expand Up @@ -78,7 +79,7 @@ const baseProps = {
onMemberAdded: vi.fn(),
};

const inA = { user_id: "inA", display_name: "Included A", public_key: "pkA" };
const inA = { user_id: "inA", handle: "included-alpha-3140", public_key: "pkA" };

beforeEach(() => {
h.searchUsers.mockReset();
Expand Down Expand Up @@ -133,14 +134,14 @@ test("existingIds filter: excluded id absent from rendered results, included id
vi.useFakeTimers();
h.searchUsers.mockResolvedValue([
inA,
{ user_id: "inB", display_name: "Excluded B", public_key: "pkB" },
{ user_id: "inB", handle: "excluded-bravo-9022", public_key: "pkB" },
]);
render(<InvitePanel {...baseProps} existingIds={new Set(["inB"])} />);

await typeAndDebounce("in");

expect(screen.getByText("Included A")).toBeTruthy();
expect(screen.queryByText("Excluded B")).toBeNull();
expect(screen.getByText("included-alpha-3140")).toBeTruthy();
expect(screen.queryByText("excluded-bravo-9022")).toBeNull();
});

test("add success (not at limit): addMemberById + assignMemberRole(default role) + reload + onMemberAdded", async () => {
Expand All @@ -153,7 +154,7 @@ test("add success (not at limit): addMemberById + assignMemberRole(default role)
await typeAndDebounce("in");
vi.useRealTimers();
h.reload.mockClear();
fireEvent.click(screen.getByText("Included A"));
fireEvent.click(screen.getByText("included-alpha-3140"));

await waitFor(() => expect(baseProps.onMemberAdded).toHaveBeenCalled());
expect(h.add).toHaveBeenCalledWith("t1", "inA");
Expand All @@ -170,7 +171,7 @@ test("add at seat limit: addMemberById NOT called, BuySeatsModal shown with that

await typeAndDebounce("in");
vi.useRealTimers();
fireEvent.click(screen.getByText("Included A"));
fireEvent.click(screen.getByText("included-alpha-3140"));

expect(h.add).not.toHaveBeenCalled();
const modal = await screen.findByTestId("buy-seats-modal");
Expand All @@ -185,7 +186,7 @@ test("add rejects {code:402} (not at limit): BuySeatsModal shown, no error text"

await typeAndDebounce("in");
vi.useRealTimers();
fireEvent.click(screen.getByText("Included A"));
fireEvent.click(screen.getByText("included-alpha-3140"));

const modal = await screen.findByTestId("buy-seats-modal");
expect(modal.dataset.pendingUser).toBe("inA");
Expand All @@ -200,7 +201,7 @@ test("add rejects Error with '402' in message (no code prop): BuySeatsModal show

await typeAndDebounce("in");
vi.useRealTimers();
fireEvent.click(screen.getByText("Included A"));
fireEvent.click(screen.getByText("included-alpha-3140"));

const modal = await screen.findByTestId("buy-seats-modal");
expect(modal.dataset.pendingUser).toBe("inA");
Expand All @@ -215,7 +216,7 @@ test("add rejects generic error (no 402): error text shown, BuySeatsModal NOT re

await typeAndDebounce("in");
vi.useRealTimers();
fireEvent.click(screen.getByText("Included A"));
fireEvent.click(screen.getByText("included-alpha-3140"));

expect(await screen.findByText("nope")).toBeTruthy();
expect(screen.queryByTestId("buy-seats-modal")).toBeNull();
Expand Down Expand Up @@ -290,7 +291,7 @@ test("BuySeatsModal onSuccess: reloadSubscription + onMemberAdded called, modal

await typeAndDebounce("in");
vi.useRealTimers();
fireEvent.click(screen.getByText("Included A"));
fireEvent.click(screen.getByText("included-alpha-3140"));
await screen.findByTestId("buy-seats-modal");
h.reload.mockClear();

Expand Down
Loading