fix: federated leaveSpace 403 and context menu for owner spaces

Use getMyUserIdForOrigin() in leaveSpace() so federated spaces send
the correct remote user ID instead of the home ID. Show context menu
for all spaces with "Invite People" action; "Leave Space" only for
non-owners.
This commit is contained in:
Jannis Braun
2026-03-11 17:45:50 +01:00
parent 3eabfb9da9
commit b709862741
2 changed files with 42 additions and 16 deletions
@@ -169,9 +169,11 @@ function SpaceContextMenu({ spaceId, x, y, onClose }: { spaceId: string; x: numb
const space = useSpaceStore((s) => s.spaces.find(sp => sp.id === spaceId)); const space = useSpaceStore((s) => s.spaces.find(sp => sp.id === spaceId));
const currentUserId = useAuthStore((s) => s.user?.id); const currentUserId = useAuthStore((s) => s.user?.id);
const leaveSpace = useSpaceStore((s) => s.leaveSpace); const leaveSpace = useSpaceStore((s) => s.leaveSpace);
const generateInvite = useSpaceStore((s) => s.generateInvite);
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId); const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
const setCurrentSpace = useSpaceStore((s) => s.setCurrentSpace); const setCurrentSpace = useSpaceStore((s) => s.setCurrentSpace);
const setShowDms = useUIStore((s) => s.setShowDms); const setShowDms = useUIStore((s) => s.setShowDms);
const addToast = useUIStore((s) => s.addToast);
const navigate = useNavigate(); const navigate = useNavigate();
const isOwner = space?.ownerId === currentUserId; const isOwner = space?.ownerId === currentUserId;
@@ -198,12 +200,25 @@ function SpaceContextMenu({ spaceId, x, y, onClose }: { spaceId: string; x: numb
}; };
}, [onClose]); }, [onClose]);
// Don't render for owners (no menu items) if (!space) return null;
if (isOwner || !space) return null;
const handleInvite = async () => {
try {
const code = await generateInvite(spaceId);
const origin = (space as any)._instanceOrigin || window.location.origin;
const url = `${origin}/invite/${code}`;
await navigator.clipboard.writeText(url);
addToast('Invite link copied to clipboard', 'success', 3000);
} catch {
addToast('Failed to generate invite', 'warning', 3000);
}
onClose();
};
// Viewport-aware clamping // Viewport-aware clamping
const menuWidth = 180; const menuWidth = 180;
const menuHeight = 40; const itemCount = isOwner ? 1 : 2;
const menuHeight = itemCount * 32 + 8;
const clampedX = Math.min(x, window.innerWidth - menuWidth - 8); const clampedX = Math.min(x, window.innerWidth - menuWidth - 8);
const clampedY = Math.min(y, window.innerHeight - menuHeight - 8); const clampedY = Math.min(y, window.innerHeight - menuHeight - 8);
@@ -213,6 +228,16 @@ function SpaceContextMenu({ spaceId, x, y, onClose }: { spaceId: string; x: numb
className="fixed z-[9999] min-w-[160px] bg-surface-overlay rounded-lg border border-white/[0.07] shadow-lg py-1 animate-in fade-in zoom-in-95 duration-100" className="fixed z-[9999] min-w-[160px] bg-surface-overlay rounded-lg border border-white/[0.07] shadow-lg py-1 animate-in fade-in zoom-in-95 duration-100"
style={{ left: clampedX, top: clampedY }} style={{ left: clampedX, top: clampedY }}
> >
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-txt-primary hover:bg-white/[0.06] transition-colors"
onClick={handleInvite}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
</svg>
Invite People
</button>
{!isOwner && (
<button <button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-accent-rose hover:bg-accent-rose/10 transition-colors" className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-accent-rose hover:bg-accent-rose/10 transition-colors"
onClick={() => { onClick={() => {
@@ -230,6 +255,7 @@ function SpaceContextMenu({ spaceId, x, y, onClose }: { spaceId: string; x: numb
</svg> </svg>
Leave Space Leave Space
</button> </button>
)}
</div>, </div>,
document.body, document.body,
); );
+1 -1
View File
@@ -239,7 +239,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
const space = get().spaces.find(s => s.id === spaceId); const space = get().spaces.find(s => s.id === spaceId);
const origin = (space as TaggedSpace)?._instanceOrigin ?? ''; const origin = (space as TaggedSpace)?._instanceOrigin ?? '';
const targetApi = getApiForOrigin(origin); const targetApi = getApiForOrigin(origin);
const userId = useAuthStore.getState().user?.id; const userId = getMyUserIdForOrigin(origin);
if (!userId) return; if (!userId) return;
await targetApi.spaces.removeMember(spaceId, userId); await targetApi.spaces.removeMember(spaceId, userId);
set((state) => ({ set((state) => ({