Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
commit
8aa76868b3
266 changed files with 20375 additions and 6619 deletions
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts.moderator }}:
|
||||
<MkAcct :user="report.assignee"/>
|
||||
</div>
|
||||
<div v-if="report.category">カテゴリ: {{ i18n.t(`_abuseReportCategory.${report.category}`) }}</div>
|
||||
<div v-if="report.category">カテゴリ: {{ i18n.ts[`_abuseReportCategory.${report.category}`] }}</div>
|
||||
<div><MkTime :time="report.createdAt"/></div>
|
||||
<div class="action">
|
||||
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
|
||||
|
|
|
@ -50,7 +50,7 @@ async function gotIt(): Promise<void> {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.t('_announcement.readConfirmText', { title: props.announcement.title }),
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: props.announcement.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span>{{ tag }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<ol v-else-if="mfmParams.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
|
||||
<span>{{ param }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -51,7 +56,7 @@ import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { MFM_TAGS } from '@/const.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
|
@ -130,7 +135,7 @@ export default {
|
|||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
q: string | null;
|
||||
q: any;
|
||||
textarea: HTMLTextAreaElement;
|
||||
close: () => void;
|
||||
x: number;
|
||||
|
@ -151,6 +156,7 @@ const hashtags = ref<any[]>([]);
|
|||
const emojis = ref<(EmojiDef)[]>([]);
|
||||
const items = ref<Element[] | HTMLCollection>([]);
|
||||
const mfmTags = ref<string[]>([]);
|
||||
const mfmParams = ref<string[]>([]);
|
||||
const select = ref(-1);
|
||||
const zIndex = os.claimZIndex('high');
|
||||
|
||||
|
@ -251,6 +257,13 @@ function exec() {
|
|||
}
|
||||
|
||||
mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q ?? ''));
|
||||
} else if (props.type === 'mfmParam') {
|
||||
if (props.q.params.at(-1) === '') {
|
||||
mfmParams.value = MFM_PARAMS[props.q.tag] ?? [];
|
||||
return;
|
||||
}
|
||||
|
||||
mfmParams.value = MFM_PARAMS[props.q.tag].filter(param => param.startsWith(props.q.params.at(-1) ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,9 +41,9 @@ const emit = defineEmits<{
|
|||
|
||||
const label = computed(() => {
|
||||
return concat([
|
||||
props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [],
|
||||
props.text ? [i18n.tsx._cw.chars({ count: props.text.length })] : [],
|
||||
props.renote ? [i18n.ts.quote] : [],
|
||||
props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [],
|
||||
props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [],
|
||||
props.poll != null ? [i18n.ts.poll] : [],
|
||||
] as string[][]).join(' / ');
|
||||
});
|
||||
|
|
|
@ -46,7 +46,7 @@ export default defineComponent({
|
|||
function getDateText(time: string) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
return i18n.t('monthAndDay', {
|
||||
return i18n.tsx.monthAndDay({
|
||||
month: month.toString(),
|
||||
day: date.toString(),
|
||||
});
|
||||
|
|
|
@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInput v-if="input.type !== 'textarea'" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
|
||||
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-if="input.type === 'textarea'" v-model="inputValue" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete">
|
||||
<template #label>{{ input.placeholder }}</template>
|
||||
<template #caption>
|
||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
||||
</template>
|
||||
</MkTextarea>
|
||||
</template>
|
||||
|
|
|
@ -82,8 +82,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
|
||||
</div>
|
||||
<div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
|
||||
<div v-if="draghover">{{ i18n.t('empty-draghover') }}</div>
|
||||
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div>
|
||||
<div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
|
||||
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div>
|
||||
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -84,7 +84,7 @@ async function onClick() {
|
|||
if (isFollowing.value) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
|
||||
text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
|
|
@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
:class="[$style.transitionRoot, (defaultStore.state.animation && $style.enableAnimation)]"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
@touchend="touchEnd"
|
||||
:class="[$style.transitionRoot]"
|
||||
@touchstart.passive="touchStart"
|
||||
@touchmove.passive="touchMove"
|
||||
@touchend.passive="touchEnd"
|
||||
>
|
||||
<Transition
|
||||
:class="[$style.transitionChildren, { [$style.swiping]: isSwipingForClass }]"
|
||||
|
@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
|
||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||
|
@ -154,7 +155,7 @@ function touchEnd(event: TouchEvent) {
|
|||
|
||||
pullDistance.value = 0;
|
||||
isSwiping.value = false;
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
isSwipingForClass.value = false;
|
||||
}, 400);
|
||||
}
|
||||
|
@ -178,28 +179,29 @@ watch(tabModel, (newTab, oldTab) => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transitionRoot.enableAnimation {
|
||||
.transitionRoot {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.transitionChildren {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
transform: translateX(var(--swipe));
|
||||
.transitionChildren {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
transform: translateX(var(--swipe));
|
||||
|
||||
&.swipeAnimation_enterActive,
|
||||
&.swipeAnimation_leaveActive {
|
||||
transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
}
|
||||
&.swipeAnimation_enterActive,
|
||||
&.swipeAnimation_leaveActive {
|
||||
transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
}
|
||||
|
||||
&.swipeAnimationRight_leaveTo,
|
||||
&.swipeAnimationLeft_enterFrom {
|
||||
transform: translateX(calc(100% + 24px));
|
||||
}
|
||||
&.swipeAnimationRight_leaveTo,
|
||||
&.swipeAnimationLeft_enterFrom {
|
||||
transform: translateX(calc(100% + 24px));
|
||||
}
|
||||
|
||||
&.swipeAnimationRight_enterFrom,
|
||||
&.swipeAnimationLeft_leaveTo {
|
||||
transform: translateX(calc(-100% - 24px));
|
||||
}
|
||||
&.swipeAnimationRight_enterFrom,
|
||||
&.swipeAnimationLeft_leaveTo {
|
||||
transform: translateX(calc(-100% - 24px));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@ function close() {
|
|||
left: 32px;
|
||||
color: var(--indicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
animation: global-blink 1s infinite;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
top: 16px;
|
||||
|
|
|
@ -22,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<audio
|
||||
ref="audioEl"
|
||||
preload="metadata"
|
||||
:class="$style.audio"
|
||||
>
|
||||
<source :src="audio.url">
|
||||
</audio>
|
||||
|
|
|
@ -450,7 +450,7 @@ onBeforeUnmount(() => {
|
|||
align-items: center;
|
||||
color: var(--indicator);
|
||||
font-size: 12px;
|
||||
animation: blink 1s infinite;
|
||||
animation: global-blink 1s infinite;
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="!muted"
|
||||
v-if="muted === false"
|
||||
v-show="!isDeleted"
|
||||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
|
@ -73,7 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -134,7 +134,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</article>
|
||||
</div>
|
||||
<div v-else :class="$style.muted" :style="hideMutedNotes ? 'display: none' : undefined" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
|
@ -196,6 +203,7 @@ const emit = defineEmits<{
|
|||
(ev: 'removeReaction', emoji: string): void;
|
||||
}>();
|
||||
|
||||
const inTimeline = inject<boolean>('inTimeline', false);
|
||||
const inChannel = inject('inChannel', null);
|
||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
|
@ -250,12 +258,20 @@ const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.vi
|
|||
const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
|
||||
const hideMutedNotes = defaultStore.state.hideMutedNotes;
|
||||
|
||||
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||
*/
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
|
||||
if (mutedWords == null) return false;
|
||||
|
||||
if (checkWordMute(note, $i, mutedWords)) return true;
|
||||
if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
|
||||
if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
|
||||
if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
|
||||
if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
|
||||
if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
|
||||
|
||||
if (checkOnly) return false;
|
||||
|
||||
if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -56,8 +56,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.t('_notification.reactedBySomeUsers', { n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.t('_notification.renotedBySomeUsers', { n: notification.users.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
<span v-else>{{ notification.header }}</span>
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
</header>
|
||||
|
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
</div>
|
||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.ts._notification._types[ntype] }}</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
|
|
|
@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span :class="$style.fg">
|
||||
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
|
||||
<Mfm :text="choice.text" :plain="true"/>
|
||||
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="!readOnly" :class="$style.info">
|
||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span>
|
||||
<span> · </span>
|
||||
<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||
|
@ -47,10 +47,11 @@ const remaining = ref(-1);
|
|||
const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
|
||||
const closed = computed(() => remaining.value === 0);
|
||||
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
|
||||
const timer = computed(() => i18n.t(
|
||||
remaining.value >= 86400 ? '_poll.remainingDays' :
|
||||
remaining.value >= 3600 ? '_poll.remainingHours' :
|
||||
remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
|
||||
const timer = computed(() => i18n.tsx._poll[
|
||||
remaining.value >= 86400 ? 'remainingDays' :
|
||||
remaining.value >= 3600 ? 'remainingHours' :
|
||||
remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
|
||||
]({
|
||||
s: Math.floor(remaining.value % 60),
|
||||
m: Math.floor(remaining.value / 60) % 60,
|
||||
h: Math.floor(remaining.value / 3600) % 24,
|
||||
|
@ -81,7 +82,7 @@ const vote = async (id) => {
|
|||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
|
||||
text: i18n.tsx.voteConfirm({ choice: props.note.poll.choices[id].text }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</p>
|
||||
<ul>
|
||||
<li v-for="(choice, i) in choices" :key="i">
|
||||
<MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
<MkInput class="input" small :modelValue="choice" :placeholder="i18n.tsx._poll.choiceN({ n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
</MkInput>
|
||||
<button class="_button" @click="remove(i)">
|
||||
<i class="ti ti-x"></i>
|
||||
|
|
|
@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||
</div>
|
||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
<MkInfo v-if="files.length > 0" warn :class="$style.guidelineInfo" :rounded="false"><Mfm :text="i18n.t('_postForm.guidelineInfo', { tosUrl: instance.tosUrl, nsfwGuideUrl })"/></MkInfo>
|
||||
<MkInfo v-if="files.length > 0" warn :class="$style.guidelineInfo" :rounded="false"><Mfm :text="i18n.tsx._postForm.guidelineInfo({ tosUrl: instance.tosUrl, nsfwGuideUrl })"/></MkInfo>
|
||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||
|
|
|
@ -56,6 +56,23 @@ function detachMedia(id: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) {
|
||||
if (mock) return;
|
||||
|
||||
detachMedia(file.id);
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('drive/files/delete', {
|
||||
fileId: file.id,
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSensitive(file) {
|
||||
if (mock) {
|
||||
emit('changeSensitive', file, !file.isSensitive);
|
||||
|
@ -138,6 +155,13 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
|
|||
text: i18n.ts.attachCancel,
|
||||
icon: 'ti ti-circle-x',
|
||||
action: () => { detachMedia(file.id); },
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.deleteFile,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => { detachAndDeleteMedia(file); },
|
||||
}], ev.currentTarget ?? ev.target).then(() => menuShowing = false);
|
||||
menuShowing = true;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ export default defineComponent({
|
|||
watch(value, () => {
|
||||
context.emit('update:modelValue', value.value);
|
||||
});
|
||||
watch(() => props.modelValue, v => {
|
||||
value.value = v;
|
||||
});
|
||||
if (!context.slots.default) return null;
|
||||
let options = context.slots.default();
|
||||
const label = context.slots.label && context.slots.label();
|
||||
|
|
|
@ -51,7 +51,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'change', _ev: KeyboardEvent): void;
|
||||
(ev: 'changeByUser'): void;
|
||||
(ev: 'update:modelValue', value: string | null): void;
|
||||
}>();
|
||||
|
||||
|
@ -76,7 +76,6 @@ const height =
|
|||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev) => {
|
||||
changed.value = true;
|
||||
emit('change', ev);
|
||||
};
|
||||
|
||||
const updated = () => {
|
||||
|
@ -138,6 +137,7 @@ function show(ev: MouseEvent) {
|
|||
active: computed(() => v.value === option.props.value),
|
||||
action: () => {
|
||||
v.value = option.props.value;
|
||||
emit('changeByUser', v.value);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -263,7 +263,7 @@ async function onSubmit(): Promise<void> {
|
|||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.t('_signup.emailSent', { email: email.value }),
|
||||
text: i18n.tsx._signup.emailSent({ email: email.value }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else {
|
||||
|
|
|
@ -105,7 +105,7 @@ async function updateAgreeServerRules(v: boolean) {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.serverRules }),
|
||||
text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.serverRules }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
agreeServerRules.value = true;
|
||||
|
@ -119,7 +119,7 @@ async function updateAgreeTosAndPrivacyPolicy(v: boolean) {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', {
|
||||
text: i18n.tsx.iHaveReadXCarefullyAndAgree({
|
||||
x: tosPrivacyPolicyLabel.value,
|
||||
}),
|
||||
});
|
||||
|
@ -135,7 +135,7 @@ async function updateAgreeNote(v: boolean) {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.basicNotesBeforeCreateAccount }),
|
||||
text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.basicNotesBeforeCreateAccount }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
agreeNote.value = true;
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
|
||||
<MkMediaList :mediaList="note.files"/>
|
||||
</details>
|
||||
<details v-if="note.poll">
|
||||
|
|
|
@ -49,6 +49,7 @@ const emit = defineEmits<{
|
|||
(ev: 'queue', count: number): void;
|
||||
}>();
|
||||
|
||||
provide('inTimeline', true);
|
||||
provide('inChannel', computed(() => props.src === 'channel'));
|
||||
|
||||
type TimelineQueryType = {
|
||||
|
|
|
@ -33,12 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
</div>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
|
||||
</div>
|
||||
<div v-if="iAmAdmin" :class="$style.adminPermissions">
|
||||
<div :class="$style.adminPermissionsHeader"><b>{{ i18n.ts.adminPermission }}</b></div>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<a href="https://misskey-hub.net/docs/for-users/" rel="nofollow noopener" target="_blank" class="_link">{{ i18n.ts.help }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
|
||||
<div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
|
||||
|
|
|
@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts._announcement.silence }}
|
||||
<template #caption>{{ i18n.ts._announcement.silenceDescription }}</template>
|
||||
</MkSwitch>
|
||||
<p v-if="reads">{{ i18n.t('nUsersRead', { n: reads }) }}</p>
|
||||
<p v-if="reads">{{ i18n.tsx.nUsersRead({ n: reads }) }}</p>
|
||||
<MkUserCardMini v-if="props.user.id" :user="props.user"></MkUserCardMini>
|
||||
<MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
|
@ -149,7 +149,7 @@ async function done(): Promise<void> {
|
|||
async function del(): Promise<void> {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: title.value }),
|
||||
text: i18n.tsx.removeAreYouSure({ x: title.value }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
|
|||
const selected = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
const dialogEl = ref();
|
||||
|
||||
const search = () => {
|
||||
function search() {
|
||||
if (username.value === '' && host.value === '') {
|
||||
users.value = [];
|
||||
return;
|
||||
|
@ -98,9 +98,9 @@ const search = () => {
|
|||
}).then(_users => {
|
||||
users.value = _users;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const ok = () => {
|
||||
function ok() {
|
||||
if (selected.value == null) return;
|
||||
emit('ok', selected.value);
|
||||
dialogEl.value.close();
|
||||
|
@ -110,12 +110,12 @@ const ok = () => {
|
|||
recents = recents.filter(x => x !== selected.value.id);
|
||||
recents.unshift(selected.value.id);
|
||||
defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
|
||||
};
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
dialogEl.value.close();
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
misskeyApi('users/show', {
|
||||
|
|
|
@ -68,7 +68,7 @@ function setAvatar(ev) {
|
|||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('cropImageAsk'),
|
||||
text: i18n.ts.cropImageAsk,
|
||||
okText: i18n.ts.cropYes,
|
||||
cancelText: i18n.ts.cropNo,
|
||||
});
|
||||
|
|
|
@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
|
||||
<div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div>
|
||||
<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
|
||||
<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
|
||||
<div>{{ i18n.t('_initialAccountSetting.youCanContinueTutorial', { name: instance.name ?? host }) }}</div>
|
||||
<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<header :class="$style.editHeader">
|
||||
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
|
||||
<template #label>{{ i18n.ts.selectWidget }}</template>
|
||||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
|
||||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
||||
</MkSelect>
|
||||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
|
@ -109,7 +109,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
|||
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: i18n.t(`_widgets.${widget.name}`),
|
||||
text: i18n.ts._widgets[widget.name],
|
||||
}, {
|
||||
icon: 'ti ti-settings',
|
||||
text: i18n.ts.settings,
|
||||
|
|
46
packages/frontend/src/components/global/I18n.vue
Normal file
46
packages/frontend/src/components/global/I18n.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<render/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends string | ParameterizedString">
|
||||
import { computed, h } from 'vue';
|
||||
import type { ParameterizedString } from '../../../../../locales/index.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: T;
|
||||
tag?: string;
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
textTag?: string;
|
||||
}>(), {
|
||||
tag: 'span',
|
||||
});
|
||||
|
||||
const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: () => unknown } : NonNullable<unknown>>();
|
||||
|
||||
const parsed = computed(() => {
|
||||
let str = props.src as string;
|
||||
const value: (string | { arg: string; })[] = [];
|
||||
for (;;) {
|
||||
const nextBracketOpen = str.indexOf('{');
|
||||
const nextBracketClose = str.indexOf('}');
|
||||
|
||||
if (nextBracketOpen === -1) {
|
||||
value.push(str);
|
||||
break;
|
||||
} else {
|
||||
if (nextBracketOpen > 0) value.push(str.substring(0, nextBracketOpen));
|
||||
value.push({
|
||||
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
|
||||
});
|
||||
}
|
||||
|
||||
str = str.substring(nextBracketClose + 1);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
|
||||
};
|
||||
</script>
|
|
@ -118,7 +118,7 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
|
|||
case 'tada': {
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
|
||||
style = 'font-size: 150%;' + (useAnim ? `animation: global-tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
|
||||
break;
|
||||
}
|
||||
case 'jelly': {
|
||||
|
|
|
@ -123,7 +123,7 @@ export const DetailNow = {
|
|||
export const RelativeOneHourAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.hoursAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
@ -162,7 +162,7 @@ export const DetailOneHourAgo = {
|
|||
export const RelativeOneDayAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.daysAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
@ -201,7 +201,7 @@ export const DetailOneDayAgo = {
|
|||
export const RelativeOneWeekAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.weeksAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
@ -240,7 +240,7 @@ export const DetailOneWeekAgo = {
|
|||
export const RelativeOneMonthAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.monthsAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
@ -279,7 +279,7 @@ export const DetailOneMonthAgo = {
|
|||
export const RelativeOneYearAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.yearsAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
|
|
@ -55,21 +55,21 @@ const relative = computed<string>(() => {
|
|||
if (invalid) return i18n.ts._ago.invalid;
|
||||
|
||||
return (
|
||||
ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) :
|
||||
ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) :
|
||||
ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) :
|
||||
ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) :
|
||||
ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) :
|
||||
ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) :
|
||||
ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) :
|
||||
ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) :
|
||||
ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) :
|
||||
ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) :
|
||||
ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) :
|
||||
ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) :
|
||||
ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) :
|
||||
ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) :
|
||||
ago.value >= -3 ? i18n.ts._ago.justNow :
|
||||
ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) :
|
||||
ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) :
|
||||
ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) :
|
||||
ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) :
|
||||
ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) :
|
||||
ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) :
|
||||
i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() })
|
||||
ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) :
|
||||
ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) :
|
||||
ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) :
|
||||
ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) :
|
||||
ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) :
|
||||
ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) :
|
||||
i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
|
||||
let str = props.src;
|
||||
const parsed = [] as (string | { arg: string; })[];
|
||||
while (true) {
|
||||
const nextBracketOpen = str.indexOf('{');
|
||||
const nextBracketClose = str.indexOf('}');
|
||||
|
||||
if (nextBracketOpen === -1) {
|
||||
parsed.push(str);
|
||||
break;
|
||||
} else {
|
||||
if (nextBracketOpen > 0) parsed.push(str.substring(0, nextBracketOpen));
|
||||
parsed.push({
|
||||
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
|
||||
});
|
||||
}
|
||||
|
||||
str = str.substring(nextBracketClose + 1);
|
||||
}
|
||||
|
||||
return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
|
||||
}
|
|
@ -16,7 +16,7 @@ import MkUserName from './global/MkUserName.vue';
|
|||
import MkEllipsis from './global/MkEllipsis.vue';
|
||||
import MkTime from './global/MkTime.vue';
|
||||
import MkUrl from './global/MkUrl.vue';
|
||||
import I18n from './global/i18n.js';
|
||||
import I18n from './global/I18n.vue';
|
||||
import RouterView from './global/RouterView.vue';
|
||||
import MkLoading from './global/MkLoading.vue';
|
||||
import MkError from './global/MkError.vue';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue