Skip to content

Feat/UI refresh misc#2547

Open
karlitschek wants to merge 31 commits intomasterfrom
feat/ui-refresh-misc
Open

Feat/UI refresh misc#2547
karlitschek wants to merge 31 commits intomasterfrom
feat/ui-refresh-misc

Conversation

@karlitschek
Copy link
Copy Markdown
Member

@karlitschek karlitschek commented May 2, 2026

karlitschek and others added 30 commits April 28, 2026 16:35
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>
@cypress
Copy link
Copy Markdown

cypress Bot commented May 2, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant