Sensitive Media Lightbox#
The Misskey-native lightbox is a fullscreen image/video viewer built entirely in Vue. It replaced the third-party PhotoSwipe dependency in PR #17687 (merged 2026-07-12), motivated by UX improvements, deeper integration with Misskey's UI, reduced implementation cost, and better performance.
The viewer is composed of four layers:
| Layer | Component | Responsibility |
|---|---|---|
| Timeline grid | MkMediaList.vue | Renders the media grid; owns the Content[] list; opens the lightbox |
| Per-image thumbnail | MkMediaImage.vue | Manages in-grid sensitive-hide state; emits mediaClick |
| Lightbox container | MkLightbox.vue | Full-screen overlay; handles multi-item slide navigation |
| Lightbox item | MkLightbox.item.vue | Per-media pane: gesture input, zoom, hide state, open/close animation |
The Content Type Contract#
MkLightbox.item.vue exports the Content interface — the sole data contract between MkMediaList and the lightbox:
export type Content = {
id: string;
type: 'image' | 'video';
url: string;
thumbnailUrl?: string | null;
width?: number | null;
height?: number | null;
filename?: string | null;
file?: Misskey.entities.DriveFile; // full DriveFile for menus / sensitivity checks
sourceElement?: HTMLElement | null; // the <img> in the grid, for open/close animation
};
MkMediaList assembles this array and opens the lightbox via os.popupAsyncWithDialog . The sourceElement is looked up by a data-marker attribute stamped on each grid <img> and is used by MkLightbox.item to compute the expand/collapse transform animation .
Sensitive-Hide State#
Sensitive-hide state is managed independently in two places: the timeline grid thumbnail and the open lightbox item.
In the timeline grid (MkMediaImage)#
MkMediaImage maintains a local hide = ref(true) . On mount and whenever the image prop changes, it calls shouldHideFileByDefault to set the initial value . The watch is deep so plugin-driven note view interruptors can trigger re-evaluation.
When hidden, clicking calls canRevealFile — an async guard that optionally shows a confirmation dialog when prefer.s.confirmWhenRevealingSensitiveMedia is enabled . Only on approval is hide set to false.
When highlightSensitiveMedia is on, a 4 px warning-color inset box-shadow is added via CSS .
In the lightbox (MkLightbox.item)#
Each lightbox item has its own hide = ref(true) . The shouldHideInGallery function applies the same logic with one important exception: the first sensitive image opened in a gallery session is shown immediately (not hidden) as long as prefer.s.nsfw !== 'force' . Subsequent navigation to other sensitive items re-hides them.
The two hide states are independent — revealing an image in the grid does not pre-reveal it in the lightbox, and vice versa.
Utility functions#
Both components share two functions from sensitive-file.ts:
shouldHideFileByDefault(file, ignoreDataSaver?)— returnstruewhennsfw === 'force', data-saver is active, or the file is sensitive andnsfw !== 'ignore'.canRevealFile(file)— async; shows a confirmation dialog iffile.isSensitive && confirmWhenRevealingSensitiveMedia; returnsfalseon cancel .
Lightbox Architecture#
MkLightbox (container)#
MkLightbox receives a Content[] array and a defaultIndex . Items are laid out as a horizontal flex strip; navigation is done by translating the strip with contentsOffset . Arrow-key navigation, Esc-to-close, and left/right click buttons are all wired here .
An activatedIndexes set controls lazy rendering: only items that have ever been the active index receive the full content load . This prevents preloading all media in the array on open.
The sourceElement of the currently active item is hidden (visibility: hidden) in the grid while the lightbox is open, providing the shared-element illusion .
On mount, window.history.pushState(null, '', '#pswp') is called so that the browser back button closes the lightbox . The #pswp hash name is kept for backwards compatibility.
MkLightbox.item (per-media pane)#
Each item handles:
- Open/close animation: if
sourceElementis provided, the item starts at the source's CSS transform and animates toscale(1) translate(0,0). Without a source element it falls back to a fade-in fromscale(0.7). - Gesture routing: single pointer events are routed to vertical-swipe-to-close, horizontal-swipe-to-navigate, or pan-while-zoomed; two-pointer events drive pinch zoom . See the Gesture Detection KB article for full detail.
- Loading strategy: a thumbnail is shown immediately; the full-resolution image/video is loaded only while the item is
activated. - Video control: injects the
<video>element via DI tokenDI.mkLightboxItemVideoElso childMkVideoControlcan access it .
Migration from PhotoSwipe#
PR #17687 removed the photoswipe npm package and its Rollup chunk. Previously, opening any media required constructing specific DOM elements for PhotoSwipe; the native implementation accepts a plain Content[] array instead. The #pswp URL hash is retained so existing popstate listeners continue to work; the hash is cleaned up on page load per PR #17696.
Key improvements delivered by the migration:
- Sensitive-media reveal state is now tracked natively (PhotoSwipe had no concept of hidden media).
- Open/close animations use the actual source
<img>element position rather than PhotoSwipe's separate thumbnail concept. - All gesture code lives in a single file rather than being distributed across library internals.
- PR #17696 added video stop, back-button close, and the per-file popup menu.
- PR #17740 fixed a swipe-to-close regression where vertical swipes could be blocked.
Key Files#
| File | Role |
|---|---|
MkMediaList.vue | Timeline grid; assembles Content[]; calls os.popupAsyncWithDialog to open lightbox |
MkMediaImage.vue | In-grid image tile; owns grid-level hide state |
MkLightbox.vue | Lightbox shell; slide strip, navigation, keyboard shortcuts, z-index |
MkLightbox.item.vue | Per-item pane; exports Content type; gesture handling, zoom, hide state, animations |
sensitive-file.ts | shouldHideFileByDefault + canRevealFile — shared sensitivity utilities |
get-file-menu.ts | Builds context menu (hide/show, sensitivity controls, links) used in both grid and lightbox |
utility/double-tap.ts | makeDoubleTapDetector — double-tap zoom recognizer used by MkLightbox.item |