diff --git a/package-lock.json b/package-lock.json index d88317a06..b2209c9d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "@nextcloud/files": "^3.10.2", "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.2.0", - "@nextcloud/moment": "^1.3.2", "@nextcloud/notify_push": "^1.3.0", "@nextcloud/router": "^3.0.1", "@nextcloud/vue": "^8.23.1", @@ -3769,49 +3768,6 @@ "npm": "^10.0.0" } }, - "node_modules/@nextcloud/moment": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.3.2.tgz", - "integrity": "sha512-VfSPnllfciZe1eU4zaHS0fE/4pPWKRUjLFxZSNQec9gkUfbskMsKH2xyPqkYLlYP9FF1uQh2+wZbzkFd6QLc4A==", - "dependencies": { - "@nextcloud/l10n": "^2.2.0", - "moment": "^2.30.1", - "node-gettext": "^3.0.0" - }, - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - } - }, - "node_modules/@nextcloud/moment/node_modules/@nextcloud/l10n": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-2.2.0.tgz", - "integrity": "sha512-UAM2NJcl/NR46MANSF7Gr7q8/Up672zRyGrxLpN3k4URNmWQM9upkbRME+1K3T29wPrUyOIbQu710ZjvZafqFA==", - "dependencies": { - "@nextcloud/router": "^2.1.2", - "@nextcloud/typings": "^1.7.0", - "dompurify": "^3.0.3", - "escape-html": "^1.0.3", - "node-gettext": "^3.0.0" - }, - "engines": { - "node": "^20.0.0", - "npm": "^9.0.0" - } - }, - "node_modules/@nextcloud/moment/node_modules/@nextcloud/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.2.0.tgz", - "integrity": "sha512-M4AVGnB5tt3MYO5RpH/R2jq7z/nW05AmRhk4Lh68krVwRIYGo8pgNikKrPGogHd2Q3UgzF5Py1drHz3uuV99bQ==", - "dependencies": { - "@nextcloud/typings": "^1.7.0", - "core-js": "^3.6.4" - }, - "engines": { - "node": "^20.0.0", - "npm": "^9.0.0" - } - }, "node_modules/@nextcloud/notify_push": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.3.0.tgz", @@ -12758,6 +12714,7 @@ }, "node_modules/lodash.get": { "version": "4.4.2", + "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -14227,14 +14184,6 @@ "node": ">=10" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.0.0", "license": "MIT" @@ -14410,12 +14359,6 @@ "node": ">= 6.13.0" } }, - "node_modules/node-gettext": { - "version": "3.0.0", - "dependencies": { - "lodash.get": "^4.4.2" - } - }, "node_modules/node-gyp-build": { "version": "4.5.0", "dev": true, @@ -22547,39 +22490,6 @@ "@nextcloud/auth": "^2.3.0" } }, - "@nextcloud/moment": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.3.2.tgz", - "integrity": "sha512-VfSPnllfciZe1eU4zaHS0fE/4pPWKRUjLFxZSNQec9gkUfbskMsKH2xyPqkYLlYP9FF1uQh2+wZbzkFd6QLc4A==", - "requires": { - "@nextcloud/l10n": "^2.2.0", - "moment": "^2.30.1", - "node-gettext": "^3.0.0" - }, - "dependencies": { - "@nextcloud/l10n": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-2.2.0.tgz", - "integrity": "sha512-UAM2NJcl/NR46MANSF7Gr7q8/Up672zRyGrxLpN3k4URNmWQM9upkbRME+1K3T29wPrUyOIbQu710ZjvZafqFA==", - "requires": { - "@nextcloud/router": "^2.1.2", - "@nextcloud/typings": "^1.7.0", - "dompurify": "^3.0.3", - "escape-html": "^1.0.3", - "node-gettext": "^3.0.0" - } - }, - "@nextcloud/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.2.0.tgz", - "integrity": "sha512-M4AVGnB5tt3MYO5RpH/R2jq7z/nW05AmRhk4Lh68krVwRIYGo8pgNikKrPGogHd2Q3UgzF5Py1drHz3uuV99bQ==", - "requires": { - "@nextcloud/typings": "^1.7.0", - "core-js": "^3.6.4" - } - } - } - }, "@nextcloud/notify_push": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.3.0.tgz", @@ -28982,7 +28892,8 @@ "dev": true }, "lodash.get": { - "version": "4.4.2" + "version": "4.4.2", + "dev": true }, "lodash.merge": { "version": "4.6.2", @@ -29928,11 +29839,6 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true }, - "moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" - }, "ms": { "version": "2.0.0" }, @@ -30054,12 +29960,6 @@ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "dev": true }, - "node-gettext": { - "version": "3.0.0", - "requires": { - "lodash.get": "^4.4.2" - } - }, "node-gyp-build": { "version": "4.5.0", "dev": true, diff --git a/package.json b/package.json index b635ca1d5..730639e28 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "@nextcloud/files": "^3.10.2", "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.2.0", - "@nextcloud/moment": "^1.3.2", "@nextcloud/notify_push": "^1.3.0", "@nextcloud/router": "^3.0.1", "@nextcloud/vue": "^8.23.1", diff --git a/src/shared/datetime.utils.ts b/src/shared/datetime.utils.ts new file mode 100644 index 000000000..4e3d2deda --- /dev/null +++ b/src/shared/datetime.utils.ts @@ -0,0 +1,106 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getCanonicalLocale } from '@nextcloud/l10n' + +const locale = getCanonicalLocale() + +const relativeTimeFormat = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }) + +/** + * Format duration in human-readable format. + * If the unit is not provided, the largest unit is used for rounded duration in milliseconds. + * @param duration - Duration in the unit + * @param unit - Unit to format to + */ +export function formatDuration(duration: number, unit?: Intl.RelativeTimeFormatUnit) { + if (!unit) { + const { value, unit: largestUnit } = convertMsToLargestTimeUnit(duration) + duration = value + unit = largestUnit + } + + return new Intl.NumberFormat(locale, { style: 'unit', unit, unitDisplay: 'long' }).format(duration) +} + +/** + * Format duration in human-readable format from now. + * If the unit is not provided, the largest unit is used for rounded duration in milliseconds. + * @param dateOrMs - Date or ms to format + * @param unit - Unit to format to + */ +export function formatDurationFromNow(dateOrMs: Date | number, unit?: Intl.RelativeTimeFormatUnit) { + return formatDuration(+new Date(dateOrMs) - Date.now(), unit) +} + +/** + * Format relative time duration in human-readable format + * @param ms - Duration in milliseconds + */ +export function formatRelativeTime(ms: number) { + const { value, unit } = convertMsToLargestTimeUnit(ms) + + return relativeTimeFormat.format(value, unit) +} + +/** + * Format relative time duration in human-readable format from now + * @param dateOrMs - Date or ms to format + */ +export function formatRelativeTimeFromNow(dateOrMs: Date | number) { + return formatRelativeTime(+new Date(dateOrMs) - Date.now()) +} + +/** + * Convert milliseconds to the largest unit rounded from 0.75 point. + * @example 123 -> { value: 0, unit: 'second' } + * @example 1000 -> { value: 1, unit: 'second' } + * @example 25 * 60 * 60 * 1000 -> { value: 25, unit: 'minute' } + * @example 35 * 60 * 60 * 1000 -> { value: 35, unit: 'minute' } + * @example 45 * 60 * 60 * 1000 -> { value: 1, unit: 'hour' } + * @example 3600000 -> { value: 1, unit: 'hour' } + * @example 86400000 -> { value: 1, unit: 'day' } + * @param ms - Duration in milliseconds + */ +export function convertMsToLargestTimeUnit(ms: number): { value: number; unit: Intl.RelativeTimeFormatUnit } { + const units = { + year: 0, + month: 0, + day: 0, + hour: 0, + minute: 0, + second: 0, + } + + units.second = ms / 1000 + units.minute = units.second / 60 + units.hour = units.minute / 60 + units.day = units.hour / 24 + units.month = units.day / 30 + units.year = units.day / 365 + + // + const round = (value: number) => Math.abs(value % 1) < 0.75 ? Math.trunc(value) : Math.round(value) + + // Loop from the largest unit to the smallest + for (const key in units) { + const unit = key as keyof typeof units + // Round the value so 59 min 59 sec 999 ms is 1 hour and not 59 minutes + const rounded = round(units[unit]) + // Return the first non-zero unit + if (rounded !== 0) { + return { + value: rounded, + unit, + } + } + } + + // now + return { + value: 0, + unit: 'second', + } +} diff --git a/src/talk/renderer/UserStatus/components/UserStatusFormClearAt.vue b/src/talk/renderer/UserStatus/components/UserStatusFormClearAt.vue index 5feb40bdd..b1aac7c1b 100644 --- a/src/talk/renderer/UserStatus/components/UserStatusFormClearAt.vue +++ b/src/talk/renderer/UserStatus/components/UserStatusFormClearAt.vue @@ -8,6 +8,7 @@ import { computed } from 'vue' import NcSelect from '@nextcloud/vue/components/NcSelect' import { translate as t } from '@nextcloud/l10n' import { clearAtToLabel, getTimestampForPredefinedClearAt } from '../userStatus.utils.ts' +import { formatDuration } from '../../../../shared/datetime.utils.ts' const props = withDefaults(defineProps<{ clearAt?: number | null, @@ -25,19 +26,19 @@ const clearAtOptions = [{ label: t('talk_desktop', 'Don\'t clear'), clearAt: null, }, { - label: t('talk_desktop', '30 minutes'), + label: formatDuration(1800 * 1000), // 30 minutes clearAt: { type: 'period', time: 1800, }, }, { - label: t('talk_desktop', '1 hour'), + label: formatDuration(3600 * 1000), // 1 hour clearAt: { type: 'period', time: 3600, }, }, { - label: t('talk_desktop', '4 hours'), + label: formatDuration(14400 * 1000), // 4 hours clearAt: { type: 'period', time: 14400, diff --git a/src/talk/renderer/UserStatus/userStatus.utils.ts b/src/talk/renderer/UserStatus/userStatus.utils.ts index cefcf19c9..35121a3f4 100644 --- a/src/talk/renderer/UserStatus/userStatus.utils.ts +++ b/src/talk/renderer/UserStatus/userStatus.utils.ts @@ -4,8 +4,8 @@ */ import type { ClearAtPredefinedConfig, PredefinedUserStatus, UserStatus, UserStatusStatusType } from './userStatus.types.ts' -import moment from '@nextcloud/moment' -import { translate as t } from '@nextcloud/l10n' +import { t, getFirstDay } from '@nextcloud/l10n' +import { formatDurationFromNow, formatDuration } from '../../../shared/datetime.utils.ts' /** * List of user status types that user can set @@ -58,11 +58,17 @@ export function getTimestampForPredefinedClearAt(clearAt: ClearAtPredefinedConfi } if (clearAt.type === 'end-of') { - switch (clearAt.time) { - case 'day': - case 'week': - return Number(moment(new Date()).endOf(clearAt.time).format('X')) + const date = new Date() + // In any case, set the end of the day + date.setHours(23, 59, 59, 999) + + if (clearAt.time === 'week') { + const firstDay = getFirstDay() + const lastDay = (firstDay + 6) % 7 + date.setDate(date.getDate() + (lastDay + 7 - date.getDay()) % 7) } + + return Math.floor(date.getTime() / 1000) } // Unknown type @@ -82,9 +88,7 @@ export function clearAtToLabel(clearAt: ClearAtPredefinedConfig | number | null) // Clear At has been already set if (typeof clearAt === 'number') { - const momentNow = moment(new Date()) - const momentClearAt = moment(new Date(clearAt * 1000)) - return moment.duration(momentNow.diff(momentClearAt)).humanize() + return formatDurationFromNow(clearAt * 1000) } // ClearAt is an object description of predefined value @@ -98,7 +102,7 @@ export function clearAtToLabel(clearAt: ClearAtPredefinedConfig | number | null) // ClearAt is an object description of predefined value if (clearAt.type === 'period') { - return moment.duration(clearAt.time * 1000).humanize() + return formatDuration(clearAt.time * 1000) } return ''