import { AppBar, ButtonGroup, Card, CardContent, Container, SwipeableDrawer, IconButton, InputAdornment, Stack, TextField, Toolbar, Typography, List, ListItemButton, ListItemIcon, ListItemText, Divider, Button, CircularProgress } from '@mui/material'; import { Box } from '@mui/system'; import React, { Fragment, useState, useEffect, useRef } from 'react'; import { Outlet, useNavigate, useParams } from 'react-router'; import HomeItem from './components/HomeItem'; import HomeCommand from './components/HomeCommand'; import { Hash, HashedObject, Identity, LinkupAddress, MutableReference, MutableSet, MutableSetEvents, MutationEvent, MutationObserver, ObjectBroadcastAgent, ObjectDiscoveryReply, PeerInfo, PeerSource, Resources, SpaceEntryPoint, WordCode } from '@hyper-hyper-space/core'; import { HyperBrowserConfig } from '../../model/HyperBrowserConfig'; import { PeerComponent, useObjectDiscoveryWithResources, useObjectState } from '@hyper-hyper-space/react'; import { Home, Folder, Device, FolderItem, SpaceLink, FolderTreeEvents, Profile, ChatSpace, Conversation } from '@hyper-hyper-space/home'; import CreateFolderDialog from './components/CreateFolderDialog'; import RenameFolderItemDialog from './components/RenameFolderDialog'; import { Link } from 'react-router-dom'; import CreateSpaceDialog from './components/CreateSpaceDialog'; import { SpaceDisplayInfo, supportedSpaces } from '../../model/SupportedSpaces'; import { FolderTreeSearch } from '../../model/FolderTreeSearch'; import AskForPersistentStorageDialog from './components/AskForPersistentStorageDialog'; import { TextSpace } from '../../model/text/TextSpace'; import { WikiSpace } from '@hyper-hyper-space/wiki-collab'; import InfoDialog from '../../components/InfoDialog'; type HomeContext = { resources: Resources | undefined, resourcesForDiscovery: Resources | undefined, home: Home | undefined, owner: Identity | undefined, localDevice: Device | undefined, chats: ChatSpace | undefined, spaceEntryPoints: {[key: Hash]: SpaceEntryPoint & HashedObject}, openFolder: (folder: Folder, path?: string) => void, openCreateFolder: () => void, openRenameFolderItem: (item: FolderItem) => void, openSpace: (entryPointHash: Hash) => void, openCreateSpace: () => void, deleteFolderItem: (item: FolderItem, parent: Folder) => void, setViewingFolder: (folder?: Folder) => void, setViewingFolderByHash: (hash: Hash) => void, viewingFolder: Folder | undefined, removeSpaceFromProfile: (link: SpaceLink) => void, showSpaceInProfile: (link: SpaceLink) => void } function HomeSpace() { const params = useParams(); const homeHash = decodeURIComponent(params.hash as Hash); const [homeResources, setHomeResources] = useState(undefined); const [loadError, setLoadError] = useState(undefined); useEffect(() => { HyperBrowserConfig.initHomeResources(homeHash, setLoadError, 'worker').then((r: Resources) => { setHomeResources(r); }).catch((reason: any) => { console.log('could not init home resources:'); console.log(reason); }) }, []); const [showAskForPersistentStorageDialog, setShowAskForPersistentStorageDialog] = useState(false); const closeAskForPersistentStorageDialog = () => { setShowAskForPersistentStorageDialog(false); } const openAskForPersistentStorageDialog = () => { setShowAskForPersistentStorageDialog(true); } const [home, setHome] = useState(undefined); const homeState = useObjectState(home); const [localDevice, setLocalDevice] = useState(undefined); const [owner, setOwner] = useState(undefined); const [desktopFolder, setDesktopFolder] = useState(undefined); const [chats, setChats] = useState(undefined); const chatsState = useObjectState(chats); const [unreadChatsCount, setUnreadChatsCount] = useState(0); const desktopFolderState = useObjectState(desktopFolder, {filterMutations: desktopFolder?.ownEventsFilter()}); const [viewingFolder, setViewingFolder] = useState() /*useEffect(() => { console.log('homeState') console.log(homeState) //console.log(Object.values((homeState?.fields['desktop']?.fields['root']?.fields['items'].contents) || {})); }, [homeState]); useEffect(() => { console.log('desktopFolderState'); console.log(desktopFolderState); console.log(desktopFolderState?.value?.items?.getMutableContents().size) }, [desktopFolderState]); */ //const [currentFolder, setCurrentFolder] = useState(homeState?.fields['desktop']?.fields['root']?.current as Folder); const [showCreateSpace, setShowCreateSpace] = useState(false); const openCreateSpace = () => { setShowCreateSpace(true); } const closeCreateSpace = () => { setShowCreateSpace(false); } const [showCreateFolder, setShowCreateFolder] = useState(false); const openCreateFolder = () => { setShowCreateFolder(true); } const closeCreateFolder = () => { setShowCreateFolder(false); } const [folderItemToRename, setFolderItemToRename] = useState(undefined); const openRenameFolderItem = (item: FolderItem) => { setFolderItemToRename(item); } const closeRenameFolderItem = () => { setFolderItemToRename(undefined); } const deleteFolderItem = (item: FolderItem, parent: Folder) => { parent.items?.deleteElement(item); parent.items?.saveQueuedOps(); } const openFolder = (folder: Folder, path?: string) => { const pathPrefix = path === undefined? './folder/' : './folder/' + encodeURIComponent(path) + '_'; const url = pathPrefix + encodeURIComponent(folder.getLastHash()); navigate(url); } const openSpace = (entryPointHash: Hash) => { window.open('./#/space/' + encodeURIComponent(entryPointHash), '_blank'); } const showSpaceInProfile = (item: SpaceLink) => { home?.profile?.published?.add(item); home?.profile?.published?.save(); } const removeSpaceFromProfile = (item: SpaceLink) => { home?.profile?.published?.delete(item); home?.profile?.published?.save(); } const setViewingFolderByHash = (hash: Hash) => { const folder = home?.desktop?._currentFolderItems.get(hash) as (Folder|undefined); if (folder !== undefined) { setViewingFolder(folder); } else { homeResources?.store.load(hash, false).then((obj?: HashedObject) => { if (obj instanceof Folder) { obj.loadItemNamesAndWatchForChanges().then(() => { setViewingFolder(obj); }); } }) } } const [resourcesForDiscovery, setResourcesForDiscovery] = useState(undefined); useEffect(() => { HyperBrowserConfig.initStarterResources().then((r: Resources) => { setResourcesForDiscovery(r); }, (reason: any) => { alert('Error initializing discovery resources: ' + reason); }) }, []); const spaceEntryPoints = new Map(); const [spaceEntryPointsState, setSpaceEntryPointsState] = useState<{[key: Hash]: HashedObject & SpaceEntryPoint}>({}) const homeContext: HomeContext = { resources: homeResources, resourcesForDiscovery: resourcesForDiscovery, home: home, localDevice: localDevice, owner: owner, chats: chats, openFolder: openFolder, openCreateFolder: openCreateFolder, openRenameFolderItem: openRenameFolderItem, openSpace: openSpace, openCreateSpace: openCreateSpace, deleteFolderItem: deleteFolderItem, setViewingFolder: setViewingFolder, setViewingFolderByHash: setViewingFolderByHash, viewingFolder: viewingFolder, removeSpaceFromProfile: removeSpaceFromProfile, showSpaceInProfile: showSpaceInProfile, spaceEntryPoints: spaceEntryPointsState }; const [showVersionMismatchAlert, setShowVersionMismatchAlert] = useState(false); useEffect(() => { const initHome = async () => { const obj = await homeResources?.store.load(homeHash, false); if (obj === undefined) { setLoadError('Error: The home object (hash ' + homeHash + ') is missing from the store.'); return; } if (!(obj instanceof Home)) { setLoadError('Error: The home object is of the wrong type:' + obj?.getClassName()); return; } if (obj.version !== Home.version) { setShowVersionMismatchAlert(true); } const newHome = obj as Home; const spacesObserver: MutationObserver = (ev: MutationEvent) => { if (ev.emitter === newHome.desktop) { if (ev.action === FolderTreeEvents.AddSpace) { HyperBrowserConfig.initSavedSpaceResources(newHome, ev.data as HashedObject).then((r: Resources) => { const entryPoint = (ev.data as HashedObject & SpaceEntryPoint).clone(); const entryPointHash = entryPoint.getLastHash(); spaceEntryPoints.set(entryPointHash, entryPoint); setSpaceEntryPointsState(Object.fromEntries(spaceEntryPoints.entries())); entryPoint.setResources(r); entryPoint.startSync(); console.log('started sync of space ' + entryPointHash); console.log('with resources', r); }); } else if (ev.action === FolderTreeEvents.RemoveSpace) { const entryPointHash = (ev.data as HashedObject).getLastHash(); const entryPoint = spaceEntryPoints.get(entryPointHash); setSpaceEntryPointsState(Object.fromEntries(spaceEntryPoints.entries())); entryPoint?.stopSync(); setTimeout(() => { entryPoint?.getResources()?.mesh?.shutdown(); setTimeout(() => { entryPoint?.getResources()?.store.close(); }, 2500); }, 5000); spaceEntryPoints.delete(entryPointHash); setSpaceEntryPointsState(Object.fromEntries(spaceEntryPoints.entries())); console.log('stopped sync of space ' + entryPointHash); } } }; newHome.desktop?.addObserver(spacesObserver); await newHome.findLocalDevice(); const newOwner = newHome?.getAuthor(); if (newOwner === undefined) { setLoadError('Error: the home object has no owner.'); return; } setOwner(newOwner); const rootHash = newHome?.desktop?.root?.hash() as Hash; const desktopFolder = await homeResources?.store.loadAndWatchForChanges(rootHash) as Folder; await desktopFolder.loadItemNamesAndWatchForChanges(); setDesktopFolder(desktopFolder); const contactsObserver: MutationObserver = (ev: MutationEvent) => { if (ev.emitter === newHome.contacts?.current) { const p = (ev.data as Profile); if (ev.action === MutableSetEvents.Add) { p.about?.loadAndWatchForChanges(); p.picture?.loadAndWatchForChanges(); p.pictureMIMEType?.loadAndWatchForChanges(); } else if (ev.action === MutableSetEvents.Delete) { p.about?.dontWatchForChanges(); p.picture?.dontWatchForChanges(); p.pictureMIMEType?.dontWatchForChanges(); } } }; newHome.contacts?.addObserver(contactsObserver); await new Promise(r => setTimeout(r, 100)); //await newHome.loadAndWatchForChanges(); console.log('STARTING SYNC') await newHome.startSync(); console.log('DONE STARTING SYNC') await newHome._localDevice?.name?.loadAndWatchForChanges(); await newHome._localDevice?.hostContactSpaces?.loadAndWatchForChanges(); newHome.contacts?.addObserver(contactsObserver); for (const p of (newHome.contacts?.current as MutableSet)._elements.values()) { p.about?.loadAndWatchForChanges(); p.picture?.loadAndWatchForChanges(); p.pictureMIMEType?.loadAndWatchForChanges(); } //await newHome.loadAndWatchForChanges(); //await newHome.loadHomeDevice(); //await newHome.startSync(); setHome(newHome); setLocalDevice(newHome._localDevice); const newChats = new ChatSpace(newOwner); let chats = await homeResources?.store.load(newChats.hash(), false) as ChatSpace; if (chats === undefined) { await homeResources?.store.save(newChats); chats = newChats; } console.log('STARTING CHAT SPACE SYNC'); await chats.startSync({localPeer: newHome._devicePeers?.localPeer as PeerInfo, peerSource: newHome._devicePeers?.peerSource as PeerSource}); console.log('DONE STARTING CHAT SPACE SYNC'); setChats(chats); const synchronizing = new Map(); const updateCoHostedSpaces = () => { const toStop = new Set(synchronizing.keys()); if (newHome._localDevice?.hostContactSpaces?.getValue() !== false) { for (const id of newHome.contacts?.hosting?.values() || []) { const pHash = new Profile(id).hash(); const p = newHome.contacts?.current?.get(pHash); if (p !== undefined) { for (const link of p.published?.values() || []) { const configHash = newHome.contacts?.getHostingConfig(link.getLastHash(), pHash)?.hash() as Hash; const config = newHome.contacts?._hostingConfig?.get(configHash); if (config?.getValue() !== 'off') { if (!synchronizing.has(link.getLastHash())) { (link.spaceEntryPoint as any as SpaceEntryPoint)?.startSync(); synchronizing.set(link.getLastHash(), link); } toStop.delete(link.hash()) } } } } } for (const hash of toStop.values()) { (synchronizing.get(hash)?.spaceEntryPoint as any as SpaceEntryPoint)?.stopSync(); synchronizing.delete(hash); } } const hostingObserver: MutationObserver = (ev: MutationEvent) => { updateCoHostedSpaces(); } newHome._localDevice?.hostContactSpaces?.addObserver(hostingObserver); newHome.contacts?.hosting?.addObserver(hostingObserver); newHome.contacts?.current?.addObserver(hostingObserver); newHome.contacts?._hostingConfig?.addObserver(hostingObserver); } if (homeResources !== undefined && homeHash !== undefined) { initHome().catch((reason: any) => { setLoadError(String(reason)) console.log('Error initializing home:'); console.log(reason); }); } return () => { console.log('stopping sync for all spaces') const toDelete: Array = [] for (const [entryPointHash, entryPoint] of spaceEntryPoints.entries()) { toDelete.push(entryPointHash); entryPoint?.stopSync(); entryPoint?.getResources()?.mesh?.shutdown(); setTimeout(() => { entryPoint?.getResources()?.store.close(); }, 5000); spaceEntryPoints.delete(entryPointHash); console.log('stopped sync of space ' + entryPointHash); } for (const entryPointHash of toDelete) { spaceEntryPoints.delete(entryPointHash); } setSpaceEntryPointsState(Object.fromEntries(spaceEntryPoints.entries())); }; }, [homeHash, homeResources]); useEffect(() => { console.log('checking for persistent storage...') if (navigator.storage?.persisted !== undefined) { navigator.storage.persisted().then((persisted: boolean) => { if (!persisted) { console.log('persistent storage not granted') openAskForPersistentStorageDialog(); } else { console.log('persistent storage granted') } }); } else { console.log('persistent storage is not supported by this browser') } }, [home]); useEffect(() => { if (chatsState?.value !== undefined) { let unread = 0; for (const conv of chatsState?.value.conversations?.values()!) { unread = unread + conv._unreadMessages.size; } setUnreadChatsCount(unread); } }, [chatsState]); const [searchValue, setSearchValue] = useState(''); const searching = searchValue !== ''; const [wordsForDiscovery, setWordsForDiscovery] = useState(undefined); const [noDiscovery, setNoDiscovery] = useState(false); const discovered = useObjectDiscoveryWithResources(resourcesForDiscovery, wordsForDiscovery, 'en', 10, true); const results = discovered? Array.from(discovered.values()).filter((r: ObjectDiscoveryReply) => r.object !== undefined && (supportedSpaces.has(r.object.getClassName()) || r.object.getClassName() === Identity.className)) : []; const [wordsForLocalSearch, setWordsForLocalSearch] = useState(undefined); const [searchResults, setSearchResults] = useState>([]); // This effect pre-fetches the identities that were returned from discovery. // Since querying for the profile and synchronizing its mutalbe parts takesa few seconds, // this prefetch helps make the display of the profile apperar more snappy. const [profilesToPreload, setProfilesToPreload] = useState>([]); useEffect(() => { if (profilesToPreload.length === 0) { const profiles: Array = []; for (const reply of discovered.values()) { if (reply.object instanceof Identity) { let p = new Profile(reply.object); profiles.push(p); } } if (profiles.length > 0) { setProfilesToPreload(profiles); } } }, [discovered]); useEffect(() => { let cancelled = false; const preloadedProfiles: Array = []; for (const profile of profilesToPreload) { HyperBrowserConfig .initTransientSpaceResources(profile.getLastHash()) .then((tmpResources: Resources) => { if (!cancelled) { const preloadedProfile = profile.clone(); preloadedProfile.setResources(tmpResources); tmpResources.store.save(preloadedProfile).then(() => { if (!cancelled) { preloadedProfile.loadAndWatchForChanges().then(() => { if (!cancelled) { preloadedProfile.startSync(); preloadedProfiles.push(preloadedProfile); } }); } }); } }); } return () => { cancelled = true; for (const profile of preloadedProfiles) { profile.stopSync().then(() => { profile.dontWatchForChanges(); const r = profile.getResources(); if (r !== undefined) { r.store.close(); r.mesh.shutdown(); } }); } }; }, [profilesToPreload, resourcesForDiscovery]); useEffect(() => { const root = homeState?.value?.desktop?.root; if (wordsForLocalSearch === undefined || root === undefined) { setSearchResults([]); } else { setSearchResults(FolderTreeSearch.byPhrase(wordsForLocalSearch, root)); } }, [wordsForLocalSearch, homeState]) const doSearch = (value: string) => { const words = value.toLowerCase().split(/[ -]+/); if (words.length === 3 && WordCode.english.check(words[0]) && WordCode.english.check(words[1]) && WordCode.english.check(words[2])) { setWordsForDiscovery(words[0] + '-' + words[1] + '-' + words[2]); setDiscoveryTimeout(window.setTimeout(discoveryTimeoutCallback, 8000)); } else { setWordsForDiscovery(undefined); } if (value.trim().length > 0) { setWordsForLocalSearch(value); } else { setWordsForLocalSearch(undefined); } }; const searchValueChanged = (e: React.ChangeEvent) => { const newValue = e.currentTarget.value; setSearchValue(newValue); setNoDiscovery(false); setShowTimeoutMessage(false); if (discoveryTimeout !== undefined) { window.clearTimeout(discoveryTimeout); } setDiscoveryTimeout(undefined); if (searchTimeout !== undefined) { window.clearTimeout(searchTimeout); } setSearchTimeout(window.setTimeout(() => doSearch(newValue), 200)); }; const searchKeyUp = (e: React.KeyboardEvent) => { setProfilesToPreload([]); if (e.key === 'Escape') { setSearchValue(''); } } //const ready = !(homeResources === undefined || home === undefined || owner === undefined || homeState === undefined); const ready = !(homeResources === undefined || desktopFolder === undefined || desktopFolderState === undefined || owner === undefined); const [showDrawer, setShowDrawer] = useState(false); const onCloseDrawer = (ev: React.SyntheticEvent) => { setShowDrawer(false); }; const onOpenDrawer = (ev: React.SyntheticEvent) => { setShowDrawer(true); }; const openDrawer = () => { setShowDrawer(true); }; const navigate = useNavigate(); const openManageLinkedDevicesDialog = () => { navigate('./devices'); }; const [searchTimeout, setSearchTimeout] = useState(undefined); const [discoveryTimeout, setDiscoveryTimeout] = useState(undefined); const [showTimeoutMessage, setShowTimeoutMessage] = useState(false); const discoveryTimeoutCallback = () => { setShowTimeoutMessage(true); setDiscoveryTimeout(undefined); } const noLookup = () => { setNoDiscovery(true); } const openStorageDialog = () => { navigate('./storage'); } return ( { !ready && { loadError === undefined &&

Initializing home space...

} { loadError !== undefined &&

{loadError}

}
} { ready && 🏠 {owner.info?.name}'s Home Space
{ !searching && {/*{Object.values((homeState?.fields['desktop']?.fields['root']?.fields['items']?.contents) || {}).map((proxy: StateProxy) => {*/} {/*{Object.values(((homeState?.value as Home)?.desktop?.root?.items?.contents()) || {}).map((proxy: FolderItem) => {*/} {Object.values(((desktopFolderState?.value as Folder)?.items?.contents()) || {}).map((proxy: FolderItem) => { const showItem = (item: FolderItem) => { //const item = homeState?.fields['desktop']?.fields['items']?.contents['itemHash']; //const name = item.fields['name'].current as MutableReference; if (item instanceof Folder && item.name?.getValue() !== undefined) { const name = item.name; return { openFolder(item); }} menu={[{name: 'Open', action: () => { openFolder(item); } }, {name: 'Rename', action: () => { openRenameFolderItem(item); }}, {name: 'Delete', action: () => { deleteFolderItem(item, desktopFolder); }} ]} />; } else if (item instanceof SpaceLink && item.name?.getValue() !== undefined) { const name = item.name; let icon = ""; let title: (string | undefined) = undefined; if (item.spaceEntryPoint instanceof TextSpace) { title = 'Text File'; icon = 'streamline-icon-pencil-write-1@48x48.png'; } else if (item.spaceEntryPoint instanceof WikiSpace) { title = 'Wiki'; icon = 'streamlinehq-book-edit-content-48.png'; } const inProfile = item.spaceEntryPoint !== undefined && home?.profile?.published?.has(item); return { openSpace(item.spaceEntryPoint?.getLastHash() as Hash); }} published={inProfile} menu={[{name: 'Open', action: () => { openSpace(item.spaceEntryPoint?.getLastHash() as Hash); } }, {name: 'Rename', action: () => { openRenameFolderItem(item); }}, {name: 'Delete', action: () => { deleteFolderItem(item, desktopFolder); }}, inProfile? {name: 'Remove from Profile', action: () => { removeSpaceFromProfile(item); }} : {name: 'Share in Profile', action: () => { showSpaceInProfile(item); }}]} title={title} />; } else { return ; } }; return showItem(proxy); }) } {/**/} {/* {alert('Pages will be available soon.')}}>*/} {/* {alert('Text documents will be available soon.')}}>*/} {/* */} {alert('Archived items will be available soon.')}}> } { !searching &&

} 🔎 ), }} label="3-words for lookup or keywords to search your home" value={searchValue} onChange={searchValueChanged} onKeyUp={searchKeyUp} > { searching && wordsForDiscovery !== undefined && !noDiscovery && Matches for 3-word code {wordsForDiscovery.replaceAll('-', ' ')} {results.length === 0 && { showTimeoutMessage && This is taking longer than expected. Are your 3-words correct? Poke someone to open this space then. } } {results.length > 0 && } style={{marginTop: '1rem'}} > {results.map((r: ObjectDiscoveryReply) => ( { r.object?.getClassName() !== Identity.className && {(supportedSpaces.get(r.object?.getClassName() as string) as SpaceDisplayInfo).name}{r.object?.getAuthor()?.info?.name && space by {r.object?.getAuthor()?.info?.name} } } { r.object?.getClassName() === Identity.className && {(r.object as Identity)?.info?.type || 'Identity'} named {(r.object as Identity)?.info?.name} } ) )} } } { searching && searchResults.length > 0 && {searchResults.map((link: SpaceLink) => ( { openSpace(link.spaceEntryPoint?.getLastHash() as Hash); }} /> ))} }
{navigate('./edit-profile');}}> {navigate('./contacts');}}> {navigate('./chats');}/*() => {alert('Chat will be available soon')}*/}> {navigate('./space-sharing');}}> {/* */}
{ showAskForPersistentStorageDialog && } { showCreateSpace && } { showCreateFolder && } { folderItemToRename !== undefined && } {/* homeState?.value?.desktop?.root */} { showVersionMismatchAlert &&

While the home you're trying to open is in version {homeState?.value?.version || '0.0.0'}, this website can handle only version {Home.version}.

You may still try to open your home, but it may not work correctly.

We're working on keeping old versions of the Hyper Browser online, but they're not ready yet.

} onClose={() => {setShowVersionMismatchAlert(false);}} /> }
}
); } export type { HomeContext }; export default HomeSpace;