diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-10 13:13:57 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-10 13:13:57 +0200 |
| commit | 914d2f0af848fb80d70d517901f44420a4e47764 (patch) | |
| tree | e264c669df7311df941c0e935dedf71c892eeed6 /packages/meshbay-hub/src/meshbay_hub/static | |
| parent | a0793dd37030f7213ecb07a018b11e6a8adf9238 (diff) | |
| download | meshbay-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.js | 57 |
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} /> `} `; } |