Skip to content

Commit ef2519a

Browse files
karlitschekclaude
andcommitted
feat(stream): add search, date range and account filters to the stream
Adds three optional restrictions to the activity stream, exposed both on the v2 API and in the app UI: - `search` matches the activity's file path, case insensitively - `from`/`to` bound the stream by an inclusive Unix timestamp range - `actor` restricts it to activities authored by one account Backend: validation and normalisation live in a new immutable SearchCriteria value object, so the query builder only ever sees range checked input. Invalid criteria return 400 rather than running a query that cannot answer them. A date range is served by the existing activity_user_time index and an actor by activity_filter_by, so both stay index-ordered; the substring search stays bounded because every stream query is already anchored to a single affecteduser. The active criteria are carried into the pagination Link header so the next page does not silently widen back to the unfiltered stream. Frontend: a filter bar above the feed with a debounced search field, an account picker built from the accounts seen so far, and date range presets alongside a custom range. The criteria are mirrored into the URL so a filtered view can be bookmarked and shared, and the feed grows a filter specific empty state that can clear them again. Signed-off-by: Frank Karlitschek <karlitschek@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 932d60e commit ef2519a

50 files changed

Lines changed: 2131 additions & 169 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/endpoint-v2.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,43 @@ Name | Type | Description
4949
`object_type` | string (Optional) | Allows to filter the activities to a given object. May only appear together with `object_id` and the `filter` type filter
5050
`object_id` | string (Optional) | Allows to filter the activities to a given object. May only appear together with `object_type` and the `filter` type filter
5151
`sort` | string - `asc` or `desc` | Should activities be given ascending or descending (from the `since`) (Default: `desc`)
52+
`search` | string (Optional) | Only return activities whose file path contains this substring, matched case insensitively. Must be between 2 and 255 characters
53+
`from` | int (Optional) | Only return activities with a timestamp at or after this Unix timestamp
54+
`to` | int (Optional) | Only return activities with a timestamp at or before this Unix timestamp
55+
`actor` | string (Optional) | Only return activities authored by this account. At most 64 characters
5256

57+
### Searching and filtering by date
58+
59+
`search` matches against the activity's file path, so it finds activities by file
60+
or folder name. It does not match the rendered subject, because subjects are
61+
stored as untranslated template keys with their data held separately.
62+
63+
`from` and `to` are inclusive and independent: either may be given on its own for
64+
an open ended range. Both are Unix timestamps in seconds, so the client decides
65+
which timezone a calendar day maps onto. Values above `2147483647` are clamped to
66+
that maximum, which is the largest value the `timestamp` column can hold.
67+
68+
### Filtering by account
69+
70+
`actor` restricts the stream to activities authored by one account, given as its
71+
account name (user id), not its display name.
72+
73+
It composes with the type filter rather than replacing it: `by` combined with an
74+
`actor` yields everyone else's activity narrowed to that one account, and `self`
75+
combined with a different account yields nothing, since the two restrict the same
76+
column.
77+
78+
### Cost
79+
80+
A date range is served by the `activity_user_time` index and an `actor` by
81+
`activity_filter_by`, so both stay index-ordered and get cheaper the narrower
82+
they are. A `search` is a substring match that no index can serve; it stays
83+
bounded because every query is already restricted to a single user, and
84+
combining it with a date range or an actor narrows the scan further. Terms
85+
shorter than 2 characters are rejected rather than run, since they would scan
86+
the user's whole stream to return a useless result.
87+
88+
All four parameters are carried over into the `Link` header for the next page.
5389

5490
## HTTP Status
5591

@@ -58,6 +94,7 @@ Status Code | Description
5894
`200 OK` | Activities
5995
`204 No Content` | The user has selected no activities to be listed in the stream
6096
`304 Not Modified` | ETag/If-None-Match are the same or the end of the activity list was reached
97+
`400 Bad Request` | The search term is too short or too long, or `from` is after `to`
6198
`403 Forbidden` | The offset activity belongs to a different user
6299
`403 Forbidden` | The user is not logged in
63100
`404 Not Found` | The filter is unknown

js/ActivityComponent.vue_vue_type_script_setup_true_lang-BKMWNZYb.chunk.mjs

Lines changed: 87 additions & 0 deletions
Large diffs are not rendered by default.

js/ActivityComponent.vue_vue_type_script_setup_true_lang-Ce6NNuXP.chunk.mjs.license renamed to js/ActivityComponent.vue_vue_type_script_setup_true_lang-BKMWNZYb.chunk.mjs.license

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
1919
SPDX-FileCopyrightText: Sindre Sorhus
2020
SPDX-FileCopyrightText: Stefan Thomas <justmoon@members.fsf.org> (http://www.justmoon.net)
2121
SPDX-FileCopyrightText: Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)
22+
SPDX-FileCopyrightText: atomiks
2223
SPDX-FileCopyrightText: inline-style-parser developers
2324
SPDX-FileCopyrightText: rhysd <lin90162@yahoo.co.jp>
2425
SPDX-FileCopyrightText: ts-md5 developers
2526

