diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-page.js | 52 |
1 files changed, 47 insertions, 5 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index fe858b2..48b1c80 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -5,6 +5,8 @@ import { t } from './i18n.js'; import { Icon } from './icon.js'; import { transfers } from './transfers.js'; import { downloadEntry } from './file-utils.js'; +import { resolveLinkedPath } from './group-link.js'; +import { copyLink, entryPath, groupItemLink } from './copy-link.js'; import { HUB, session, hubFetch, ensureFreshToken, _loadBundleKey, _loadRecoveryKey, _storeBundleKey, @@ -33,7 +35,8 @@ import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js'; */ function GroupPage({ groupId, group, token, username, userId, userPrefs, onRefreshAuth, onJoined, onGroupUpdated, onPresence, onLeft, - onPlayQueue: parentOnPlayQueue, onStopMusic }) { + onPlayQueue: parentOnPlayQueue, onStopMusic, + openPath = '', onLinkOpened }) { const [status, setStatus] = useState('idle'); // The tab bar pins under the navigation bar and tells the application's own // toolbar how far down to pin (style.css, "Sticky chrome"). @@ -313,12 +316,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, }); }, []); + // Which group `entries` is the index of. The page is not remounted between + // groups, so for one render after a switch `status` and `entries` are still + // the last group's — and a link into the new one must not be looked up there. + const indexGroupRef = useRef(null); + // One place that takes an index from the node and puts it everywhere it has to // go. Deleting a file used to refresh the table and leave the cache alone, so // the search page went on offering a file that no longer existed until the // group was reconnected. const applyIndex = useCallback((indexMsg) => { const fresh = indexMsg.entries || []; + indexGroupRef.current = groupId; setEntries(fresh); if (indexMsg.dirs) setNodeDirs(indexMsg.dirs); if (indexMsg.roots) setNodeRoots(indexMsg.roots); @@ -731,6 +740,36 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, await downloadEntry(transfers, transport, gekRef.current, entry); }, []); + // A link that named a path inside the group (`#/name@owner/root/dir/file`, + // group-link.js): a file is downloaded, and Files opens on its folder either + // way. Acted on once the index is in — before that, nothing can say whether + // the path is a file, a folder or nothing — and then handed back to the + // router, which takes the path out of the address so a reload does not + // download it again. + const [openDirectory, setOpenDirectory] = useState(null); + useEffect(() => { setOpenDirectory(null); }, [groupId]); + useEffect(() => { + if (!openPath || status !== 'connected' || indexGroupRef.current !== groupId) return; + const target = resolveLinkedPath(entries, nodeDirs, openPath); + if (!target) { + setError(t('group.link_path_unknown', { path: openPath })); + } else { + const dir = target.kind === 'file' ? (target.entry.path || '') : target.dir; + setOpenDirectory({ dir }); + chooseTab('files'); + if (target.kind === 'file') downloadFileForModal(target.entry); + } + if (onLinkOpened) onLinkOpened(); + }, [openPath, status, groupId]); + + // The link "Copy link" puts on the clipboard, for an entry or a folder path + // (group-link.js). Null until the hub's row for the group is in: the handle + // is its name and its owner's, and the node knows neither. + const linkFor = useCallback((target) => { + if (!group || !group.name || !group.owner_username || !target) return null; + return groupItemLink(group, typeof target === 'string' ? target : entryPath(target)); + }, [group]); + const refreshIndex = useCallback(async () => { const transport = transportRef.current; if (!transport || !transport.connected) return; @@ -861,7 +900,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, entries, availableEntries, nodeDirs, nodeRoots, setEntries, setNodeDirs, setNodeRoots, applyIndex, isNodeAdmin, operatorPaired, attachRoot, attachDir, userId, setError, onPreview, - onRefreshIndex: refreshIndex, onActivity: touchActivity, + onRefreshIndex: refreshIndex, onActivity: touchActivity, linkFor, // Plural everywhere, and built from the registry rather than a list of app // names kept here: Videos and Music read a list, Photos always did, and an // application added to `APPS` gets its own entry without this file @@ -987,7 +1026,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, </div> ${apps.map(a => tab === a.key && html` - <${a.Component} key=${a.key + '-' + groupId} ...${commonProps} /> + <${a.Component} key=${a.key + '-' + groupId} ...${commonProps} + ...${a.key === 'files' ? { openDirectory } : {}} /> `)} ${tab === 'settings' && html` @@ -1029,7 +1069,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, transportRef=${transportRef} gekRef=${gekRef} onClose=${() => setPreviewEntry(null)} - onDownload=${() => downloadFileForModal(previewEntry)} /> + onDownload=${() => downloadFileForModal(previewEntry)} + onCopyLink=${linkFor(previewEntry) && (() => copyLink(linkFor(previewEntry)))} /> `} ${videoEntry && html` <${VideoPlayer} @@ -1037,7 +1078,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, transportRef=${transportRef} gekRef=${gekRef} onClose=${() => setVideoEntry(null)} - onDownload=${() => downloadFileForModal(videoEntry)} /> + onDownload=${() => downloadFileForModal(videoEntry)} + onCopyLink=${linkFor(videoEntry) && (() => copyLink(linkFor(videoEntry)))} /> `} </div> `; |