@@ -49,9 +49,9 @@ import { loadState } from '@nextcloud/initial-state'
4949import { translate as t } from ' @nextcloud/l10n'
5050import moment from ' @nextcloud/moment'
5151import { generateOcsUrl } from ' @nextcloud/router'
52- import { useInfiniteScroll } from ' @vueuse/core'
52+ import { useDocumentVisibility , useInfiniteScroll } from ' @vueuse/core'
5353import axios from ' axios'
54- import { computed , nextTick , onMounted , ref , watch } from ' vue'
54+ import { computed , nextTick , onMounted , onUnmounted , ref , watch } from ' vue'
5555import { useRoute } from ' vue-router'
5656import NcAppContent from ' @nextcloud/vue/components/NcAppContent'
5757import NcEmptyContent from ' @nextcloud/vue/components/NcEmptyContent'
@@ -99,11 +99,31 @@ const hasMoreActivites = ref(true)
9999const allActivities = ref <ActivityModel []>([])
100100
101101/**
102- * Last loaded activity
102+ * Last loaded activity (oldest) for backward pagination
103103 * This is set by the backend in the API result as a header value for pagination
104104 */
105105const lastActivityLoaded = ref <string >()
106106
107+ /**
108+ * First loaded activity ID (newest) for polling new activities
109+ */
110+ const newestActivityId = ref <number >()
111+
112+ /**
113+ * Polling interval in milliseconds
114+ */
115+ const POLL_INTERVAL = 30000
116+
117+ /**
118+ * Polling timer reference
119+ */
120+ let pollTimer: ReturnType <typeof setInterval > | undefined
121+
122+ /**
123+ * Document visibility for pausing polling when tab is hidden
124+ */
125+ const visibility = useDocumentVisibility ()
126+
107127/**
108128 * Container element for the activites
109129 */
@@ -153,10 +173,16 @@ async function loadActivities() {
153173 const since = lastActivityLoaded .value ?? ' 0'
154174 loading .value = true
155175 const response = await ncAxios .get (generateOcsUrl (' apps/activity/api/v2/activity/{filter}?format=json&previews=true&since={since}' , { filter: props .filter , since }))
156- allActivities .value .push (... response .data .ocs .data .map ((raw ) => new ActivityModel (raw )))
176+ const newActivities = response .data .ocs .data .map ((raw ) => new ActivityModel (raw ))
177+ allActivities .value .push (... newActivities )
157178 lastActivityLoaded .value = response .headers [' x-activity-last-given' ]
158179 hasMoreActivites .value = true
159180
181+ // Track the newest activity ID for polling
182+ if (newestActivityId .value === undefined && newActivities .length > 0 ) {
183+ newestActivityId .value = newActivities [0 ].id
184+ }
185+
160186 nextTick (async () => {
161187 if (container .value && container .value .clientHeight === container .value .scrollHeight ) {
162188 // Container is non-scrollable, thus useInfiniteScroll isn't triggered
@@ -179,10 +205,52 @@ async function loadActivities() {
179205}
180206
181207/**
182- * Load activites when mounted
208+ * Poll for new activities and prepend them to the list
209+ */
210+ async function pollNewActivities() {
211+ if (loading .value || newestActivityId .value === undefined || visibility .value === ' hidden' ) {
212+ return
213+ }
214+
215+ try {
216+ const response = await ncAxios .get (generateOcsUrl (' apps/activity/api/v2/activity/{filter}?format=json&previews=true&since={since}&sort=asc' , { filter: props .filter , since: String (newestActivityId .value ) }))
217+ const newActivities: ActivityModel [] = response .data .ocs .data .map ((raw ) => new ActivityModel (raw ))
218+ if (newActivities .length > 0 ) {
219+ // Sort newest first for prepending
220+ newActivities .sort ((a : ActivityModel , b : ActivityModel ) => b .id - a .id )
221+ newestActivityId .value = newActivities [0 ]! .id
222+ allActivities .value .unshift (... newActivities )
223+ }
224+ } catch (error ) {
225+ // Silently ignore polling errors (304 = no new activities)
226+ if (! axios .isAxiosError (error ) || error .response ?.status !== 304 ) {
227+ logger .error (error as Error )
228+ }
229+ }
230+ }
231+
232+ function startPolling() {
233+ stopPolling ()
234+ pollTimer = setInterval (pollNewActivities , POLL_INTERVAL )
235+ }
236+
237+ function stopPolling() {
238+ if (pollTimer !== undefined ) {
239+ clearInterval (pollTimer )
240+ pollTimer = undefined
241+ }
242+ }
243+
244+ /**
245+ * Load activites when mounted and start polling
183246 */
184247onMounted (() => {
185248 loadActivities ()
249+ startPolling ()
250+ })
251+
252+ onUnmounted (() => {
253+ stopPolling ()
186254})
187255
188256/**
@@ -191,6 +259,7 @@ onMounted(() => {
191259watch (props , () => {
192260 allActivities .value = []
193261 lastActivityLoaded .value = undefined
262+ newestActivityId .value = undefined
194263 loadActivities ()
195264})
196265 </script >
0 commit comments