From d9cdd6bc3876e24f6b131d19bfeacf0b5b1ead7a Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Tue, 10 Mar 2026 01:20:15 +0100 Subject: [PATCH] feat(stream): autoload newer activities Fix https://github.com/nextcloud/activity/issues/405 Signed-off-by: Anna Larch --- src/views/ActivityAppFeed.vue | 79 ++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/src/views/ActivityAppFeed.vue b/src/views/ActivityAppFeed.vue index c59c5c03c..c2d9359ca 100644 --- a/src/views/ActivityAppFeed.vue +++ b/src/views/ActivityAppFeed.vue @@ -49,9 +49,9 @@ import { loadState } from '@nextcloud/initial-state' import { translate as t } from '@nextcloud/l10n' import moment from '@nextcloud/moment' import { generateOcsUrl } from '@nextcloud/router' -import { useInfiniteScroll } from '@vueuse/core' +import { useDocumentVisibility, useInfiniteScroll } from '@vueuse/core' import axios from 'axios' -import { computed, nextTick, onMounted, ref, watch } from 'vue' +import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' import { useRoute } from 'vue-router' import NcAppContent from '@nextcloud/vue/components/NcAppContent' import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' @@ -99,11 +99,31 @@ const hasMoreActivites = ref(true) const allActivities = ref([]) /** - * Last loaded activity + * Last loaded activity (oldest) for backward pagination * This is set by the backend in the API result as a header value for pagination */ const lastActivityLoaded = ref() +/** + * First loaded activity ID (newest) for polling new activities + */ +const newestActivityId = ref() + +/** + * Polling interval in milliseconds + */ +const POLL_INTERVAL = 30000 + +/** + * Polling timer reference + */ +let pollTimer: ReturnType | undefined + +/** + * Document visibility for pausing polling when tab is hidden + */ +const visibility = useDocumentVisibility() + /** * Container element for the activites */ @@ -153,10 +173,16 @@ async function loadActivities() { const since = lastActivityLoaded.value ?? '0' loading.value = true const response = await ncAxios.get(generateOcsUrl('apps/activity/api/v2/activity/{filter}?format=json&previews=true&since={since}', { filter: props.filter, since })) - allActivities.value.push(...response.data.ocs.data.map((raw) => new ActivityModel(raw))) + const newActivities = response.data.ocs.data.map((raw) => new ActivityModel(raw)) + allActivities.value.push(...newActivities) lastActivityLoaded.value = response.headers['x-activity-last-given'] hasMoreActivites.value = true + // Track the newest activity ID for polling + if (newestActivityId.value === undefined && newActivities.length > 0) { + newestActivityId.value = newActivities[0].id + } + nextTick(async () => { if (container.value && container.value.clientHeight === container.value.scrollHeight) { // Container is non-scrollable, thus useInfiniteScroll isn't triggered @@ -179,10 +205,52 @@ async function loadActivities() { } /** - * Load activites when mounted + * Poll for new activities and prepend them to the list + */ +async function pollNewActivities() { + if (loading.value || newestActivityId.value === undefined || visibility.value === 'hidden') { + return + } + + try { + 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) })) + const newActivities: ActivityModel[] = response.data.ocs.data.map((raw) => new ActivityModel(raw)) + if (newActivities.length > 0) { + // Sort newest first for prepending + newActivities.sort((a: ActivityModel, b: ActivityModel) => b.id - a.id) + newestActivityId.value = newActivities[0]!.id + allActivities.value.unshift(...newActivities) + } + } catch (error) { + // Silently ignore polling errors (304 = no new activities) + if (!axios.isAxiosError(error) || error.response?.status !== 304) { + logger.error(error as Error) + } + } +} + +function startPolling() { + stopPolling() + pollTimer = setInterval(pollNewActivities, POLL_INTERVAL) +} + +function stopPolling() { + if (pollTimer !== undefined) { + clearInterval(pollTimer) + pollTimer = undefined + } +} + +/** + * Load activites when mounted and start polling */ onMounted(() => { loadActivities() + startPolling() +}) + +onUnmounted(() => { + stopPolling() }) /** @@ -191,6 +259,7 @@ onMounted(() => { watch(props, () => { allActivities.value = [] lastActivityLoaded.value = undefined + newestActivityId.value = undefined loadActivities() })