Feat/UI refresh misc#2547
Open
karlitschek wants to merge 31 commits intomasterfrom
Open
Conversation
Replace the 20px monochrome event icon with the actor's 32px color NcAvatar, and place the event-type icon as a small circular badge in the bottom-right corner. Activities with no actor (system events) keep the legacy event icon as the primary avatar so automated events are not mis-attributed to a real user. Makes the stream feel like "people doing things" instead of "system log entries". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a 3px coloured left border to each activity row, keyed to a small
event family ("created", "deleted", "changed", "share", "comment",
"favorite", "neutral"). Hovering the row shows a subtle background
tint, also via theme variables so it follows light/dark mode.
Makes a long stream visually scannable: the colour bar tells you at
a glance whether you are looking at deletions vs shares vs comments,
without having to read the subject line of every entry.
Unknown types fall through to the neutral bucket so we never paint
a misleading colour on an activity we don't understand.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a small subtitle line under the page heading that summarises what
happened today based on the activities already loaded:
Today: 12 changes, 3 shares, 1 deletion
The buckets ("created", "changed", "deleted", "share", "comment",
"favorite", "other") match the type-family taxonomy used elsewhere so
the visual story is consistent across header and rows.
Computed entirely from the already-loaded `allActivities` ref — no
extra API call. Hides itself when nothing has loaded yet or when
today has no activity, so the line collapses cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two small life-signs improvements to the stream: * On first paint, replace the centred NcLoadingIcon "empty" state with six animated skeleton rows. The user sees a list shape immediately instead of a blank panel with a spinner, which makes the page feel considerably faster even when the underlying request takes the same time. * Wrap the per-day ActivityGroups in a Vue TransitionGroup (220ms fade + slight translate-Y) so freshly-prepended items from the 30-second poll animate in instead of popping into existence. No behavioural changes — purely visual. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bump activity preview thumbnails from 50px to 80px and add a 1.6x scale-up on hover, so users can preview a screenshot or attachment without leaving the stream. The zoom is gated to real previews — MIME-type icons keep the old behaviour so they don't blur when scaled. Position+z-index ensure the zoomed thumbnail floats above neighbouring rows. object-fit: cover keeps non-square previews looking right at the new larger size. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The notification settings page used a 30+ row × 2 column table of
checkboxes that was both visually intimidating and hostile to scan.
Replace it with:
* A "Quick presets" row at the top with three one-click options:
- Mute everything
- Essentials only (push everywhere; email on share/mention/comment)
- Send everything
Implemented by reusing the existing toggleMethodForGroup action so
the save flow batches per-group instead of per-activity.
* One card per activity group (Files / Sharing / …) with:
- Per-method "all" master switches in the card header that show
indeterminate state when the group is partially enabled
- Inline switches per activity row, in line with the row label
instead of stacked into a wide table
Switches replace checkboxes — same behaviour, friendlier affordance.
The component still receives all data from the Vuex store and exposes
the same name and slot, so the existing UserSettings and
DefaultActivitySettings views need no changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a filter bar between the page heading and the activity list with
four controls:
* free-text search over subject + message
* person filter against the activity's user (uid contains)
* From / To date range (inclusive day boundaries)
The OCS v2 endpoint does not yet accept search / date-range / actor
parameters, so filtering runs client-side over whatever the infinite
scroll has already loaded. When a filter is active, a "{n} matching"
badge and a "Reset" button appear; the existing pagination keeps
pulling more activities under the same filter, so the user can dial
in by scrolling.
A natural follow-up is to extend APIv2Controller and Data with a
`q=`, `from=`, `to=` and `actor=` parameter set so this becomes a
true server-side filter — kept as a separate change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a per-user list of saved filter combinations (filter + search + person + date range), persisted to localStorage so pinning works without backend changes. * New `useSavedViews` composable in src/utils/savedViews.ts. Defensive load (any parse error or shape mismatch resets to empty), shared reactive ref, debounced write-through via Vue watch. * Filter bar gains a "Save view" button next to "Reset"; when clicked, prompts for a name (with a sensible auto-generated suggestion built from the active filter values) and stores the combo. * When saved views exist, a chip row is rendered above the filter form. Clicking a chip applies the saved combination and, if the filter id differs, navigates the router to the matching OCP filter so the API call refreshes too. * Each chip has a small × control to delete the view. Stacked on feat/search-filters (saved combos use those filter values). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a three-option density picker to the activity settings drawer in the app navigation sidebar. The user's choice is persisted to localStorage and applied as a data-attribute on the <html> element, so existing scoped row CSS can opt in via attribute selectors without prop-drilling. Defaults to "cozy" (today's behaviour). "Compact" tightens row padding and font-size for power users with long streams; "comfortable" is roomier with a 44px min-height for accessibility. Implementation lives in `src/utils/density.ts` as a tiny shared composable so other future per-user view preferences can follow the same pattern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a new "Insights" view at /insights that gives users a small weekly snapshot of their activity instead of just an infinite log: * 7-day sparkline of daily activity counts (inline SVG polyline + shaded fill, no charting library). * Day-of-week labels with per-day counts under the sparkline. * Top 5 collaborators by activity count (avatars + counts). * Top 5 most-touched files by object name. * Type-family breakdown bars (created / changed / deleted / share / comment / favourite / other) coloured to match the row accents used elsewhere. Implementation is purely client-side: a single OCS call for the 200 most-recent activities feeds every panel. No backend changes are required, so this can ship independently. A new `IconChartBar` entry is added to the sidebar, and an `/insights` route is added ahead of the `/:filter?` catch-all so vue-router does not try to interpret "insights" as a filter id. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comments-on-the-same-file currently each take their own row in the stream, which is noisy on busy projects. Group consecutive comment activities targeting the same (objectType, objectId) into one collapsible CommentThread row that shows: > 3 comments on design-spec.md 5 minutes ago Clicking the row reveals the underlying ActivityComponent rows. Single comments fall through to the normal row so they keep the preview and full layout — only ≥ 2 consecutive comments collapse. Implementation is in two parts: * `CommentThread.vue` — new component that renders the collapsed summary + an expandable list of activities. Uses the same teal accent colour as comment-typed rows in the type-accent palette so it visually belongs to the same family. * `ActivityGroup.vue` — buckets the activities array into thread vs single-row items inside a small computed. Buckets reset whenever a non-comment activity, or a comment on a different target, appears between comments, so threads only ever group genuinely contiguous chatter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "No activity yet" state was a single line of text and a tiny
monochrome icon — uninviting for new users and people landing on the
"Self" or "By you" filters before they have done anything.
Replace it with:
* A 64px app icon on a soft radial backdrop, surrounded by two
staggered, slowly pulsing rings — life signs without being noisy.
* A friendlier headline ("Nothing has happened here yet") and a
filter-aware description that nudges the user toward the action
most likely to populate the stream they are looking at. A small
deterministic hash picks one of two messages per filter so the
copy stays varied across the app but stable per page.
* Two action buttons in the empty-content slot — "Open Files"
(primary) and "Notification settings" (secondary) — so the user
has somewhere to go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolved conflicts with feat/colored-avatars in GenericActivity.vue: - Combined the type-family class on <li> with the actor-avatar block so each row shows both the colored avatar/badge and the type accent border. - Merged hasActor and typeFamily computed properties side-by-side. - CSS: kept colored-avatars structure (avatar/avatar-badge classes) alongside type-accents structure (--type-* border colors and hover background).
Resolved conflict at top of ActivityAppFeed.vue: kept the today-summary <p> before the now-skeleton loading list.
feat/saved-views was branched on top of feat/search-filters so this single merge commit brings in both feature commits. Resolved conflicts in ActivityAppFeed.vue: - Template: ordered the new elements as today-summary <p>, saved-views chip row, filter form, then the existing skeleton list. - Script: imports/computeds from both branches sit side-by-side and don't actually overlap, but the SCSS overlapped — placed the &__today-summary, saved-views/chip, and __filters* nested rules inside .activity-app, then closed it, then kept the top-level .activity-fade-* / .activity-skeleton / @Keyframes blocks added by skeleton-loaders.
Resolved conflict in ActivityAppFeed.vue: kept both the typeFamily() helper + todaySummary computed (from feat/today-summary) and the emptyDescription computed + filesLink/personalSettingsLink consts (from feat/empty-state) side-by-side in the script section. The CSS additions from empty-state landed without conflict.
Three follow-up tweaks to the combined UI refresh based on hands-on testing: 1) Move the search / person / from / to filters into the same row as the page heading. Wrapped heading + form in a new `__topbar` flex container, dropped NcTextField in favour of compact native <input> elements (search + date), and tightened widths (search 160px, person 110px, dates 130px) so the whole bar fits on one line without dwarfing the heading. Today-summary and the saved-views chips moved underneath the topbar. 2) Floating preview popup at the bottom of the viewport. Added a second <img> next to each thumbnail with class `__preview-popup`. It is hidden by default; on hover of the thumbnail it renders `position: fixed` at `bottom: 16px`, centred horizontally, scaled up to the natural image size (max 80vw / 70vh). This means the zoomed preview always appears at the bottom of the document regardless of which row is hovered, and the original thumbnail stays in place so the row layout doesn't jump. pointer-events: none so the popup doesn't steal the click — clicking the small thumbnail still opens the file. 3) Removed the dark hover border around the popup. The previous style added a 2px main-text-coloured border + outline on hover; the new popup has only a soft shadow on a clean main-background tile, which reads better at large sizes. The thumbnail still gets a subtle primary-element border colour change on hover so the user has visual confirmation they're on a hover target.
Replace the static "centered at the bottom of the viewport" popup with a cursor-tracked one. On every mousemove inside a preview thumbnail the popup is placed at (clientX + 16, clientY + 16) so it sits to the lower-right of the pointer; if the cursor is close enough to the viewport's right or bottom edge that the popup would overflow, the position flips to the other side of the cursor so the image stays fully visible. Implementation: * Component data: hoveredPreviewIndex, popupX, popupY. * @mousemove on the preview wrapper updates all three; @Mouseleave resets hoveredPreviewIndex to -1 to hide the popup. * The popup <img> is rendered with v-if hoveredPreviewIndex === index and binds :style="{ left, top }" for absolute positioning. * CSS dropped the old :hover selector and the static left/bottom values — those are now driven by the inline style.
Two requested removals on the combined branch: 1) Empty state: removed the "Open Files" and "Notification settings" buttons from the NcEmptyContent #action slot. The friendly headline, description, and pulse decoration stay; the page just stops trying to route the user elsewhere. generateUrl import dropped along with the filesLink / personalSettingsLink consts. 2) Density toggle: removed the Compact / Cozy / Comfortable radio group from the Activity settings drawer in the navigation sidebar, the accompanying script wiring (useDensity + densityOptions), and the density-aware CSS in ActivityAppNavigation.vue. src/utils/density.ts deleted. The original feat/density-toggle branch is unchanged in case the feature ever wants to come back.
Two related UI tweaks: 1) List/grid view toggle. Adds a small two-button segmented control to the activity topbar (list / grid icons from the Material Design icon set). The choice is stored in localStorage under `activity:view-mode` so it persists across reloads. In grid mode the per-day <ul> becomes a CSS grid (`repeat(auto-fill, minmax(260px, 1fr))`) and each `.activity-entry` reflows into a vertical card: avatar on top, content + date below, preview thumbnails at full card width. Type-family accent borders are preserved. Day headings still span the full width. ActivityGroup.vue's outer <ul> gets a new `activity-group__list` class so the parent `.activity-app--grid` styles can target it precisely without leaking into the preview-thumbnail or comment- thread inner lists. 2) Preview popup now anchors to the hovered thumbnail instead of following the cursor. Switched the preview wrapper handler from @mousemove to @mouseenter; the handler now reads the thumbnail's bounding rect and places the popup at (rect.left, rect.bottom + 8). Edge handling shifts the popup left if it would overflow the right edge, and flips it above the thumbnail if there isn't enough room below — same fudge dimensions as before so no flicker. The result is a much more stable popup that doesn't dance around when the cursor moves over a single thumbnail.
Two requested simplifications on the combined branch: 1) Remove the list/grid view toggle. The activity stream is always rendered in list view. Reverts the segmented control in the topbar, the IconViewList / IconViewGrid imports, the viewMode ref and its localStorage persistence, the `activity-app--grid` :deep styles, and the helper `activity-group__list` class on the per-day <ul> in ActivityGroup.vue. 2) Trigger the preview popup only when hovering the small thumbnail image itself — not the surrounding link/span wrapper. Moved @mouseenter and @Mouseleave from the .activity-entry__preview wrapper down to the <img class="...preview-image">. The getBoundingClientRect() lookup now reads the image's exact box, so the popup also sits flush under the visible thumbnail rather than the slightly larger anchor area.
Switch the preview hover handler back from @mouseenter to @mousemove so the popup tracks the pointer as the user moves over the small thumbnail, anchored at (clientX + 16, clientY + 16). Edge handling flips the popup to the other side of the cursor when near the right or bottom viewport edge so it stays fully on-screen. Trigger remains scoped to the small <img> element (not the surrounding link wrapper), so the popup only appears while the cursor is actually over the thumbnail image.
…ontext menu Six UI/UX upgrades plus a polish pass: #13 Server-side search. Threaded a `q=` parameter through APIv2Controller into Data::get; the activity SQL gains an iLike clause against subject and message when the parameter is non-empty. Frontend wires filterText into a 300ms-debounced ref that drives the OCS request and resets the paginated state when the query changes; client-side filter keeps person + date narrowing only. Search now works against the full history, not just what's been paginated in. #3 Refresh button. A round 32px button with a refresh icon sits in the topbar; clicking spins the icon for 600ms and triggers pollNewActivities() so the user has a manual "check for new activity" affordance. The minimum spin guarantees visible feedback even when the poll completes instantly. #11 Permalinks. Each activity row gets a hover-revealed copy-link button + a more-actions button. The link is a hash-query of the form `/apps/activity/#/all?id=42`. ActivityAppFeed reads the id from the hash on mount and after every paginated load, scrolls the matching row into view, and applies a soft yellow flash on the highlighted row. #5 Streak counter. Insights tab now opens with a row of large stat tiles: streak (with an animated flame when active), total loaded activities, today count, and people-involved count. Streak is consecutive days back from today (or yesterday if today's bucket is still empty). #6 Heatmap calendar. GitHub-style 12-week × 7-day grid below the stats row. Levels 0–4 are buckets of the loaded sample's max, tinted with `color-mix(...primary-element)` so the palette automatically follows the user's theme. Hovering a cell shows the day's date and count. #8 Right-click + actions menu. Per-row context menu (also reachable via the … button) with three actions: Open in Files, Copy link, Mute this event type. Mute persists to localStorage via a small composable in src/utils/mutedTypes.ts; the feed filters muted types out of the displayed list automatically. #16 Browser-level virtualisation. Each .activity-entry now uses `content-visibility: auto` + `contain-intrinsic-size: 80px 100%`, so the browser skips layout/paint for off-screen rows. No dependency, no restructure — the perf benefit is most noticeable on accounts with thousands of activities and the existing infinite-scroll has accumulated a lot of DOM. vue-virtual-scroller remains an option for a follow-up if true DOM-level recycling becomes necessary. Polish pass: * Day headings get an uppercase, letter-spaced look with a subtle hairline that fades to the right of the text. * Activity rows pick up a soft inset border on hover so they feel "liftable". * Today summary line gets a small ✨ glyph and a touch more breathing room around the topbar.
…eview popup
This rolls up the remaining uncommitted UI-refresh work that does not
belong to any of the four standalone PRs (digest XSS fix, dual-DB doc,
admin DB panel, digest email previews).
User-visible additions:
- Pinned activities — `src/utils/pinnedActivities.ts` plus rendering
hooks in the feed and insights view; pin actions go through the
Teleported context menu in `GenericActivity.vue`.
- Muted users — `src/utils/mutedUsers.ts` companion to the existing
muted-types path; surfaced from the activity row context menu.
- Activity bursts — `src/components/ActivityBurst.vue` collapses
large bulk events ("Carl uploaded 47 files to /Foo") into a single
expandable row instead of 47 list items.
- Refined preview popup — corrects the offscreen-snapping bug by
measuring the popup's actual size via a `Teleport`-anchored ref
and clamping into the viewport, rather than flipping above with a
pessimistic 70vh fudge.
- Saved views, activity insights tweaks, secondary avatars,
--fresh highlight, Reply/Restore/Pin context menu items, and the
Teleported context menu (escapes `content-visibility: auto`
containment in the feed shell).
Companion changes:
- `src/components/ActivityComponent.vue` — wires the new context-menu
items and burst rendering into the existing renderer.
- `src/components/ActivityGroup.vue` — surfaces "show previews" and
forwards new state.
- `src/components/CommentThread.vue` — picks up the new mute/pin
states for comment activity.
- `src/utils/savedViews.ts` — minor polish on the saved-view storage.
- `src/views/ActivityAppFeed.vue` — search bar, refresh button,
permalinks, infinite scroll, pinned section.
- `src/views/ActivityInsights.vue` — uses the new pinned/muted state.
Signed-off-by: Frank Karlitschek <frank@nextcloud.com>
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.
@jancborchardt @nickvergessen