diff --git a/packages/server/src/routes/spaces.ts b/packages/server/src/routes/spaces.ts
index 5ad31e77..f9be826f 100644
--- a/packages/server/src/routes/spaces.ts
+++ b/packages/server/src/routes/spaces.ts
@@ -1103,6 +1103,7 @@ export async function spaceRoutes(app: FastifyInstance): Promise {
const instanceName = settings?.instanceName ?? 'Backspace';
return reply.code(200).send({
+ spaceId: space.id,
spaceName: space.name,
description: space.description ?? null,
icon: space.icon ?? null,
diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts
index 64435542..2205fa0b 100644
--- a/packages/shared/src/types.ts
+++ b/packages/shared/src/types.ts
@@ -53,6 +53,7 @@ export interface Space {
}
export interface InvitePreview {
+ spaceId: string;
spaceName: string;
description: string | null;
icon: string | null;
diff --git a/packages/web/src/components/JoinPage.tsx b/packages/web/src/components/JoinPage.tsx
index b8cd763a..a7cbc40f 100644
--- a/packages/web/src/components/JoinPage.tsx
+++ b/packages/web/src/components/JoinPage.tsx
@@ -8,7 +8,7 @@ import { parseInviteInput } from '../utils/inviteParser';
import { Avatar } from './ui/Avatar';
import type { InvitePreview } from '@backspace/shared';
-type JoinPhase = 'preview' | 'connect' | 'fallback' | 'other-instance';
+type JoinPhase = 'preview' | 'connect' | 'fallback' | 'other-instance' | 'already-member';
export function JoinPage() {
const { inviteCode: rawInviteCode } = useParams<{ inviteCode: string }>();
@@ -106,7 +106,13 @@ export function JoinPage() {
setPhase('connect');
setError('');
} else {
- setError(err instanceof Error ? err.message : 'Failed to join space');
+ const msg = err instanceof Error ? err.message : 'Failed to join space';
+ if (msg.toLowerCase().includes('already a member')) {
+ setPhase('already-member');
+ setError('');
+ } else {
+ setError(msg);
+ }
}
} finally {
setIsJoining(false);
@@ -130,7 +136,13 @@ export function JoinPage() {
setFallbackPassword('');
setError('');
} else {
- setError(err instanceof Error ? err.message : 'Failed to connect');
+ const msg = err instanceof Error ? err.message : 'Failed to connect';
+ if (msg.toLowerCase().includes('already a member')) {
+ setPhase('already-member');
+ setError('');
+ } else {
+ setError(msg);
+ }
}
} finally {
setIsJoining(false);
@@ -148,7 +160,13 @@ export function JoinPage() {
const space = await joinByCode(parsed.code, parsed.origin);
navigate(`/channels/${space.id}`);
} catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to log in');
+ const msg = err instanceof Error ? err.message : 'Failed to log in';
+ if (msg.toLowerCase().includes('already a member')) {
+ setPhase('already-member');
+ setError('');
+ } else {
+ setError(msg);
+ }
} finally {
setIsJoining(false);
}
@@ -295,6 +313,14 @@ export function JoinPage() {
>
Log in
+ {' · '}
+
) : token ? (
@@ -431,6 +457,11 @@ export function JoinPage() {
)}
+ {/* Phase: already-member — green success with auto-redirect */}
+ {phase === 'already-member' && preview && (
+
+ )}
+
{/* Phase: fallback — different password on remote */}
{phase === 'fallback' && (