2627
This file is generated from multiple sources. Included packages:
28+
- @floating-ui/dom
29+
- version: 1.7.6
30+
- license: MIT
31+
- @floating-ui/utils
32+
- version: 0.2.11
33+
- license: MIT
2734
- @nextcloud/capabilities
2835
- version: 1.2.1
2936
- license: GPL-3.0-or-later

js/ActivityComponent.vue_vue_type_script_setup_true_lang-BKMWNZYb.chunk.mjs.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/ActivityComponent.vue_vue_type_script_setup_true_lang-Ce6NNuXP.chunk.mjs

Lines changed: 0 additions & 87 deletions
This file was deleted.

js/ActivityComponent.vue_vue_type_script_setup_true_lang-Ce6NNuXP.chunk.mjs.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

js/ActivityTab-Cxq6JF8r.chunk.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(function(){"use strict";try{if(typeof document<"u"){var t=document.createElement("style");t.appendChild(document.createTextNode(".download-summary[data-v-81e4514f]{display:flex;align-items:flex-start;padding:8px 0;margin-bottom:calc(var(--default-grid-baseline) * 2);color:var(--color-text-maxcontrast)}.download-summary__icon[data-v-81e4514f]{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:20px;height:20px;margin-top:2px;opacity:.5}.download-summary__text[data-v-81e4514f]{padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activity[data-v-50818749]{display:flex;flex-direction:column;overflow:hidden;height:100%}.activity__actions[data-v-50818749]{display:flex;flex-direction:column;width:100%}.activity__list[data-v-50818749]{flex-grow:1;overflow:scroll}.activity__empty-content[data-v-50818749]{height:100%}[data-v-50818749] .empty-content__icon span{background-size:64px;width:64px;height:64px}")),document.head.appendChild(t)}}catch(e){console.error("vite-plugin-css-injected-by-js",e)}})();
2+
import{l as R}from"./activity-sidebar.mjs";import{b as A,m as I,h as G,o as D}from"./NcCheckboxRadioSwitch-BVTMQSAg-DheoiI42.chunk.mjs";import{d as b,x as V,B as E,y as T,a as i,c as o,j as B,a1 as p,J as h,m as n,e as S,f as u,t as H,E as g,l as M,b as r,g as y,F as v,K as k,n as q}from"./translation-DoG5ZELJ-C5SW80h5.chunk.mjs";import{a as j}from"./index-BrNGPgve.chunk.mjs";import{j as F,f as O,A as W}from"./ActivityComponent.vue_vue_type_script_setup_true_lang-BKMWNZYb.chunk.mjs";import{l as f}from"./logger-CXg3FpL2.chunk.mjs";import{g as U,a as z,b as J}from"./api-fwrRGLr6.chunk.mjs";import"./preload-helper-DxYC2qmj.chunk.mjs";var w;(function(t){t[t.User=0]="User",t[t.Group=1]="Group",t[t.Link=3]="Link",t[t.Email=4]="Email",t[t.Remote=6]="Remote",t[t.Team=7]="Team",t[t.Guest=8]="Guest",t[t.RemoteGroup=9]="RemoteGroup",t[t.Room=10]="Room",t[t.Deck=12]="Deck",t[t.FederatedGroup=14]="FederatedGroup",t[t.ScienceMesh=15]="ScienceMesh"})(w||(w={}));const K=b({__name:"ActivitySidebarPlugin",props:{plugin:{},node:{}},emits:["reloadActivities"],setup(t,{emit:e}){const a=t,s=e,d=B();return V(()=>a.plugin.mount(d.value,{node:a.node,context:E()?.proxy??void 0,reload:()=>s("reloadActivities")})),T(()=>a.plugin.unmount()),(_,c)=>(i(),o("div",{ref_key:"attachTarget",ref:d},null,512))}}),Z='<svg xmlns="http://www.w3.org/2000/svg" id="mdi-download-circle" viewBox="0 0 24 24"><path d="M12 2C17.5 2 22 6.5 22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2M8 17H16V15H8V17M16 10H13.5V6H10.5V10H8L12 14L16 10Z" /></svg>',$=b({name:"DownloadSummary",components:{NcIconSvgWrapper:A},props:{fileId:{type:Number,required:!0}},data(){return{totalCount:0,monthlyCount:0,downloadSVG:Z}},computed:{summaryText(){return this.monthlyCount>0&&this.monthlyCount<this.totalCount?p("activity","Downloaded %n time (%s in the last 30 days)","Downloaded %n times (%s in the last 30 days)",this.totalCount,[String(this.monthlyCount)]):p("activity","Downloaded %n time","Downloaded %n times",this.totalCount)}},watch:{fileId:{immediate:!0,handler(){this.fetchCounts()}}},methods:{async fetchCounts(){if(this.fileId){this.totalCount=0,this.monthlyCount=0;try{const t=await I.get(j("apps/activity/api/v2/activity/downloads/count"),{params:{format:"json",object_type:"files",object_id:this.fileId}});this.totalCount=t.data.ocs.data.total,this.monthlyCount=t.data.ocs.data.last30d}catch(t){f.error("Failed to fetch download counts",{error:t})}}},t:h,n:p}}),Q={key:0,class:"download-summary"},X={class:"download-summary__icon"},Y={class:"download-summary__text"};function tt(t,e,a,s,d,_){const c=n("NcIconSvgWrapper");return t.totalCount>0?(i(),o("div",Q,[S("span",X,[u(c,{svg:t.downloadSVG,size:20},null,8,["svg"])]),S("span",Y,H(t.summaryText),1)])):g("",!0)}const it=G($,[["render",tt],["__scopeId","data-v-81e4514f"]]),et=b({name:"ActivityTab",components:{ActivityComponent:O,DownloadSummary:it,NcEmptyContent:F,NcIconSvgWrapper:A,NcLoadingIcon:D,ActivitySidebarPlugin:K},props:{node:{type:Object,required:!0},folder:{type:Object,required:!1,default:void 0},view:{type:Object,required:!1,default:void 0}},expose:["update"],data(){return{error:"",loading:!0,activities:[],lightningBoltSVG:R,sidebarPlugins:[]}},computed:{hasPublicLink(){return Object.values(this.node?.attributes?.["share-types"]??{}).flat().includes(w.Link)}},watch:{node:{immediate:!0,async handler(){await this.update()}}},async mounted(){this.node&&await this.update()},methods:{async update(){this.sidebarPlugins=[];const t=J();t.length>0&&M(()=>{this.sidebarPlugins=t}),this.resetState(),await this.getActivities()},async getActivities(){try{this.loading=!0;const t=await this.processActivities(await this.loadRealActivities()),e=await z({node:this.node});this.activities=[...t,...e].sort((a,s)=>s.timestamp-a.timestamp)}catch(t){this.error=h("activity","Unable to load the activity list"),f.error("Error loading the activity list",{error:t})}finally{this.loading=!1}},resetState(){this.loading=!0,this.error="",this.activities=[]},async loadRealActivities(){try{const{data:t}=await I.get(j("apps/activity/api/v2/activity/filter"),{params:{format:"json",object_type:"files",object_id:this.node.fileid}});return t.ocs.data}catch(t){if(t.response!==void 0&&t.response.status===304)return[];throw t}},processActivities(t){t=t.map(a=>new W(a)),f.debug(`Processed ${t.length} activity(ies)`,{activities:t,node:this.node});const e=U();return t.filter(a=>!e||e.every(s=>s(a)))},t:h}}),at={key:0,class:"activity__actions"},ot={key:4,class:"activity__list"};function st(t,e,a,s,d,_){const c=n("NcIconSvgWrapper"),m=n("NcEmptyContent"),P=n("ActivitySidebarPlugin"),x=n("DownloadSummary"),L=n("NcLoadingIcon"),N=n("ActivityComponent");return i(),o("div",{class:q([{"icon-loading":t.loading},"activity"])},[t.error||!t.node?(i(),r(m,{key:0,name:t.error},{icon:y(()=>[u(c,{svg:t.lightningBoltSVG},null,8,["svg"])]),_:1},8,["name"])):(i(),o(v,{key:1},[t.sidebarPlugins.length>0?(i(),o("div",at,[(i(!0),o(v,null,k(t.sidebarPlugins,(l,C)=>(i(),r(P,{key:C,plugin:l,node:t.node,onReloadActivities:e[0]||(e[0]=nt=>t.getActivities())},null,8,["plugin","node"]))),128))])):g("",!0),t.hasPublicLink&&t.node.fileid?(i(),r(x,{key:1,fileId:t.node.fileid},null,8,["fileId"])):g("",!0),t.loading?(i(),r(m,{key:2,class:"activity__empty-content",name:t.t("activity","Loading activities")},{icon:y(()=>[u(L)]),_:1},8,["name"])):t.activities.length===0?(i(),r(m,{key:3,class:"activity__empty-content",name:t.t("activity","No activity yet")},{icon:y(()=>[u(c,{svg:t.lightningBoltSVG},null,8,["svg"])]),_:1},8,["name"])):(i(),o("ul",ot,[(i(!0),o(v,null,k(t.activities,l=>(i(),r(N,{key:l.id,activity:l,showPreviews:!1,onReload:e[1]||(e[1]=C=>t.getActivities())},null,8,["activity"]))),128))]))],64))],2)}const vt=G(et,[["render",st],["__scopeId","data-v-50818749"]]);export{vt as default};
3+
//# sourceMappingURL=ActivityTab-Cxq6JF8r.chunk.mjs.map
Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/ActivityTab-EzBumh36.chunk.mjs

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)