From 848305abe2a806461fa8f72c3c6c5f5810173b45 Mon Sep 17 00:00:00 2001 From: Valerian Saliou Date: Thu, 5 Dec 2024 18:43:02 -0300 Subject: [PATCH] fix: use a progressing time provider for ui computeds --- src/cards/inbox/MessageAuthor.vue | 15 +++- src/components/inbox/InboxBanner.vue | 24 +++-- .../inbox/InboxDetailsUserInformation.vue | 22 ++++- src/composables/timer.ts | 89 +++++++++++++++++++ 4 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 src/composables/timer.ts diff --git a/src/cards/inbox/MessageAuthor.vue b/src/cards/inbox/MessageAuthor.vue index 5dab1080..e96fa309 100644 --- a/src/cards/inbox/MessageAuthor.vue +++ b/src/cards/inbox/MessageAuthor.vue @@ -95,6 +95,9 @@ import { Room, JID, ParticipantInfo } from "@prose-im/prose-sdk-js"; import Store from "@/store"; import { InboxEntryMessage } from "@/store/tables/inbox"; +// PROJECT: COMPOSABLES +import { useTimerMinutes } from "@/composables/timer"; + export default { name: "MessageAuthor", @@ -125,6 +128,14 @@ export default { } }, + setup() { + const { date } = useTimerMinutes(); + + return { + localDateMinutes: date + }; + }, + computed: { message(): InboxEntryMessage | void { return Store.$inbox.getMessage(this.room.id, this.messageId); @@ -203,10 +214,8 @@ export default { this.profile?.information?.location.timezone || null; if (profileTimezone !== null) { - const nowDate = new Date(); - return `${this.$filters.date.localTime( - nowDate, + this.localDateMinutes, profileTimezone.offset )} local time`; } diff --git a/src/components/inbox/InboxBanner.vue b/src/components/inbox/InboxBanner.vue index ff53d54b..39ba8db8 100644 --- a/src/components/inbox/InboxBanner.vue +++ b/src/components/inbox/InboxBanner.vue @@ -44,6 +44,7 @@ import { JID, Room } from "@prose-im/prose-sdk-js"; // PROJECT: COMPOSABLES import { useEvents } from "@/composables/events"; +import { useTimerMinutes } from "@/composables/timer"; // PROJECT: STORES import Store from "@/store"; @@ -90,6 +91,18 @@ export default { } }, + setup() { + // Export a local date that monotonically updates every minute + // Notice: this is required, since a Vue computed on a Date instance will \ + // only return once, and be treated as a static value afterwards. \ + // Meaning the date value would not progress as time passes. + const { date } = useTimerMinutes(); + + return { + localDateMinutes: date + }; + }, + data() { return { // --> STATE <-- @@ -156,7 +169,7 @@ export default { // Apply offset to date (in minutes) // Notice: create new date object, as not to mutate the provided one. const userDate = new Date( - this.localDate.getTime() + + this.localDateMinutes.getTime() + userTimezone.offset * MINUTE_TO_MILLISECONDS ); @@ -167,7 +180,7 @@ export default { userTimeHour < CONSIDER_TIME_ASLEEP_BEFORE ) { return this.$filters.date.localTime( - this.localDate, + this.localDateMinutes, userTimezone.offset ); } @@ -176,15 +189,10 @@ export default { return null; }, - localDate(): Date { - // Return current local date (for local environment) - return new Date(); - }, - localTimezoneOffset(): number { // Return current local TZO (for local environment) // Important: negate result since JS returns inverted TZOs. - return -this.localDate.getTimezoneOffset(); + return -this.localDateMinutes.getTimezoneOffset(); }, session(): typeof Store.$session { diff --git a/src/components/inbox/InboxDetailsUserInformation.vue b/src/components/inbox/InboxDetailsUserInformation.vue index d49e5d7b..b15a45aa 100644 --- a/src/components/inbox/InboxDetailsUserInformation.vue +++ b/src/components/inbox/InboxDetailsUserInformation.vue @@ -65,6 +65,9 @@ import { PropType } from "vue"; import { JID, Availability } from "@prose-im/prose-sdk-js"; import { getCountryCode, getCountryName } from "crisp-countries-languages"; +// PROJECT: COMPOSABLES +import { useTimerMinutes } from "@/composables/timer"; + // PROJECT: STORES import Store from "@/store"; @@ -98,6 +101,18 @@ export default { } }, + setup() { + // Export a local date that monotonically updates every minute + // Notice: this is required, since a Vue computed on a Date instance will \ + // only return once, and be treated as a static value afterwards. \ + // Meaning the date value would not progress as time passes. + const { date } = useTimerMinutes(); + + return { + localDateMinutes: date + }; + }, + data() { return { // --> DATA <-- @@ -149,11 +164,12 @@ export default { userCountry = this.profile.information.location.country || null; if (userTimezone !== null) { - const nowDate = new Date(); - entries.push({ id: "timezone", - title: this.$filters.date.localTime(nowDate, userTimezone.offset), + title: this.$filters.date.localTime( + this.localDateMinutes, + userTimezone.offset + ), icon: "clock.fill" }); } diff --git a/src/composables/timer.ts b/src/composables/timer.ts new file mode 100644 index 00000000..bdc9a2ff --- /dev/null +++ b/src/composables/timer.ts @@ -0,0 +1,89 @@ +/* + * This file is part of prose-app-web + * + * Copyright 2024, Prose Foundation + */ + +/************************************************************************** + * IMPORTS + * ************************************************************************* */ + +// NPM +import { Ref, ref, onMounted, onBeforeUnmount } from "vue"; + +/************************************************************************** + * COMPOSABLE + * ************************************************************************* */ + +function useTimerMinutes(): { + date: Ref; +} { + // --> INTERNALS <-- + + let timer = null as null | ReturnType; + + // --> DATA <-- + + const date = ref(new Date()); + + // --> METHODS <-- + + const scheduleNextTick = async function () { + if (timer === null) { + // Acquire current timestamp + const nowTime = Date.now(); + + // Acquire date at next minute, at second zero (clone current date) + const nextMinuteDate = new Date(nowTime); + + nextMinuteDate.setSeconds(0); + nextMinuteDate.setMinutes(nextMinuteDate.getMinutes() + 1); + + // Calculate time remaining to next minute + const timeToNextMinute = nextMinuteDate.getTime() - nowTime; + + // Schedule next timer (to fire at next minute, at second zero) + // Important: do not use a reliable timeout scheduler here, since the \ + // date output value is solely used by UI and therefore we want to \ + // avoid triggering UI digests when the application is put in the \ + // background. + timer = setTimeout(() => { + timer = null; + + // Update date + // Important: do not assign 'nextMinuteDate' here, since the \ + // JavaScript VM might have throttled this timer and thus the date \ + // might have became stale relative to now. Always create a new date \ + // object for this reason. + date.value = new Date(); + + // Schedule next timer tick + scheduleNextTick(); + }, timeToNextMinute); + } + }; + + // --> LIFECYCLE <-- + + onMounted(() => { + // Schedule first timer tick + scheduleNextTick(); + }); + + onBeforeUnmount(() => { + // Unschedule timer? (if any) + if (timer !== null) { + clearTimeout(timer); + + timer = null; + } + }); + + return { date }; +} + +/************************************************************************** + * EXPORTS + * ************************************************************************* */ + +export { useTimerMinutes };