Skip to content

Commit d9cdd6b

Browse files
committed
feat(stream): autoload newer activities
Fix #405 Signed-off-by: Anna Larch <anna@nextcloud.com>
1 parent df9a4ea commit d9cdd6b

1 file changed

Lines changed: 74 additions & 5 deletions

File tree

src/views/ActivityAppFeed.vue

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ import { loadState } from '@nextcloud/initial-state'
4949
import { translate as t } from '@nextcloud/l10n'
5050
import moment from '@nextcloud/moment'
5151
import { generateOcsUrl } from '@nextcloud/router'
52-
import { useInfiniteScroll } from '@vueuse/core'
52+
import { useDocumentVisibility, useInfiniteScroll } from '@vueuse/core'
5353
import axios from 'axios'
54-
import { computed, nextTick, onMounted, ref, watch } from 'vue'
54+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
5555
import { useRoute } from 'vue-router'
5656
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
5757
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
@@ -99,11 +99,31 @@ const hasMoreActivites = ref(true)
9999
const 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
*/
105105
const 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
*/
184247
onMounted(() => {
185248
loadActivities()
249+
startPolling()
250+
})
251+
252+
onUnmounted(() => {
253+
stopPolling()
186254
})
187255
188256
/**
@@ -191,6 +259,7 @@ onMounted(() => {
191259
watch(props, () => {
192260
allActivities.value = []
193261
lastActivityLoaded.value = undefined
262+
newestActivityId.value = undefined
194263
loadActivities()
195264
})
196265
</script>

0 commit comments

Comments
 (0)