tldraw/apps/dotcom/src/components/MultiplayerEditor.tsx
alex ce493dcfaf
[4/5] sync -> sync-core, sync-react -> sync (#4123)
Renames `@tldraw/sync` to `@tldraw/sync-core`, and `@tldraw/sync-react`
to `@tldraw/sync`. This also adds an export * from sync-core to sync.

- [x] `other`
2024-07-10 16:09:10 +00:00

189 lines
4.9 KiB
TypeScript

import { ROOM_OPEN_MODE, RoomOpenModeToPath, type RoomOpenMode } from '@tldraw/dotcom-shared'
import { useMultiplayerSync } from '@tldraw/sync'
import { useCallback } from 'react'
import {
DefaultHelpMenu,
DefaultHelpMenuContent,
DefaultKeyboardShortcutsDialog,
DefaultKeyboardShortcutsDialogContent,
DefaultMainMenu,
EditSubmenu,
Editor,
ExportFileContentSubMenu,
ExtrasGroup,
PeopleMenu,
PreferencesGroup,
TLComponents,
Tldraw,
TldrawUiButton,
TldrawUiButtonIcon,
TldrawUiButtonLabel,
TldrawUiMenuGroup,
TldrawUiMenuItem,
ViewSubmenu,
assertExists,
useActions,
useEditor,
useTranslation,
useValue,
} from 'tldraw'
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
import { assetUrls } from '../utils/assetUrls'
import { MULTIPLAYER_SERVER } from '../utils/config'
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
import { multiplayerAssetStore } from '../utils/multiplayerAssetStore'
import { useSharing } from '../utils/sharing'
import { OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION, useFileSystem } from '../utils/useFileSystem'
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
import { DocumentTopZone } from './DocumentName/DocumentName'
import { MultiplayerFileMenu } from './FileMenu'
import { Links } from './Links'
import { ShareMenu } from './ShareMenu'
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
import { StoreErrorScreen } from './StoreErrorScreen'
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
const components: TLComponents = {
ErrorFallback: ({ error }) => {
throw error
},
HelpMenu: () => (
<DefaultHelpMenu>
<TldrawUiMenuGroup id="help">
<DefaultHelpMenuContent />
</TldrawUiMenuGroup>
<Links />
</DefaultHelpMenu>
),
MainMenu: () => (
<DefaultMainMenu>
<MultiplayerFileMenu />
<EditSubmenu />
<ViewSubmenu />
<ExportFileContentSubMenu />
<ExtrasGroup />
<PreferencesGroup />
<Links />
</DefaultMainMenu>
),
KeyboardShortcutsDialog: (props) => {
const actions = useActions()
return (
<DefaultKeyboardShortcutsDialog {...props}>
<TldrawUiMenuGroup label="shortcuts-dialog.file" id="file">
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
<TldrawUiMenuItem {...actions[OPEN_FILE_ACTION]} />
</TldrawUiMenuGroup>
<DefaultKeyboardShortcutsDialogContent />
</DefaultKeyboardShortcutsDialog>
)
},
TopPanel: () => {
const editor = useEditor()
const isOffline = useValue(
'offline',
() => {
const status = assertExists(
editor.store.props.multiplayerStatus,
'should be used with multiplayer store'
)
return status.get() === 'offline'
},
[]
)
return <DocumentTopZone isOffline={isOffline} />
},
SharePanel: () => {
const editor = useEditor()
const msg = useTranslation()
return (
<div className="tlui-share-zone" draggable={false}>
<PeopleMenu>
<div className="tlui-people-menu__section">
<TldrawUiButton
type="menu"
data-testid="people-menu.invite"
onClick={() => editor.addOpenMenu('share menu')}
>
<TldrawUiButtonLabel>{msg('people-menu.invite')}</TldrawUiButtonLabel>
<TldrawUiButtonIcon icon="plus" />
</TldrawUiButton>
</div>
</PeopleMenu>
<ShareMenu />
</div>
)
},
}
export function MultiplayerEditor({
roomOpenMode,
roomSlug,
}: {
roomOpenMode: RoomOpenMode
roomSlug: string
}) {
const handleUiEvent = useHandleUiEvents()
const storeWithStatus = useMultiplayerSync({
uri: `${MULTIPLAYER_SERVER}/${RoomOpenModeToPath[roomOpenMode]}/${roomSlug}`,
roomId: roomSlug,
assets: multiplayerAssetStore,
})
const sharingUiOverrides = useSharing()
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
const isReadonly =
roomOpenMode === ROOM_OPEN_MODE.READ_ONLY || roomOpenMode === ROOM_OPEN_MODE.READ_ONLY_LEGACY
const handleMount = useCallback(
(editor: Editor) => {
if (!isReadonly) {
;(window as any).app = editor
;(window as any).editor = editor
}
editor.updateInstanceState({
isReadonly,
})
editor.registerExternalAssetHandler('url', createAssetFromUrl)
},
[isReadonly]
)
if (storeWithStatus.error) {
return <StoreErrorScreen error={storeWithStatus.error} />
}
return (
<div className="tldraw__editor">
<Tldraw
store={storeWithStatus}
assetUrls={assetUrls}
onMount={handleMount}
overrides={[sharingUiOverrides, fileSystemUiOverrides]}
initialState={isReadonly ? 'hand' : 'select'}
onUiEvent={handleUiEvent}
components={components}
inferDarkMode
>
<UrlStateSync />
<SneakyOnDropOverride isMultiplayer />
<ThemeUpdater />
</Tldraw>
</div>
)
}
export function UrlStateSync() {
const syncViewport = useCallback((params: UrlStateParams) => {
window.history.replaceState(
{},
document.title,
window.location.pathname + `?v=${params.v}&p=${params.p}`
)
}, [])
useUrlState(syncViewport)
return null
}