fix: standardize relative timestamping (#3612)

* fix(frontend): relative timestamps are incorrectly rounded.

Closes: #1371

* fix(all): remove legacy fromNow for proper relative timestamp creation

Closes: #1395
This commit is contained in:
Calum H.
2025-05-07 22:37:35 +01:00
committed by GitHub
parent 6d57da2053
commit 1884410e0d
33 changed files with 233 additions and 150 deletions

View File

@@ -19,7 +19,14 @@ import {
WorldIcon,
XIcon,
} from '@modrinth/assets'
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
import {
Avatar,
Button,
ButtonStyled,
Notifications,
OverflowMenu,
useRelativeTime,
} from '@modrinth/ui'
import { useLoading, useTheming } from '@/store/state'
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
import AccountsCard from '@/components/ui/AccountsCard.vue'
@@ -62,6 +69,8 @@ import FriendsList from '@/components/ui/friends/FriendsList.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
const formatRelativeTime = useRelativeTime()
const themeStore = useTheming()
const news = ref([])
@@ -590,7 +599,7 @@ function handleAuxClick(e) {
</h4>
<p class="my-1 text-sm text-secondary leading-tight">{{ item.summary }}</p>
<p class="text-right text-sm text-secondary opacity-60 leading-tight m-0">
{{ dayjs(item.date).fromNow() }}
{{ formatRelativeTime(dayjs(item.date).toISOString()) }}
</p>
</a>
<hr

View File

@@ -9,7 +9,7 @@ import {
StopCircleIcon,
TimerIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled } from '@modrinth/ui'
import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { finish_install, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
@@ -19,10 +19,9 @@ import { showProfileInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
import { trackEvent } from '@/helpers/analytics'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { formatCategory } from '@modrinth/utils'
dayjs.extend(relativeTime)
const formatRelativeTime = useRelativeTime()
const props = defineProps({
instance: {
@@ -173,7 +172,9 @@ onUnmounted(() => unlisten())
</div>
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
<TimerIcon />
<span class="text-sm"> Played {{ dayjs(instance.last_played).fromNow() }} </span>
<span class="text-sm">
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
</span>
</div>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
import { Avatar, ButtonStyled, OverflowMenu, useRelativeTime } from '@modrinth/ui'
import {
UserPlusIcon,
MoreVerticalIcon,
@@ -18,6 +18,8 @@ import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const formatRelativeTime = useRelativeTime()
const props = defineProps<{
credentials: unknown | null
signIn: () => void
@@ -205,7 +207,9 @@ onUnmounted(() => {
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
</template>
</p>
<p class="m-0 text-sm text-secondary">{{ friend.created.fromNow() }}</p>
<p class="m-0 text-sm text-secondary">
{{ formatRelativeTime(friend.created.toISOString()) }}
</p>
</div>
<div class="flex gap-2">
<template v-if="friend.id === userCredentials.user_id">

View File

@@ -8,7 +8,14 @@ import {
SpinnerIcon,
StopCircleIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui'
import {
Avatar,
ButtonStyled,
commonMessages,
OverflowMenu,
SmartClickable,
useRelativeTime,
} from '@modrinth/ui'
import { useVIntl } from '@vintl/vintl'
import { computed, nextTick, ref, onMounted, onUnmounted } from 'vue'
import { showProfileInFolder } from '@/helpers/utils'
@@ -25,6 +32,7 @@ import { handleError } from '@/store/notifications'
import { process_listener } from '@/helpers/events'
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
const router = useRouter()
@@ -144,7 +152,7 @@ onUnmounted(() => {
<template v-if="instance.last_played">
{{
formatMessage(commonMessages.playedLabel, {
time: dayjs(instance.last_played).fromNow(),
time: formatRelativeTime(instance.last_played.toISOString()),
})
}}
</template>

View File

@@ -7,6 +7,14 @@ import {
showWorldInFolder,
} from '@/helpers/worlds.ts'
import { formatNumber } from '@modrinth/utils'
import {
useRelativeTime,
Avatar,
ButtonStyled,
commonMessages,
OverflowMenu,
SmartClickable,
} from '@modrinth/ui'
import {
IssuesIcon,
EyeIcon,
@@ -25,7 +33,6 @@ import {
UserIcon,
XIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui'
import type { MessageDescriptor } from '@vintl/vintl'
import { defineMessages, useVIntl } from '@vintl/vintl'
import type { Component } from 'vue'
@@ -36,6 +43,7 @@ import { useRouter } from 'vue-router'
import { Tooltip } from 'floating-vue'
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
const router = useRouter()
@@ -255,7 +263,7 @@ const messages = defineMessages({
<template v-if="world.last_played">
{{
formatMessage(commonMessages.playedLabel, {
time: dayjs(world.last_played).fromNow(),
time: formatRelativeTime(dayjs(world.last_played).toISOString()),
})
}}
</template>

View File

@@ -184,7 +184,7 @@
"
class="date"
>
{{ fromNow(notif.extra_data.version.date_published) }}
{{ formatRelativeTime(notif.extra_data.version.date_published) }}
</span>
</span>
</div>
@@ -201,7 +201,7 @@
v-tooltip="$dayjs(notification.created).format('MMMM D, YYYY [at] h:mm A')"
class="inline-flex"
>
<CalendarIcon class="mr-1" /> Received {{ fromNow(notification.created) }}
<CalendarIcon class="mr-1" /> Received {{ formatRelativeTime(notification.created) }}
</span>
</span>
<div v-if="compact" class="notification__actions">
@@ -331,6 +331,7 @@ import {
XIcon,
ExternalIcon,
} from "@modrinth/assets";
import { useRelativeTime } from "@modrinth/ui";
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
import { getUserLink } from "~/helpers/users.js";
@@ -345,6 +346,8 @@ import Categories from "~/components/ui/search/Categories.vue";
const app = useNuxtApp();
const emit = defineEmits(["update:notifications"]);
const formatRelativeTime = useRelativeTime();
const props = defineProps({
notification: {
type: Object,

View File

@@ -75,7 +75,7 @@
class="stat date"
>
<UpdatedIcon aria-hidden="true" />
<span class="date-label">Updated </span>{{ fromNow(updatedAt) }}
<span class="date-label">Updated </span>{{ formatRelativeTime(updatedAt) }}
</div>
<div
v-else-if="showCreatedDate"
@@ -83,7 +83,7 @@
class="stat date"
>
<CalendarIcon aria-hidden="true" />
<span class="date-label">Published </span>{{ fromNow(createdAt) }}
<span class="date-label">Published </span>{{ formatRelativeTime(createdAt) }}
</div>
</div>
</article>
@@ -95,6 +95,7 @@ import Categories from "~/components/ui/search/Categories.vue";
import Badge from "~/components/ui/Badge.vue";
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
import Avatar from "~/components/ui/Avatar.vue";
import { useRelativeTime } from "@modrinth/ui";
export default {
components: {
@@ -213,8 +214,9 @@ export default {
},
setup() {
const tags = useTags();
const formatRelativeTime = useRelativeTime();
return { tags };
return { tags, formatRelativeTime };
},
computed: {
projectTypeDisplay() {

View File

@@ -95,7 +95,7 @@
</nuxt-link>
<span>&nbsp;</span>
<span v-tooltip="$dayjs(report.created).format('MMMM D, YYYY [at] h:mm A')">{{
fromNow(report.created)
formatRelativeTime(report.created)
}}</span>
<CopyCode v-if="flags.developerMode" :text="report.id" class="report-id" />
</div>
@@ -105,11 +105,14 @@
<script setup>
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
import { renderHighlightedString } from "~/helpers/highlight.js";
import { useRelativeTime } from "@modrinth/ui";
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
const formatRelativeTime = useRelativeTime();
defineProps({
report: {
type: Object,

View File

@@ -3,6 +3,7 @@ import dayjs from "dayjs";
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
import { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { useRelativeTime } from "@modrinth/ui";
import {
DISMISSABLE,
getDismissableMetadata,
@@ -11,6 +12,7 @@ import {
import { useVIntl } from "@vintl/vintl";
const { formatMessage } = useVIntl();
const formatRelativeTime = useRelativeTime();
const props = defineProps<{
notice: ServerNoticeType;
@@ -25,7 +27,7 @@ const props = defineProps<{
<div class="text-sm">
<span v-if="notice.announce_at">
{{ dayjs(notice.announce_at).format("MMM D, YYYY [at] h:mm A") }} ({{
dayjs(notice.announce_at).fromNow()
formatRelativeTime(notice.announce_at)
}})
</span>
<template v-else> Never begins </template>
@@ -35,7 +37,7 @@ const props = defineProps<{
v-if="notice.expires"
v-tooltip="dayjs(notice.expires).format('MMMM D, YYYY [at] h:mm A')"
>
{{ dayjs(notice.expires).fromNow() }}
{{ formatRelativeTime(notice.expires) }}
</span>
<template v-else> Never expires </template>
</div>

View File

@@ -103,7 +103,7 @@ import {
ModrinthIcon,
ScaleIcon,
} from "@modrinth/assets";
import { AutoLink, OverflowMenu } from "@modrinth/ui";
import { AutoLink, OverflowMenu, useRelativeTime } from "@modrinth/ui";
import { renderString } from "@modrinth/utils";
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";

View File

@@ -1,17 +0,0 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime); // eslint-disable-line import/no-named-as-default-member
export const useCurrentDate = () => useState("currentDate", () => Date.now());
export const updateCurrentDate = () => {
const currentDate = useCurrentDate();
currentDate.value = Date.now();
};
export const fromNow = (date) => {
const currentDate = useCurrentDate();
return dayjs(date).from(currentDate.value);
};

View File

@@ -1,18 +0,0 @@
import { createFormatter, type Formatter } from "@vintl/how-ago";
import type { IntlController } from "@vintl/vintl/controller";
const formatters = new WeakMap<IntlController<any>, Formatter>();
export function useRelativeTime(): Formatter {
const vintl = useVIntl();
let formatter = formatters.get(vintl);
if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl));
formatter = (value, options) => formatterRef.value(value, options);
formatters.set(vintl, formatter);
}
return formatter;
}

View File

@@ -871,6 +871,7 @@ import {
ProjectSidebarDetails,
ProjectSidebarLinks,
ScrollablePanel,
useRelativeTime,
} from "@modrinth/ui";
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";

View File

@@ -92,7 +92,7 @@
<div class="mb-4 mt-2 flex w-full items-center gap-1 text-sm text-secondary">
{{ formatCategory(subscription.interval) }} ⋅ {{ subscription.status }} ⋅
{{ dayjs(subscription.created).format("MMMM D, YYYY [at] h:mma") }} ({{
dayjs(subscription.created).fromNow()
formatRelativeTime(subscription.created)
}})
</div>
</div>
@@ -151,7 +151,7 @@
</span>
<span class="text-sm text-secondary">
{{ dayjs(charge.due).format("MMMM D, YYYY [at] h:mma") }}
<span class="text-secondary">({{ dayjs(charge.due).fromNow() }}) </span>
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
</span>
<div
v-if="flags.developerMode"
@@ -196,7 +196,15 @@
</div>
</template>
<script setup>
import { Avatar, ButtonStyled, CopyCode, DropdownSelect, NewModal, Toggle } from "@modrinth/ui";
import {
Avatar,
ButtonStyled,
CopyCode,
DropdownSelect,
NewModal,
Toggle,
useRelativeTime,
} from "@modrinth/ui";
import { formatCategory, formatPrice } from "@modrinth/utils";
import {
CheckIcon,
@@ -215,7 +223,9 @@ const flags = useFeatureFlags();
const route = useRoute();
const data = useNuxtApp();
const vintl = useVIntl();
const { formatMessage } = vintl;
const formatRelativeTime = useRelativeTime();
const messages = defineMessages({
userNotFoundError: {

View File

@@ -156,7 +156,7 @@
<div class="text-sm">
<span v-if="notice.announce_at">
{{ dayjs(notice.announce_at).format("MMM D, YYYY [at] h:mm A") }} ({{
dayjs(notice.announce_at).fromNow()
formatRelativeTime(notice.announce_at)
}})
</span>
<template v-else> Never begins </template>
@@ -166,7 +166,7 @@
v-if="notice.expires"
v-tooltip="dayjs(notice.expires).format('MMMM D, YYYY [at] h:mm A')"
>
{{ dayjs(notice.expires).fromNow() }}
{{ formatRelativeTime(notice.expires) }}
</span>
<template v-else> Never expires </template>
</div>
@@ -267,6 +267,7 @@ import {
NewModal,
TeleportDropdownMenu,
Toggle,
useRelativeTime,
} from "@modrinth/ui";
import { SettingsIcon, PlusIcon, SaveIcon, TrashIcon, EditIcon, XIcon } from "@modrinth/assets";
import dayjs from "dayjs";
@@ -278,6 +279,8 @@ import { usePyroFetch } from "~/composables/pyroFetch.ts";
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
const { formatMessage } = useVIntl();
const formatRelativeTime = useRelativeTime();
const app = useNuxtApp() as unknown as { $notify: any };
const notices = ref<ServerNoticeType[]>([]);

View File

@@ -391,6 +391,7 @@ import {
DropdownSelect,
FileInput,
PopoutMenu,
useRelativeTime,
} from "@modrinth/ui";
import { isAdmin } from "@modrinth/utils";

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import { useRelativeTime } from "@modrinth/ui";
const vintl = useVIntl();
const { formatMessage } = vintl;

View File

@@ -185,7 +185,7 @@
<CalendarIcon aria-hidden="true" />
<span>
Received
{{ fromNow(notification.date_modified) }}
{{ formatRelativeTime(notification.date_modified) }}
</span>
</div>
</div>
@@ -527,7 +527,7 @@
</template>
<script setup>
import { Multiselect } from "vue-multiselect";
import { ButtonStyled } from "@modrinth/ui";
import { ButtonStyled, useRelativeTime } from "@modrinth/ui";
import {
CompassIcon,
LogInIcon,
@@ -544,6 +544,8 @@ import ProjectCard from "~/components/ui/ProjectCard.vue";
import { homePageProjects, homePageSearch, homePageNotifs } from "~/generated/state.json";
const formatRelativeTime = useRelativeTime();
const searchQuery = ref("leave");
const sortType = ref("relevance");

View File

@@ -94,7 +94,7 @@
<IssuesIcon v-if="project.age_warning" />
Submitted
<span v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')">{{
fromNow(project.queued)
formatRelativeTime(project.queued)
}}</span>
</span>
<span v-else class="submitter-info"><UnknownIcon /> Unknown queue date</span>
@@ -103,7 +103,7 @@
</template>
<script setup>
import { Chips } from "@modrinth/ui";
import { Chips, useRelativeTime } from "@modrinth/ui";
import {
UnknownIcon,
EyeIcon,
@@ -128,6 +128,8 @@ const now = app.$dayjs();
const TIME_24H = 86400000;
const TIME_48H = TIME_24H * 2;
const formatRelativeTime = useRelativeTime();
const { data: projects } = await useAsyncData("moderation/projects?count=1000", () =>
useBaseFetch("moderation/projects?count=1000", { internal: true }),
);

View File

@@ -203,7 +203,13 @@
</template>
<script setup>
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import { Checkbox, ConfirmModal, commonSettingsMessages, commonMessages } from "@modrinth/ui";
import {
Checkbox,
ConfirmModal,
commonSettingsMessages,
commonMessages,
useRelativeTime,
} from "@modrinth/ui";
import {
hasScope,

View File

@@ -57,7 +57,7 @@
</template>
<script setup>
import { XIcon } from "@modrinth/assets";
import { commonMessages, commonSettingsMessages } from "@modrinth/ui";
import { commonMessages, commonSettingsMessages, useRelativeTime } from "@modrinth/ui";
definePageMeta({
middleware: "auth",

View File

@@ -360,6 +360,7 @@ import {
ContentPageHeader,
commonMessages,
NewModal,
useRelativeTime,
} from "@modrinth/ui";
import { isStaff } from "~/helpers/users.js";
import NavTabs from "~/components/ui/NavTabs.vue";