Skip to content

chore: 2.0.0-beta.5 - #5

Closed
skjnldsv wants to merge 22 commits into
mainfrom
chore/release-2.0.0-beta.5
Closed

chore: 2.0.0-beta.5#5
skjnldsv wants to merge 22 commits into
mainfrom
chore/release-2.0.0-beta.5

Conversation

@skjnldsv

@skjnldsv skjnldsv commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Version bump and changelog for the next beta. Merge after #3 and #4 — it is the release of those two, and means nothing without them.

What it carries:

Nothing is published by merging this. The npm publish workflow fires on a release being published, so once this lands I can promote the draft release and the next tag moves to beta.5.

Once it is out the server PR wants repointing at it; beta.4 is what nextcloud/server#63954 currently resolves.

👾 This pull request was assisted by Claude Code, commits carry an Assisted-by trailer.

Nextcloud added 22 commits September 8, 2026 19:58
Build, tests, typecheck, linting, licensing and the docs deployment,
taken from nextcloud-image-editor so the two libraries are set up the
same way. No Playwright here: the viewer's browser tests exercise it
inside the server, and that is where they stay.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Converted from the compiled catalogues the server app shipped, so no
existing translation is lost in the move: 97 locales, 2254 translated
strings. Only the strings the code actually uses are kept — the server
catalogues also carried the app store description, which is appinfo's
and stays behind.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
The viewer app in nextcloud/server keeps its Vue implementation in
apps/viewer/src and publishes only its API bindings, which leaves apps
outside the server tree unable to depend on either. All of it lives here
now, and the server consumes it like any other package.

The port is faithful — the 159 unit and component tests come across
unchanged and still pass — with the changes a library needs and an app
does not:

- Translations are the package's own, through the gettext bundle, rather
  than the server's app-scoped catalogue under the 'viewer' id.
- The blank video plyr needs is bundled instead of fetched from
  /apps/viewer/img, which only resolves when the server app serves it.
- Fonts left over from the in-tree image editor are dropped: that editor
  is @nextcloud/image-editor now and brings its own.

The Playwright specs stay in the server. They exercise the viewer inside
the Files list, public shares and the sidebar, which is integration with
the server rather than anything this package can test on its own.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Several apps on a page can each bundle this library, at different
versions, and they still have to share one viewer: it is a modal on the
body that owns the history entries, the focus trap and the keyboard
handling, and two of them fighting over those is a bug you can see.

Importing the package no longer loads a viewer, then. It registers a
candidate, which costs the version and a closure. The first time
something opens a file the newest candidate is loaded and mounted, once,
and every other copy delegates to it. Losing copies never fetch their
implementation chunk at all.

Newest rather than first to register: script order across apps is not
something anyone controls, so electing the first to arrive would make
the viewer behave differently depending on which app's bundle happened
to load first.

The registry moves off window._oca_viewer_handlers into a scope keyed by
handler ABI, window._nc_viewer_scope.handlers_v1. Keying it by package
version would split the registry on every major, and with a single
viewer a split registry means handlers that quietly never open.

Registering a handler is now cheap: defining the custom elements is what
pulls the components in, and that happens as the viewer mounts, so the
entry no longer carries them.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Carries over the integration docs that were in the server's api_package
README — registering a handler, opening the viewer, the list ordering
contract and the migration table off OCA.Viewer — and adds what is new
here: how a page ends up with one viewer, and why the scope is keyed by
handler ABI rather than by package version.

The screenshot shows the same photo the image editor's does.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Copies within a major are compatible and the newest simply wins, so
there is nothing to say. Different majors are worth a word: only one of
them runs, and the app that pinned the other expects behaviour the
elected copy may not have.

Covering the election brought out a real bug in the version comparison
while writing the tests for it: prerelease identifiers were compared as
text, so 2.0.0-beta.10 sorted below 2.0.0-beta.2. They follow semver
now, numeric identifiers compared as numbers.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
getViewer() built it on first call, so until something opened a file
there was nothing on the scope to hold a reference to, and code reaching
for the service before the first open found nothing there. It is an
empty shell until the viewer mounts, so building it early costs a
constructor.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Two apps on the same major share a viewer and the highest of them is the
one that loads. Separate majors only get a viewer each when the handler
ABI itself changed, which is what moves the scope key.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Per-chunk CSS leaves it to the consuming bundler to inject a chunk's
stylesheet when that chunk loads, and that is not something a library
can count on. In the server it did not happen, so the image editor
opened with none of its own styles: present, visible, and zero pixels
tall.

One stylesheet, exported as @nextcloud/viewer/style for the consumer to
import, which is how @nextcloud/image-editor does it and what this
library already does when consuming that one.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Emitting stylesheets leaves it to the consuming bundler to link a
chunk's CSS when that chunk loads, and a library cannot count on that.
In the server it did not happen: the image editor opened present,
visible and zero pixels tall, with none of its own styles.

Each chunk now injects its own styles as it runs, so the lazily loaded
viewer brings what it needs with it and consumers import nothing. The
style export is gone with the stylesheet it pointed at.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
The deploy needs Pages enabled on the repository, which is not something
a push can do, so the first run after enabling it has nothing to retry
from without another commit.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
The viewer talks to WebDAV and to the previews endpoint, and a page with
neither was the open question behind the end-to-end tests staying in the
server. It is not a problem: a Files node whose source is a plain URL is
supported outright, and with hasPreview left off the viewer loads that
URL directly. Everything else it reaches for is guarded — loadState has
a default, the Files router is optional, and the public-share helpers
are only called for files that have a preview.

So the playground serves its fixtures from the dev server and the viewer
opens them with nothing behind it. It doubles as the demo page the docs
now link to.

First specs to move: navigation, and one pinning down that paging
follows the handler group rather than the whole list, which is why an
image list stops at the images.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
The backdrop followed the user's theme: --background-invert-if-dark is
'no' in the default theme, so a light theme gave a light viewer. A photo
or a video reads better against dark, and the room around it should not
compete with it, so the viewer is dark and does not ask.

A handler showing something other than media can still ask for a light
backdrop; that is what the theme option is for. Nothing built in does.

Covered in the playground, which carries the server's default theming
and is therefore the light theme.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
The rewrite reduced the edit gate to the handler's canEdit flag. The
viewer it replaced also checked the file:

    canEdit() {
      return !this.isMobile
        && this.canDownload
        && this.currentFile?.permissions?.includes('W')
        && this.isImage
        ...

So the Edit button was being offered on read-only files, and an
editing=true URL opened the editor on one. Both go through the write
permission now, alongside the handler's own flag.

The playground serves its fixtures from a WebDAV-shaped path as part of
this. @nextcloud/files gives a node it considers foreign no owner and
read-only permissions whatever it was constructed with, so every file
there was read-only and the writable case could not be represented at
all. One fixture still uses a plain URL, which is what the viewer is
able to open in the first place.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Found by reading the viewer this one replaced side by side with it.

The context menu is refused again over a file that may not be
downloaded. Hiding the download control was never the whole job: the
file is on screen, and the browser's own menu offers to save it, so a
share that forbids downloading was a right click away from being
worked around.

The page title says which file is being looked at again, and gives the
page its own title back on close. It follows navigation too, so paging
through a folder retitles the tab.

Full screen is back in the menu, which is most of the point of a viewer
on a large photo. Leaving the viewer leaves full screen with it.

Loading a file is dropped when the viewer moves to another one. Only a
different file supersedes what is in flight: reloading the same one, on
a retry or a resize, must not cancel the request already serving it.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
openFolder() sorted by name because I claimed there was no active sort
to read in that path. There is: the viewer this one replaced fetched it
from the files list and sorted by it.

    this.sortingConfig = await getSortingConfig()
    sortNodes(nodes, {
      sortingMode: this.sortingConfig.key,
      sortingOrder: this.sortingConfig.asc ? 'asc' : 'desc',
    })

So a user with their files sorted by date opened a folder and paged
through it by name, which is the bug this was supposed to fix. It reads
the same setting again now. A public share has none, and neither does a
request that fails: both fall back to names ascending, as the files list
does.

Relates to nextcloud/viewer#3027

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
The action the rewrite replaced checked the file before offering to
open it; this one checked the handler and the node type only. A file
with no read permission was still offered.

Deleted files stay openable, which is what people have asked for: the
trashbin reports them as GD, readable and deletable, and already points
at its own preview endpoint. A node that is not dav-backed always
reports readable, so nothing outside a server changes either.

Also covers what was shipped untested: canDownload, which decides
whether the context menu is refused, and the page title and full screen
restored alongside it.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
The Files app opens the same file more than once: once for the click,
and again as the sidebar opens. Every open reset the viewer to loading
and waited for the handler to say it had finished — but the handler
still had the same file, its image was already loaded, and nothing was
going to load again. The spinner stayed up over a picture that was
sitting there fully rendered.

Opening what is already open is no longer a new load. A different file,
or a comparison, still is.

This is what kept the server's sidebar-while-loading test red; the whole
viewer suite passes there now. The viewer it replaced had an isSameFile
notion for the same reason, which the rewrite dropped.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
The server has moved to ^1.0.0-beta.2, and pinning ^0.4.0 made this
package refuse to install there:

    Could not resolve dependency:
    peer @nextcloud/sharing@"^0.4.0" from @nextcloud/viewer@2.0.0-beta.2

The two functions the viewer uses from it, isPublicShare and
getSharingToken, have the same signatures in 0.4 and 1.x, so both are
accepted rather than moving the floor.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
window.__vd was added while tracking down the spinner that stayed up
after a file was reopened, and I removed the other half of that
instrumentation but not this. It went out in beta.2 and beta.3, exposing
the viewer's loading state on the window object.

The fileinfo utils go with it. genFileInfo turned a WebDAV response into
the object shape OCA.Viewer.open({ fileinfo }) took, and nothing has
called it since the viewer started working in Files nodes throughout:
no reference to any of them outside their own tests. numberUtil existed
only to serve genFileInfo.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Nextcloud <info@nextcloud.com>
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.89%. Comparing base (4d142b2) to head (8868581).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main       #5   +/-   ##
=======================================
  Coverage   69.89%   69.89%           
=======================================
  Files          32       32           
  Lines        2259     2259           
  Branches      372      372           
=======================================
  Hits         1579     1579           
  Misses        674      674           
  Partials        6        6           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@skjnldsv skjnldsv self-assigned this Sep 9, 2026
@skjnldsv skjnldsv added status: review Waiting for reviews status: developing Work in progress type: refactor ♻️ Refactor code (not a bug fix, not a feature just refactoring) and removed status: review Waiting for reviews type: refactor ♻️ Refactor code (not a bug fix, not a feature just refactoring) labels Sep 9, 2026
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.

1 participant