Skip to content

Commit 800828d

Browse files
authored
Overhaul state management for multiple rooms (#112)
* overhaul room store into property store. basic room selection * fix device readonly mode * remove some logs * remove dead code * fix build * stable sort rooms * move stager panel into room select blocker * fix false companion mode with desktop peer * desktop room management * fix bug with deleting rooms
1 parent 31f5fa0 commit 800828d

File tree

93 files changed

+1557
-773
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+1557
-773
lines changed

admin/src/pages/HomePage.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ const HomePage = () => {
1212
},
1313
});
1414

15+
if (!session.id) {
16+
return (
17+
<Box>
18+
<Link to={`${import.meta.env.VITE_MAIN_UI_ORIGIN}/login`}>Login</Link>
19+
</Box>
20+
);
21+
}
22+
1523
return (
1624
<Box stacked>
1725
<Heading level={1}>Alef Admin</Heading>

app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"react-hot-toast": "2.5.1",
4545
"react-hotkeys-hook": "^4.6.1",
4646
"suncalc": "^1.9.0",
47+
"suspend-react": "^0.1.3",
4748
"three": "catalog:",
4849
"valtio": "^2.1.3",
4950
"zustand": "^5.0.3"

app/src/components/core/PropertyRoomStoreProvider.tsx

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { useCurrentDevice } from '@/services/publicApi/deviceHooks';
2-
import { useAllProperties, useProperty } from '@/services/publicApi/propertyHooks';
3-
import { PropertySocketProvider } from '@/services/publicApi/PropertySocketProvider';
2+
import { useAllProperties } from '@/services/publicApi/propertyHooks';
43
import { useMe } from '@/services/publicApi/userHooks';
5-
import { RoomStoreProvider } from '@/stores/roomStore';
6-
import { PrefixedId } from '@alef/common';
7-
import { ReactNode } from 'react';
4+
import { PropertyStoreProvider } from '@/stores/propertyStore';
5+
import { ReactNode, Suspense } from 'react';
86
import { ModeProvider } from '../xr/modes/ModeContext';
97

108
export interface PropertyRoomStoreProviderProps {
@@ -22,9 +20,9 @@ export function PropertyRoomStoreProvider({ children }: PropertyRoomStoreProvide
2220
// room locally. Internally, we provide a Stager experience with an extra option to pair
2321
// a device with a user account.
2422
return (
25-
<RoomStoreProvider roomId="r-local">
23+
<PropertyStoreProvider propertyId={null}>
2624
<ModeProvider value="staging">{children}</ModeProvider>
27-
</RoomStoreProvider>
25+
</PropertyStoreProvider>
2826
);
2927
}
3028

@@ -50,18 +48,9 @@ function WrappedWithPropertyAndRoom({ children }: { children: ReactNode }) {
5048
throw new Error(`Expected the server to provision a default property`);
5149
}
5250

53-
const {
54-
data: { rooms },
55-
} = useProperty(defaultProperty.id);
56-
const defaultRoomId = Object.keys(rooms)[0] as PrefixedId<'r'>;
57-
58-
if (!defaultRoomId) {
59-
throw new Error(`Expected the server to provision a default room`);
60-
}
61-
6251
return (
63-
<PropertySocketProvider propertyId={defaultProperty.id}>
64-
<RoomStoreProvider roomId={defaultRoomId}>{children}</RoomStoreProvider>
65-
</PropertySocketProvider>
52+
<Suspense>
53+
<PropertyStoreProvider propertyId={defaultProperty.id}>{children}</PropertyStoreProvider>
54+
</Suspense>
6655
);
6756
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useHasSelectedRoom, useSelectRoom } from '@/stores/propertyStore/hooks/editing';
2+
import { useRoomIds } from '@/stores/propertyStore/hooks/rooms';
3+
import { useEffect, useState } from 'react';
4+
5+
export function useSelectRoomBlocker({ closeOnSelect }: { closeOnSelect?: boolean } = {}) {
6+
const hasSelectedRoom = useHasSelectedRoom();
7+
const roomIds = useRoomIds();
8+
9+
const startWithSelectorVisible = roomIds.length !== 1;
10+
11+
const [showSelector, setShowSelector] = useState(startWithSelectorVisible);
12+
13+
// if there's just one room, select it automatically
14+
const onlyRoomId = roomIds.length === 1 && roomIds[0];
15+
const selectRoom = useSelectRoom();
16+
useEffect(() => {
17+
if (onlyRoomId) {
18+
selectRoom(onlyRoomId);
19+
}
20+
}, [onlyRoomId, selectRoom]);
21+
22+
const showContent = hasSelectedRoom;
23+
const close = () => {
24+
setShowSelector(false);
25+
};
26+
27+
const finalShowSelector = showSelector && (!closeOnSelect || !hasSelectedRoom);
28+
// TODO: auto-select room based on similarity to detected floor plane in XR if
29+
// multiple rooms are available.
30+
31+
return { showContent, showSelector: finalShowSelector, close };
32+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useSelectRoom } from '@/stores/propertyStore/hooks/editing';
2+
import { useRoomCreatedAt, useRoomIds } from '@/stores/propertyStore/hooks/rooms';
3+
import { PrefixedId } from '@alef/common';
4+
import { Button, Dialog } from '@alef/sys';
5+
import { ReactNode } from 'react';
6+
import { useSelectRoomBlocker } from '../core/useSelectRoomBlocker';
7+
8+
export function DesktopSelectRoomBlocker({ children }: { children: ReactNode }) {
9+
const { showContent, showSelector } = useSelectRoomBlocker({ closeOnSelect: true });
10+
11+
return (
12+
<>
13+
{showSelector && !showContent && <RoomSelectorPanel />}
14+
{showContent && children}
15+
</>
16+
);
17+
}
18+
19+
function RoomSelectorPanel() {
20+
const roomIds = useRoomIds();
21+
const selectRoom = useSelectRoom();
22+
23+
return (
24+
<Dialog open>
25+
<Dialog.Content title={roomIds.length === 0 ? 'Create a room' : 'Select a room'}>
26+
<Dialog.Description>Select a room to continue</Dialog.Description>
27+
{roomIds.map((roomId, index) => (
28+
<RoomItem key={roomId} index={index} roomId={roomId} onSelect={() => selectRoom(roomId)} />
29+
))}
30+
</Dialog.Content>
31+
</Dialog>
32+
);
33+
}
34+
35+
function RoomItem({ onSelect, roomId, index }: { onSelect: () => void; roomId: PrefixedId<'r'>; index: number }) {
36+
const createdAt = useRoomCreatedAt(roomId);
37+
return (
38+
<Button onClick={onSelect} variant="action">
39+
Room {index + 1} (created {new Date(createdAt).toLocaleDateString()})
40+
</Button>
41+
);
42+
}

app/src/components/desktop/DesktopUI.module.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
}
105105

106106
.main {
107-
width: auto;
107+
width: var(--main-size);
108108
}
109109
}
110110

@@ -115,6 +115,10 @@
115115
grid-template-rows: 1fr auto;
116116
}
117117

118+
.main {
119+
width: auto;
120+
}
121+
118122
.content {
119123
display: none;
120124
}

app/src/components/desktop/DesktopUI.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { NavBar } from '@/components/navBar/NavBar';
22
import { useDetectSticky } from '@/hooks/useDetectSticky';
33
import { useMedia } from '@/hooks/useMedia';
44
import { useIsLoggedIn } from '@/services/publicApi/userHooks';
5-
import { useUndo } from '@/stores/roomStore';
6-
import { useEditorMode, useSelect, useSelectedObjectId } from '@/stores/roomStore/hooks/editing';
5+
import { useUndo } from '@/stores/propertyStore';
6+
import { useEditorMode, useSelect, useSelectedObjectId } from '@/stores/propertyStore/hooks/editing';
77
import { EditorMode, isPrefixedId } from '@alef/common';
88
import { Box, Heading, Icon, Tabs, Text } from '@alef/sys';
9+
import clsx from 'clsx';
910
import { ReactNode, Suspense } from 'react';
1011
import { useHotkeys } from 'react-hotkeys-hook';
1112
import { DeviceDiscovery } from '../devices/DeviceDiscovery';
@@ -24,7 +25,7 @@ import { DesktopLayoutTools } from './layouts/DesktopLayoutTools';
2425
import { DesktopLightEditor } from './lighting/DesktopLightEditor';
2526
import { DesktopLightsMainEditor } from './lighting/DesktopLightsMainEditor';
2627
import { HeadsetConnectedIndicator } from './presence/HeadsetConnectedIndicator';
27-
import clsx from 'clsx';
28+
import { RoomPicker } from './rooms/RoomPicker';
2829

2930
export interface DesktopUIProps {
3031
children?: ReactNode;
@@ -46,6 +47,7 @@ export function DesktopUI({ children }: DesktopUIProps) {
4647
<DesktopUIMain />
4748
<Box className={cls.content}>
4849
<DesktopUISecondary />
50+
<RoomPicker />
4951
{!isMobile && children}
5052
</Box>
5153
</Tabs>

app/src/components/desktop/common/DesktopSecondaryContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function DesktopSecondaryContent({ children, title, open, onOpenChange }:
2323
if (!open) return null;
2424

2525
return (
26-
<Frame float="top-left" p stacked gapped style={{ minWidth: 300 }}>
26+
<Frame float="top-left" p stacked gapped style={{ minWidth: 300, maxWidth: '25vw' }}>
2727
<Box justify="between">
2828
<Heading level={2}>{title}</Heading>
2929
</Box>

app/src/components/desktop/common/MainPanelResizer.module.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
position: relative;
77
z-index: 10;
88
border: var(--border);
9+
display: flex;
910
&:focus-visible {
1011
background: var(--paper);
1112
outline: 2px solid var(--focus);
1213
}
1314
}
1415

15-
@media (max-width: 1024px) {
16+
@media (max-width: 768px) {
1617
.root {
1718
display: none;
1819
}

app/src/components/desktop/furniture/DesktopAddFurniture.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { useSetPlacingFurniture } from '@/stores/roomStore/hooks/editing';
1+
import { useMedia } from '@/hooks/useMedia';
2+
import { useSetPlacingFurniture } from '@/stores/propertyStore/hooks/editing';
23
import { Box, Button, Dialog, Icon } from '@alef/sys';
4+
import { useState } from 'react';
35
import { useHotkeys } from 'react-hotkeys-hook';
46
import { DesktopOnlineFurniturePicker } from './DesktopOnlineFurniturePicker';
57

@@ -8,18 +10,31 @@ export function DesktopAddFurniture({ className }: { className?: string }) {
810
useHotkeys('esc', () => {
911
setSelectedModelId(null);
1012
});
13+
const [open, setOpen] = useState(false);
14+
15+
const isMobile = useMedia('(max-width: 768px)');
1116

1217
return (
1318
<Box className={className} full="width" justify="stretch" align="stretch">
14-
<Dialog onOpenChange={() => setSelectedModelId(null)}>
19+
<Dialog onOpenChange={setOpen} open={open}>
1520
<Dialog.Trigger asChild>
1621
<Button color="suggested" full>
1722
<Icon name="plus" />
1823
Add furniture
1924
</Button>
2025
</Dialog.Trigger>
2126
<Dialog.Content title="Asset Library" width="large">
22-
<DesktopOnlineFurniturePicker style={{ minHeight: 0 }} />
27+
<DesktopOnlineFurniturePicker
28+
style={{ minHeight: 0 }}
29+
onSelect={() => {
30+
// TODO: clean up this logic and make it clearer what the intention is.
31+
// the intention is... mobile devices are meant to be companions to
32+
// headsets for placing furniture, whereas on desktop we want to
33+
// hide the modal and let you use the viewport to place it.
34+
if (isMobile) return;
35+
setOpen(false);
36+
}}
37+
/>
2338
</Dialog.Content>
2439
</Dialog>
2540
</Box>

0 commit comments

Comments
 (0)