From 40156d0b60d4f2fef58491a7a2f87e25813e729b Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 21:42:21 +0200 Subject: [PATCH] fix(frontend): auto-refresh relative dates as time passes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relative dates ("5 minutes ago", "in 2 hours") were computed once via dayjs().fromNow() and never recomputed, so a view left open kept showing the value from the moment it was rendered. Compute the relative string against the shared, ticking `now` from useGlobalNow() instead. This makes every reactive caller — , direct formatDateSince() calls, and formatDisplayDate() when the user's date display is set to relative — re-render on the existing 60s tick. Absolute date formats don't read `now`, so they never needlessly re-render. useGlobalNow can now be initialised from a plain helper rather than only from a component, so its route-update hook is guarded with getCurrentInstance(). --- frontend/src/composables/useGlobalNow.ts | 14 +++++++++----- frontend/src/helpers/time/formatDate.ts | 8 +++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/frontend/src/composables/useGlobalNow.ts b/frontend/src/composables/useGlobalNow.ts index 83d9cf9ef..c5e3510e7 100644 --- a/frontend/src/composables/useGlobalNow.ts +++ b/frontend/src/composables/useGlobalNow.ts @@ -1,4 +1,4 @@ -import { ref } from 'vue' +import { getCurrentInstance, ref } from 'vue' import { createGlobalState, useIntervalFn } from '@vueuse/core' import { onBeforeRouteUpdate } from 'vue-router' @@ -18,10 +18,14 @@ export const useGlobalNow = createGlobalState(() => { useIntervalFn(update, GLOBAL_NOW_INTERVAL, { immediate: true }) - // ensure the now value is refreshed when the route changes - onBeforeRouteUpdate(() => { - update() - }) + // Now that this state can be initialised from a plain helper (formatDateSince), the + // first caller is not guaranteed to be a component — guard the route hook accordingly. + if (getCurrentInstance()) { + // ensure the now value is refreshed when the route changes + onBeforeRouteUpdate(() => { + update() + }) + } return { now, diff --git a/frontend/src/helpers/time/formatDate.ts b/frontend/src/helpers/time/formatDate.ts index 4ff9a4da5..ed7f4a3d7 100644 --- a/frontend/src/helpers/time/formatDate.ts +++ b/frontend/src/helpers/time/formatDate.ts @@ -5,6 +5,7 @@ import {i18n} from '@/i18n' import {createSharedComposable} from '@vueuse/core' import {computed, toValue, type MaybeRefOrGetter} from 'vue' import {useDateDisplay} from '@/composables/useDateDisplay' +import {useGlobalNow} from '@/composables/useGlobalNow' import {useTimeFormat} from '@/composables/useTimeFormat' import {DATE_DISPLAY, type DateDisplay} from '@/constants/dateDisplay' import {TIME_FORMAT, type TimeFormat} from '@/constants/timeFormat' @@ -49,8 +50,13 @@ export const formatDateSince = (date: Date | string | null) => { const locale = DAYJS_LOCALE_MAPPING[i18n.global.locale.value.toLowerCase()] ?? 'en' + // Computing the relative string against the shared, ticking `now` (instead of fromNow's + // internal Date.now()) makes every reactive caller re-render on the 60s tick, so open views + // don't keep showing a stale "x minutes ago". + const {now} = useGlobalNow() + return date - ? dayjs(date).locale(locale).fromNow() + ? dayjs(date).locale(locale).from(now.value) : '' }