aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
diff options
context:
space:
mode:
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.js52
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>
`;