aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-10 13:13:57 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-10 13:13:57 +0200
commit914d2f0af848fb80d70d517901f44420a4e47764 (patch)
treee264c669df7311df941c0e935dedf71c892eeed6 /packages/meshbay-hub/src/meshbay_hub/static
parenta0793dd37030f7213ecb07a018b11e6a8adf9238 (diff)
downloadmeshbay-914d2f0af848fb80d70d517901f44420a4e47764.tar.gz
feat(hub): delete a photo from the Photos app
On a right-clicked tile and in the lightbox bar, after a confirmation, for the node's operator or the photo's uploader, as in Files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photos-app.js57
1 files changed, 49 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js b/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
index afc815a..f18acd2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
@@ -12,6 +12,7 @@ import { SourceTag } from './group-name.js';
import { CastButton, castSession, useCastDevice, showPhoto } from './cast-session.js';
import { Menu, useMenu } from './menu.js';
import { copyLink } from './copy-link.js';
+import { ask } from './ask.js';
// ── Photos ───────────────────────────────────────────────────────────────────
//
@@ -155,7 +156,7 @@ const ZOOM_MAX = 400;
// a slow node must not skip a photo nobody saw.
const SLIDESHOW_MS = 5000;
-function Lightbox({ photos, index, transportRef, gekRef, onClose, onNav, linkFor }) {
+function Lightbox({ photos, index, transportRef, gekRef, onClose, onNav, linkFor, onDelete }) {
const entry = photos[index];
const tRef = entry._tRef || transportRef;
const gRef = entry._gRef || gekRef;
@@ -290,6 +291,10 @@ function Lightbox({ photos, index, transportRef, gekRef, onClose, onNav, linkFor
<button class="video-close" onClick=${() => copyLink(linkFor(entry))}
title=${t('link.copy')} aria-label=${t('link.copy')}>
<${Icon} name="link" /></button>`}
+ ${onDelete && onDelete(entry) && html`
+ <button class="video-close" onClick=${onDelete(entry)}
+ title=${t('group.delete')} aria-label=${t('group.delete')}>
+ <${Icon} name="trash" /></button>`}
<button class="video-close" onClick=${onClose} title=${t('video.close')}>
<${Icon} name="close" /></button>
</div>
@@ -317,17 +322,31 @@ function Lightbox({ photos, index, transportRef, gekRef, onClose, onNav, linkFor
`;
}
-function AlbumView({ album, entries, transportRef, gekRef, setError, onBack, readOnly, linkFor }) {
+function AlbumView({
+ album, entries, transportRef, gekRef, setError, onBack, readOnly, linkFor, onDelete,
+}) {
const [lightboxIndex, setLightboxIndex] = useState(null);
- // Right-click a photo for its link. A tile has no dots to press, so on a
- // touchscreen the link is in the lightbox's bar instead.
+ // Right-click a photo for its link, or to delete it. A tile has no dots to
+ // press, so on a touchscreen both are in the lightbox's bar instead.
const { menu, openAt, close: closeMenu } = useMenu();
const onTileMenu = (e, entry) => {
const link = linkFor ? linkFor(entry) : null;
- if (!link) return;
- openAt(e, [{ label: t('link.copy'), icon: 'link', onSelect: () => copyLink(link) }]);
+ const remove = onDelete ? onDelete(entry) : null;
+ const items = [
+ link && { label: t('link.copy'), icon: 'link', onSelect: () => copyLink(link) },
+ remove && { label: t('group.delete'), icon: 'trash', danger: true, onSelect: remove },
+ ].filter(Boolean);
+ if (!items.length) return;
+ openAt(e, items);
};
+ // A photo deleted from under the lightbox: show its neighbour, or close
+ // when it was the album's last.
+ const count = album.photos.length;
+ useEffect(() => {
+ setLightboxIndex((i) => (i === null || i < count ? i : (count ? count - 1 : null)));
+ }, [count]);
+
const zip = useCallback(async () => {
const transport = transportRef.current;
await downloadDirectory(
@@ -373,7 +392,8 @@ function AlbumView({ album, entries, transportRef, gekRef, setError, onBack, rea
${lightboxIndex !== null && html`
<${Lightbox} photos=${album.photos} index=${lightboxIndex}
transportRef=${transportRef} gekRef=${gekRef}
- onClose=${() => setLightboxIndex(null)} onNav=${navigate} linkFor=${linkFor} />
+ onClose=${() => setLightboxIndex(null)} onNav=${navigate} linkFor=${linkFor}
+ onDelete=${onDelete} />
`}
${menu && html`<${Menu} ...${menu} onClose=${closeMenu} />`}
`;
@@ -385,6 +405,7 @@ function PhotosApp({
groupId, transportRef, gekRef, status, entries, availableEntries,
photoDirectories, setError,
hideFilter, readOnly, linkFor,
+ isNodeAdmin, userId, applyIndex,
}) {
const [openDir, setOpenDir] = useState(null);
const [filter, setFilter] = useState('');
@@ -402,6 +423,25 @@ function PhotosApp({
const openAlbum = openDir != null ? albums.find((a) => a.dir === openDir) : null;
+ // The rule Files applies, and the node enforces: the node's operator, or
+ // whoever uploaded the photo. Null for a photo this member may not delete,
+ // otherwise what deletes it once the person has said yes.
+ const deleteFor = (entry) => {
+ if (readOnly || status !== 'connected' || !applyIndex) return null;
+ if (!isNodeAdmin && !(userId && entry.uploader_id === userId)) return null;
+ return async () => {
+ if (!await ask(t('group.delete_confirm', { name: entry.name }))) return;
+ const transport = transportRef.current;
+ if (!transport || !transport.connected) return;
+ try {
+ await transport.deleteFile(entry.id, transport.signFn);
+ applyIndex(await transport.fetchIndex());
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+ };
+
return html`
${(status === 'discovering' || status === 'connecting' || status === 'fetching') && html`
<p class="page-message"><span class="spinner"></span>${' '}${t('status.connecting_short')}</p>
@@ -438,7 +478,8 @@ function PhotosApp({
${status === 'connected' && openAlbum && html`
<${AlbumView} album=${openAlbum} entries=${entries}
transportRef=${transportRef} gekRef=${gekRef} setError=${setError}
- onBack=${() => setOpenDir(null)} readOnly=${readOnly} linkFor=${linkFor} />
+ onBack=${() => setOpenDir(null)} readOnly=${readOnly} linkFor=${linkFor}
+ onDelete=${deleteFor} />
`}
`;
}