diff --git a/packages/client/src/components/birthday-date.vue b/packages/client/src/components/birthday-date.vue
index 675f3bca6..a8da9f4ac 100644
--- a/packages/client/src/components/birthday-date.vue
+++ b/packages/client/src/components/birthday-date.vue
@@ -4,6 +4,7 @@
diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue
index e7aa0fa4d..cc872096c 100644
--- a/packages/client/src/components/global/time.vue
+++ b/packages/client/src/components/global/time.vue
@@ -10,6 +10,7 @@
import { onUnmounted } from 'vue';
import { i18n } from '@/i18n';
import { lang } from '@/config';
+import { DAY, WEEK, MONTH, YEAR, HOUR, MINUTE, SECOND } from '@/const';
const props = withDefaults(defineProps<{
time: Date | string;
@@ -34,26 +35,26 @@ const absolute = ((): string => {
let now = $ref(new Date());
const relative = $computed(() => {
- const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
+ const ago = now.getTime() - _time.getTime();
- if (ago >= 31536000) {
- return i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() });
- } else if (ago >= 2592000) {
- return i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() });
- } else if (ago >= 604800) {
- return i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() });
- } else if (ago >= 86400) {
- return i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() });
+ if (ago >= YEAR) {
+ return i18n.t('_ago.yearsAgo', { n: Math.round(ago / YEAR).toString() });
+ } else if (ago >= MONTH) {
+ return i18n.t('_ago.monthsAgo', { n: Math.round(ago / MONTH).toString() });
+ } else if (ago >= WEEK) {
+ return i18n.t('_ago.weeksAgo', { n: Math.round(ago / WEEK).toString() });
+ } else if (ago >= DAY) {
+ return i18n.t('_ago.daysAgo', { n: Math.round(ago / DAY).toString() });
}
// if the format is 'date', the relative date precision is no more than days ago
if (props.format !== 'date') {
- if (ago >= 3600) {
- return i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() });
- } else if (ago >= 60) {
- return i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() });
- } else if (ago >= 10) {
- return i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() });
+ if (ago >= HOUR) {
+ return i18n.t('_ago.hoursAgo', { n: Math.round(ago / HOUR).toString() });
+ } else if (ago >= MINUTE) {
+ return i18n.t('_ago.minutesAgo', { n: (~~(ago / MINUTE)).toString() });
+ } else if (ago >= 10 * SECOND) {
+ return i18n.t('_ago.secondsAgo', { n: (~~(ago % MINUTE)).toString() });
}
}
diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue
index b2a98b7ee..e4247f57c 100644
--- a/packages/client/src/components/poll-editor.vue
+++ b/packages/client/src/components/poll-editor.vue
@@ -56,6 +56,7 @@ import MkButton from './ui/button.vue';
import { formatDateTimeString } from '@/scripts/format-time-string';
import { addTime } from '@/scripts/time';
import { i18n } from '@/i18n';
+import { DAY, HOUR, MINUTE, SECOND } from '@/const';
const props = defineProps<{
modelValue: {
@@ -92,11 +93,11 @@ if (props.modelValue.expiresAt) {
expiration.value = 'infinite';
}
-function onInput(i, value) {
+function onInput(i: number, value: string): void {
choices.value[i] = value;
}
-function add() {
+function add(): void {
choices.value.push('');
// TODO
// nextTick(() => {
@@ -104,25 +105,22 @@ function add() {
// });
}
-function remove(i) {
+function remove(i: number): void {
choices.value = choices.value.filter((_, _i) => _i !== i);
}
function get() {
- const calcAt = () => {
+ const calcAt = (): number => {
return new Date(`${atDate.value} ${atTime.value}`).getTime();
};
- const calcAfter = () => {
+ const calcAfter = (): number | null => {
let base = parseInt(after.value);
switch (unit.value) {
- case 'day': base *= 24;
- // fallthrough
- case 'hour': base *= 60;
- // fallthrough
- case 'minute': base *= 60;
- // fallthrough
- case 'second': return base *= 1000;
+ case 'day': return base * DAY;
+ case 'hour': return base * HOUR;
+ case 'minute': return base * MINUTE;
+ case 'second': return base * SECOND;
default: return null;
}
};
diff --git a/packages/client/src/components/signin.vue b/packages/client/src/components/signin.vue
index 6fb37f4fd..112e13bd3 100644
--- a/packages/client/src/components/signin.vue
+++ b/packages/client/src/components/signin.vue
@@ -61,6 +61,7 @@ import * as os from '@/os';
import { login } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
+import { MINUTE } from '@/const';
let signing = $ref(false);
let user = $ref(null);
@@ -124,7 +125,7 @@ function queryKey() {
type: 'public-key',
transports: ['usb', 'nfc', 'ble', 'internal'],
})),
- timeout: 60 * 1000,
+ timeout: MINUTE,
},
}).catch(() => {
queryingKey = false;
diff --git a/packages/client/src/const.ts b/packages/client/src/const.ts
index 505cf2748..51b69f86b 100644
--- a/packages/client/src/const.ts
+++ b/packages/client/src/const.ts
@@ -1,6 +1,15 @@
-// ブラウザで直接表示することを許可するファイルの種類のリスト
-// ここに含まれないものは application/octet-stream としてレスポンスされる
-// SVGはXSSを生むので許可しない
+// Time constants
+export const SECOND = 1000;
+export const MINUTE = 60 * SECOND;
+export const HOUR = 60 * MINUTE;
+export const DAY = 24 * HOUR;
+export const WEEK = 7 * DAY;
+export const MONTH = 30 * DAY;
+export const YEAR = 365 * DAY;
+
+// List of file types allowed to be viewed directly in the browser.
+// Anything not included here will be reported as application/octet-stream
+// SVG is not allowed because it can lead to XSS
export const FILE_TYPE_BROWSERSAFE = [
// Images
'image/png',
diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue
index 4f3730aa6..412122863 100644
--- a/packages/client/src/pages/settings/2fa.vue
+++ b/packages/client/src/pages/settings/2fa.vue
@@ -75,6 +75,7 @@ import MkSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import { $i } from '@/account';
import { i18n } from '@/i18n';
+import { MINUTE } from '@/const';
const twoFactorData = ref(null);
const supportsCredentials = ref(!!navigator.credentials);
@@ -188,7 +189,7 @@ function addSecurityKey() {
displayName: $i!.name,
},
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
- timeout: 60000,
+ timeout: MINUTE,
attestation: 'direct',
},
saving: true,
diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts
index a6fa61b22..3721c5711 100644
--- a/packages/client/src/scripts/get-user-menu.ts
+++ b/packages/client/src/scripts/get-user-menu.ts
@@ -7,11 +7,12 @@ import * as os from '@/os';
import { userActions } from '@/store';
import { $i, iAmModerator } from '@/account';
import { mainRouter } from '@/router';
+import { DAY, HOUR, MINUTE, WEEK } from '@/const';
export function getUserMenu(user) {
const meId = $i ? $i.id : null;
- async function pushList() {
+ async function pushList(): Promise {
const t = i18n.ts.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく
const lists = await os.api('users/lists/list');
if (lists.length === 0) {
@@ -34,7 +35,7 @@ export function getUserMenu(user) {
});
}
- async function inviteGroup() {
+ async function inviteGroup(): Promise {
const groups = await os.api('users/groups/owned');
if (groups.length === 0) {
os.alert({
@@ -56,7 +57,7 @@ export function getUserMenu(user) {
});
}
- async function toggleMute() {
+ async function toggleMute(): Promise {
if (user.isMuted) {
os.apiWithDialog('mute/delete', {
userId: user.id,
@@ -82,10 +83,10 @@ export function getUserMenu(user) {
if (canceled) return;
const expiresAt = period === 'indefinitely' ? null
- : period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
- : period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
- : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
- : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
+ : period === 'tenMinutes' ? Date.now() + (MINUTE * 10)
+ : period === 'oneHour' ? Date.now() + HOUR
+ : period === 'oneDay' ? Date.now() + DAY
+ : period === 'oneWeek' ? Date.now() + WEEK
: null;
os.apiWithDialog('mute/create', {
@@ -97,7 +98,7 @@ export function getUserMenu(user) {
}
}
- async function toggleBlock() {
+ async function toggleBlock(): Promise {
if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', {
@@ -107,7 +108,7 @@ export function getUserMenu(user) {
});
}
- async function toggleSilence() {
+ async function toggleSilence(): Promise {
if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return;
os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
@@ -117,7 +118,7 @@ export function getUserMenu(user) {
});
}
- async function toggleSuspend() {
+ async function toggleSuspend(): Promise {
if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return;
os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
@@ -127,7 +128,7 @@ export function getUserMenu(user) {
});
}
- function reportAbuse() {
+ function reportAbuse(): void {
os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), {
user,
}, {}, 'closed');
@@ -143,7 +144,7 @@ export function getUserMenu(user) {
return !confirm.canceled;
}
- async function invalidateFollow() {
+ async function invalidateFollow(): Promise {
os.apiWithDialog('following/invalidate', {
userId: user.id,
}).then(() => {
diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts
index 147f5d383..0025d26e2 100644
--- a/packages/client/src/scripts/theme.ts
+++ b/packages/client/src/scripts/theme.ts
@@ -13,6 +13,7 @@ export type Theme = {
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
+import { SECOND } from '@/const';
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
@@ -53,7 +54,7 @@ export function applyTheme(theme: Theme, persist = true) {
timeout = window.setTimeout(() => {
document.documentElement.classList.remove('_themeChanging_');
- }, 1000);
+ }, SECOND);
// Deep copy
const _theme = JSON.parse(JSON.stringify(theme));
diff --git a/packages/client/src/scripts/time.ts b/packages/client/src/scripts/time.ts
index e720a7aa9..2e578000c 100644
--- a/packages/client/src/scripts/time.ts
+++ b/packages/client/src/scripts/time.ts
@@ -1,6 +1,8 @@
+import { DAY, HOUR } from '@/const';
+
const dateTimeIntervals = {
- 'day': 86400000,
- 'hour': 3600000,
+ 'day': DAY,
+ 'hour': HOUR,
'ms': 1,
};
diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue
index 7ecc2e78d..130583fe4 100644
--- a/packages/client/src/widgets/memo.vue
+++ b/packages/client/src/widgets/memo.vue
@@ -16,6 +16,7 @@ import { GetFormResultType } from '@/scripts/form';
import MkContainer from '@/components/ui/container.vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
+import { SECOND } from '@/const';
const name = 'memo';
@@ -54,7 +55,7 @@ const saveMemo = (): void => {
const onChange = (): void => {
changed.value = true;
window.clearTimeout(timeoutId);
- timeoutId = window.setTimeout(saveMemo, 1000);
+ timeoutId = window.setTimeout(saveMemo, SECOND);
};
watch(() => defaultStore.reactiveState.memo, newText => {