Skip to content

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

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

feat(viewer): drop the bundled viewer app for @nextcloud/viewer#63954
skjnldsv wants to merge 10 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

This comment was marked as resolved.

@skjnldsv

skjnldsv commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Tracking issue for all of this, including the apps outside the organisation: nextcloud-libraries/nextcloud-viewer#17

The viewer's own end-to-end suite lived in its repository and ran
against a server it provisioned itself. It belongs here: what it covers
is the viewer opening files out of the Files list, out of a public
share, and out of a folder, none of which the library can exercise on
its own.

The specs run on the server's Playwright harness, with a ViewerPage
section and a fixture for it, and the harness gained a port override so
several checkouts can run at once.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Which mimes the server can render a preview for decides what a client
offers to open: heic, heif and tiff are viewable through a preview and
not otherwise. That list was an initial state the viewer app provided
on every page, so anything wanting it had to be on a page that app had
touched.

As a capability it is where clients already look, public share pages
included, and no app has to provide it. Reading it costs nothing: the
preview manager registers closures and instantiates no provider, and it
reports nothing at all when previews are turned off.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
The viewer arrives as @nextcloud/viewer now rather than as an app of its
own, and something has to put it on the page. The files app is the one
that owns file actions, which is what a viewer handler registers, so it
takes the job over from the viewer app's own listener, on the same event
and with the same reach: every page a file can be opened from, not only
the Files list.

The script does nothing but import the package, which registers the
handlers for images, video and audio. The viewer itself is fetched when
a file is opened.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Viewer 7.0.0 dropped the OCA.Viewer global. Three callers were still on
it: the versions sidebar, the "Open online" button of the open locally
dialog, and the pdf link in the admin settings.

The new API takes nodes, so 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; the open-locally action
passes the node and the folder listing it already has; and the settings
link looks its file up over dav. Whether to offer the "View" button is
canView() from the package, the same test the click-to-open action uses.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Nothing needs it any more: the handlers come from @nextcloud/viewer, the
files app 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>
The viewer is loaded from core rather than from the files app: it
affects every page a file can be opened from, not only the Files list,
so the listener and the init script belong there.

The init script calls registerDefaultHandlers() rather than relying on
the side effect of importing the package.

@nextcloud/image-editor and @skjnldsv/vue-plyr were left behind by the
move to the package, which brings both itself. Nothing in the server
imports either.

The media fixtures join the other test data in tests/data/media, with
their license annotations, and the playwright server timeout drops to
the five minutes CI uses rather than fifteen.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
The init script was in the legacy webpack build, which is where the
files app lives, but that build is vue 2.7 and the viewer library is
vue 3. It only worked because the library sits in the root node_modules
and resolution walked up to vue 3 there rather than to the 2.7 in
build/frontend-legacy. Aliasing vue in that build would have broken it.

It builds with the vite frontend now, where the viewer app's own entry
used to be, referenced by relative path: a `core` symlink next to the
`apps` ones makes eslint resolve a different config for the whole run
and reports 122 errors in files nothing here touches.

camelcase, dompurify and vue-material-design-icons came in with the
vendored viewer and are already declared, at the same versions, in
build/frontend-legacy where the code using them is built. Master has
none of the three at the root, so this drops them and the only
dependency this adds is @nextcloud/viewer.

The event listener registration in the files app went with the class
that moved to core, which psalm caught:

    UndefinedClass: Class, interface or enum named
    OCA\Files\Listener\LoadViewerListener does not exist

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
php-cs, rector-summary and strict all fail on the same one line diff:
the OC\Template\LoadViewerListener import went next to the other
listeners rather than into its own namespace group.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
@skjnldsv

skjnldsv commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

object storage failures unrelated

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants