Skip to content

feat(viewer): drop the bundled viewer app for @nextcloud/viewer - #63954

Open
skjnldsv wants to merge 9 commits into
masterfrom
feat/merge-viewer
Open

skjnldsv wants to merge 9 commits into
masterfrom
feat/merge-viewer

Conversation

@skjnldsv

@skjnldsv skjnldsv commented Sep 2, 2026

Copy link
Copy Markdown
Member

Summary

The viewer used to be a bundled app that every other app had to check for before
it could register a handler. It is a library now — @nextcloud/viewer, in
nextcloud-libraries/nextcloud-viewer
and this drops the app.

An app that wants to add a file view imports the package and registers its
handler. It no longer matters whether the viewer app is enabled, or which app
loaded it first: several copies on a page elect the newest between them, and only
that copy fetches the viewer, only when a file is opened.

What is left in the server

apps/files puts a thirteen line init script on every page a file can be opened
from, on the same event the viewer app's own listener used. The script does
nothing but import the package; the handlers for images, video and audio come
with it. dist/files-viewer-init.js is 24 kB, 8.3 kB gzipped.

LoadViewer goes with the app. Three places dispatched it behind a
class_exists() guard to ask the app to load itself; none of them need to now.

enabled_preview_providers becomes a core capability

Which mimes have a preview provider decides what the viewer offers to open — heic,
heif and tiff are viewable through a preview and not otherwise. That was an
initial state the viewer app provided, so anything wanting it had to be on a page
that app had touched. It is core.previews.enabled_providers now, next to the
capabilities clients already read, public share pages included.

"core": {
    "previews": {
        "enabled_providers": ["/image\\/png/", "/image\\/jpeg/", ""]
    }
}

PreviewManager::getProviders() registers closures and instantiates no provider,
so this costs nothing per request, and it reports nothing when previews are off.

OCA.Viewer is gone, and three callers were still on it

Viewer 7.0.0 dropped the global. These do nothing on current master:

  • the versions sidebar — view, compare, and whether to offer either
  • the "Open online" button of the open-locally dialog
  • the pdf link in the admin settings

They take nodes now. The versions sidebar builds one for the version it shows,
pointing at the version's own dav endpoint rather than at the 250px thumbnail
meant for the list; canView() from the package decides whether to offer the
button, which is the same test the click-to-open action uses.

Third-party apps calling OCA.Viewer.open() — Photos, Talk, Deck, Mail,
Collectives — need to move to the package. There is no shim.

Testing

  • tests/playwright/e2e/viewer/, which comes over from the viewer's own
    repository in the first commit: 46 passed, 1 skipped
  • frontend unit tests: 198 (vue3) and 818 (legacy), including new specs for
    versionToNode and the capability
  • tests/lib/OCS/CoreCapabilitiesTest.php, tests/lib/AppTest.php,
    tests/lib/App/AppManagerTest.php

Upgrades: viewer joins the abandoned apps CleanUpAbandonedApps already
cleans the config rows for.

TODO

  • Manual check of the versions compare view and the settings pdf link on a live instance
  • Before/after screenshots for the versions sidebar
  • Developer documentation: the manual still describes the OCA.Viewer API and the LoadViewer event
  • Coordinate with Photos, Talk, Deck, Mail and Collectives before this lands

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@skjnldsv skjnldsv self-assigned this Sep 2, 2026
@skjnldsv skjnldsv added enhancement 2. developing Work in progress technical debt 🧱 🤔🚀 labels Sep 2, 2026
@skjnldsv skjnldsv added this to the Nextcloud 36 milestone Sep 2, 2026
@skjnldsv
skjnldsv force-pushed the feat/merge-viewer branch 2 times, most recently from 8018718 to 7a2fb77 Compare September 2, 2026 12:50
Comment thread apps/viewer/test/component/mountViewer.ts Fixed
@skjnldsv

Copy link
Copy Markdown
Member Author

Ready for reviews! 💪 🚀

The suite the viewer app carried in Cypress, ported to Playwright and to
the fixtures the server harness already sets up: opening every media
type, navigating between files, sorting, sharing, odd names, and the
actions around them.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2.0.0-beta.9, which carries the viewer and its handlers. The entry is
built with the Vue 3 frontend rather than the legacy one.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
The viewer only offers a mime its handler can actually show, and for
HEIC, HEIF, TIFF and friends that means the server has to render a
preview. The app read the enabled providers from an initial state it
provided itself; a package cannot, so core reports them under
capabilities.previews.enabled_providers, the same array_keys() the app
used.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
A file link is not only on the Files list: an app can put one on any
page, and the old LoadViewer event existed so those pages could ask the
app to load itself. Core puts the init script on every rendered page
instead, the themed error page aside, and the script only registers the
handlers: nothing of the viewer is fetched until a file is opened.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
The global was the app's own API and goes with it. The versions sidebar,
the local-edit action and the settings PDF link ask the package instead.

Comparison stays with the pictures it means something for: the old
viewer asked each handler through a canCompare flag that none of the
bundled ones set, and the package carries no such flag yet.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Nothing needs it any more: the handlers come from @nextcloud/viewer, core
puts the init script on every page, and the enabled preview providers are
a core capability.

With it goes the LoadViewer event, which three places dispatched behind a
class_exists() guard to ask the app to load itself. An app that wants a
file view imports the package and registers a handler; it no longer
matters whether the viewer app is enabled, or which app got there first.

The app config rows left behind on an upgrade are cleaned up by the
repair step that already does this for the other apps we dropped.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
It carries the close fix: the viewer no longer leaves openfile=true in
the URL while the history unwinds, which had the Files list opening a
second viewer over the one that was closing.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Three of them asked a question whose answer changes under them:

waitForClosed asked whether the first matching modal was hidden, which
passes while a second one is still up. It counts them now, and close()
waits for openfile to leave the URL before looking.

runAction decided between a header button and a menu entry with one
isVisible() call. The header re-renders whenever the shown file
changes, so on the second delete it found neither and then waited for a
menu it had not opened. The sequence is retried as a unit.

The sidebar test asserted the loading spinner was on screen. The list
fetches the preview for its own row and the viewer reuses it, so
delaying the viewer's request holds nothing back: the spinner measured
about 600ms, which passed on CI and failed here. It asserts the action
it exists for instead.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
The old suite had two specs for it and the port dropped both: a viewer
opened on a list of its own rather than a folder, which is the API
Photos and the versions sidebar use, and loadMore continuing that list
when navigation runs out.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants