feat: motion and polish across the timeline - #2074
Merged
Merged
Conversation
Blocking and muting could only be done from an account's profile, and there was no way to see what you had blocked: `/api/v1/blocks` and `/api/v1/mutes` answered correctly but nothing in the web client ever called them, so undoing a block meant remembering who it was. Settings → Blocked and muted accounts now lists both, with unblock and unmute inline. Unblocking goes through the store action the profile page uses, so a relationship stays in step wherever it is shown, and a row only disappears once the server has confirmed — a refusal leaves it where it was rather than claiming success. The page paths of the client-side router also needed server routes: /blocked, and /follow_requests which had the same defect already — reloading or bookmarking either was a 404. They both render the app shell now. The route names carry a postfix because routes are keyed by name, and re-using one silently drops every declaration but the last. Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com> Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com>
'Reset local cache' posted to /api/v1/cache/refresh, a route that does not exist and never has: the request 404s, nothing catches it, so the button reported nothing and did nothing. It arrived with the v0.9.3 design refresh without a backend to talk to. 'Help & documentation' pointed at a personal fork rather than any documentation of this app. That leaves the settings section with the one entry that does something. Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com> Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com>
The app runs on Vue 3.5 but carried Vue 2 idioms that the compiler accepts and then ignores, so each one silently did nothing: - The timeline's fade was dead. Both <transition-group>s use the `list` transition, whose starting class was `.list-enter` — Vue 2's name. Vue 3 applies `.list-enter-from`, which nothing defined, so posts appeared abruptly. Corrected, given the `.list-move` that makes the neighbours slide instead of jump when an entry arrives, and defined once in the global block rather than duplicated in a scoped one. - The welcome banner animated out but never in: `slide-fade` had no enter classes at all. - The report dialog could not close itself. `:open.sync` is Vue 2; Vue 3 ignores the modifier, so the binding was one-way and the dialog's own dismissal never reached the flag. Now `v-model:open`. - eslint was linting a Vue 3 app against the Vue 2 ruleset. The shared config ships a `vue3` sub-config for exactly this; switching to it rejected nothing valid and immediately found four unused bindings, including an unused linkifyjs namespace import. - Five components emitted events without declaring `emits`. - Motion now respects prefers-reduced-motion everywhere, which nothing did before. - Deleted the popoverMenu mixin, unused since NcActions replaced hand-rolled menus, and the App-level composer path that Navigation stopped emitting when it grew its own modal. tests/js/vue3.test.js asserts all of it against the source, because none of these fail loudly: it reports the file and line of a Vue 2 transition class, a `.sync` binding, a removed lifecycle hook or API, an undeclared emit, or an animation without a motion guard. Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com> Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com>
The app had four opacity transitions and no keyframes at all. This gives the surfaces people touch most some feedback, in plain CSS — no animation library, nothing added to the bundle but the rules themselves. - Liking a post pops the heart and sends a ring out of it, once, and boosting spins the icon. Undoing neither: there is nothing to celebrate about taking a like back. - Both actions are applied optimistically and were rolled back in silence when the server refused. The button now shakes, so a refusal is something you see rather than something you miss. - The first page of a timeline is three post-shaped placeholders with a travelling sheen instead of a spinner on an empty page, so the timeline settles into its own shape. Later pages keep the spinner: the posts are already on screen. - Poll results grow from zero, one bar after another. - Images resolve out of the blurhash the app already decodes and then threw away in a single frame — it now fades under the image. - Posts that arrive while you are further down the page offer a pill instead of appearing unannounced above the fold. - The composer shows the length allowance as a ring that fills, turning amber near the limit and counting down past it, rather than a limit discovered by hitting it. - Post cards lift slightly under the pointer and take a focus ring. Every one of these is behind prefers-reduced-motion, and the guard test now insists on it for `animation` as well as `transition`. TimelineList also asks for its own translate functions rather than leaning on the implicit globals, which is how the components that were already testable do it — the global `n` does not interpolate. Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com> Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com>
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacks on #2073 — branched off it, because the
prefers-reduced-motionguard test belongs underneath this. Merge that one first.The app had four
transition: opacitydeclarations and zero keyframes. This gives the surfaces people actually touch some feedback, in plain CSS — no animation library, nothing added to the entrypoints but the rules themselves (they compile into the lazy chunks their components already live in).What changes
Undoing a like or a boost deliberately gets no celebration — there's nothing to celebrate about taking a like back.
The two that are more than decoration
The refusal shake.
postLike/postBoostapply the change optimistically and roll it back when the server says no, which until now happened invisibly — the icon just flipped back. Making that visible is motion doing work.Skeletons over a spinner. A spinner on an empty page says nothing about what is coming; placeholders in the shape of posts mean the timeline settles into itself. Note this also fixed a real bug I introduced and caught mid-change: the "more to load" marker was briefly rendering during the first load.
Constraints held
prefers-reduced-motion, and the guard test from feat: manage blocked and muted accounts, and drop the settings entries that never worked #2073 now insists on it foranimationas well astransition— verified by deleting one guard and watching it name the file.--color-primary-element,--color-warning,--border-radius-pill), so it themes with the rest of the suite rather than against it.Tests
7 new specs: the like burst appearing only when liking, the refusal class on a rejected action, skeletons on the first page and a spinner on later ones, the arrival pill showing only when scrolled away, the pill scrolling to top and clearing, the blurhash crossfade classes, and the composer ring's fill/warning/over states. Suites: 640 vitest, 1839 PHP, eslint (Vue 3 config) and stylelint clean.
Two harness fixes fell out:
mountPost's dispatch used to resolveundefined, which is exactly what the store returns on failure, so success and refusal were indistinguishable in tests; andTimelineListnow imports its translate functions rather than leaning on the implicit globals, because the globalndoes not interpolate%n— the same reason the better-tested components here already import them.🤖 Generated with Claude Code