summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/pages')
-rw-r--r--packages/frontend/src/pages/about.federation.vue2
-rw-r--r--packages/frontend/src/pages/admin-user.vue12
-rw-r--r--packages/frontend/src/pages/admin/RolesEditorFormula.vue101
-rw-r--r--packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue4
-rw-r--r--packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue6
-rw-r--r--packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue2
-rw-r--r--packages/frontend/src/pages/admin/abuses.vue2
-rw-r--r--packages/frontend/src/pages/admin/ads.vue33
-rw-r--r--packages/frontend/src/pages/admin/announcements.vue77
-rw-r--r--packages/frontend/src/pages/admin/bot-protection.vue18
-rw-r--r--packages/frontend/src/pages/admin/branding.vue22
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue9
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.register.vue3
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue2
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager2.vue4
-rw-r--r--packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue4
-rw-r--r--packages/frontend/src/pages/admin/index.vue2
-rw-r--r--packages/frontend/src/pages/admin/job-queue.vue6
-rw-r--r--packages/frontend/src/pages/admin/overview.active-users.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.vue2
-rw-r--r--packages/frontend/src/pages/admin/roles.edit.vue15
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue15
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue27
-rw-r--r--packages/frontend/src/pages/admin/security.vue14
-rw-r--r--packages/frontend/src/pages/admin/server-rules.vue48
-rw-r--r--packages/frontend/src/pages/admin/settings.vue12
-rw-r--r--packages/frontend/src/pages/admin/system-webhook.item.vue8
-rw-r--r--packages/frontend/src/pages/admin/users.vue3
-rw-r--r--packages/frontend/src/pages/announcements.vue9
-rw-r--r--packages/frontend/src/pages/api-console.vue2
-rw-r--r--packages/frontend/src/pages/auth.form.vue8
-rw-r--r--packages/frontend/src/pages/auth.vue2
-rw-r--r--packages/frontend/src/pages/avatar-decoration-edit-dialog.vue6
-rw-r--r--packages/frontend/src/pages/avatar-decorations.vue4
-rw-r--r--packages/frontend/src/pages/channel-editor.vue40
-rw-r--r--packages/frontend/src/pages/channels.vue17
-rw-r--r--packages/frontend/src/pages/chat/XMessage.vue8
-rw-r--r--packages/frontend/src/pages/chat/home.home.vue6
-rw-r--r--packages/frontend/src/pages/chat/room.form.vue2
-rw-r--r--packages/frontend/src/pages/chat/room.vue2
-rw-r--r--packages/frontend/src/pages/clip.vue9
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue43
-rw-r--r--packages/frontend/src/pages/drive.file.info.vue5
-rw-r--r--packages/frontend/src/pages/drop-and-fusion.game.vue6
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue30
-rw-r--r--packages/frontend/src/pages/emojis.emoji.vue22
-rw-r--r--packages/frontend/src/pages/flash/flash-edit.vue2
-rw-r--r--packages/frontend/src/pages/flash/flash.vue20
-rw-r--r--packages/frontend/src/pages/follow-requests.vue8
-rw-r--r--packages/frontend/src/pages/gallery/edit.root.vue4
-rw-r--r--packages/frontend/src/pages/gallery/post.vue4
-rw-r--r--packages/frontend/src/pages/my-lists/list.vue4
-rw-r--r--packages/frontend/src/pages/notifications.vue7
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.blocks.vue41
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.vue4
-rw-r--r--packages/frontend/src/pages/page.vue8
-rw-r--r--packages/frontend/src/pages/qr.read.vue2
-rw-r--r--packages/frontend/src/pages/reversi/game.board.vue23
-rw-r--r--packages/frontend/src/pages/reversi/game.setting.vue68
-rw-r--r--packages/frontend/src/pages/reversi/game.vue6
-rw-r--r--packages/frontend/src/pages/reversi/index.vue12
-rw-r--r--packages/frontend/src/pages/scratchpad.vue2
-rw-r--r--packages/frontend/src/pages/search.note.vue29
-rw-r--r--packages/frontend/src/pages/search.user.vue14
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue7
-rw-r--r--packages/frontend/src/pages/settings/account-data.vue12
-rw-r--r--packages/frontend/src/pages/settings/accounts.vue4
-rw-r--r--packages/frontend/src/pages/settings/apps.vue4
-rw-r--r--packages/frontend/src/pages/settings/deck.vue34
-rw-r--r--packages/frontend/src/pages/settings/drive-cleaner.vue11
-rw-r--r--packages/frontend/src/pages/settings/drive.ImageFrameItem.vue2
-rw-r--r--packages/frontend/src/pages/settings/drive.WatermarkItem.vue2
-rw-r--r--packages/frontend/src/pages/settings/drive.vue5
-rw-r--r--packages/frontend/src/pages/settings/email.vue4
-rw-r--r--packages/frontend/src/pages/settings/emoji-palette.palette.vue33
-rw-r--r--packages/frontend/src/pages/settings/emoji-palette.vue57
-rw-r--r--packages/frontend/src/pages/settings/index.vue8
-rw-r--r--packages/frontend/src/pages/settings/mute-block.emoji-mute.vue6
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue7
-rw-r--r--packages/frontend/src/pages/settings/mute-block.word-mute.vue8
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue39
-rw-r--r--packages/frontend/src/pages/settings/notifications.vue22
-rw-r--r--packages/frontend/src/pages/settings/other.vue8
-rw-r--r--packages/frontend/src/pages/settings/plugin.vue4
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue117
-rw-r--r--packages/frontend/src/pages/settings/profile.vue43
-rw-r--r--packages/frontend/src/pages/settings/profiles.vue9
-rw-r--r--packages/frontend/src/pages/settings/sounds.sound.vue4
-rw-r--r--packages/frontend/src/pages/settings/sounds.vue11
-rw-r--r--packages/frontend/src/pages/settings/statusbar.statusbar.vue16
-rw-r--r--packages/frontend/src/pages/settings/theme.vue2
-rw-r--r--packages/frontend/src/pages/tag.vue5
-rw-r--r--packages/frontend/src/pages/theme-editor.vue4
-rw-r--r--packages/frontend/src/pages/timeline.vue13
-rw-r--r--packages/frontend/src/pages/user-tag.vue1
-rw-r--r--packages/frontend/src/pages/user/activity.following.vue2
-rw-r--r--packages/frontend/src/pages/user/activity.notes.vue2
-rw-r--r--packages/frontend/src/pages/user/activity.pv.vue2
-rw-r--r--packages/frontend/src/pages/user/home.vue23
-rw-r--r--packages/frontend/src/pages/user/index.activity.vue2
-rw-r--r--packages/frontend/src/pages/user/index.files.vue2
-rw-r--r--packages/frontend/src/pages/user/notes.vue2
-rw-r--r--packages/frontend/src/pages/welcome.setup.vue8
103 files changed, 861 insertions, 618 deletions
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue
index bbfb9a3b7c..c109000108 100644
--- a/packages/frontend/src/pages/about.federation.vue
+++ b/packages/frontend/src/pages/about.federation.vue
@@ -97,7 +97,7 @@ const paginator = markRaw(new Paginator('federation/instances', {
})),
}));
-function getStatus(instance) {
+function getStatus(instance: Misskey.entities.FederationInstance) {
if (instance.isSuspended) return 'Suspended';
if (instance.isBlocked) return 'Blocked';
if (instance.isSilenced) return 'Silenced';
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 22e377c75d..b084eb5ab2 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._role.policies }}</template>
<div class="_gaps">
<div v-for="policy in Object.keys(info.policies)" :key="policy">
- {{ policy }} ... {{ info.policies[policy] }}
+ {{ policy }} ... {{ info.policies[policy as keyof typeof info.policies] }}
</div>
</div>
</MkFolder>
@@ -209,6 +209,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, defineAsyncComponent, watch, ref, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
+import type { ChartSrc } from '@/components/MkChart.vue';
import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -231,7 +232,6 @@ import { ensureSignin, iAmAdmin, iAmModerator } from '@/i.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
-import type { ChartSrc } from '@/components/MkChart.vue';
const $i = ensureSignin();
@@ -251,7 +251,7 @@ const {
} = useMkSelect({
items: [
{ label: i18n.ts.notes, value: 'per-user-notes' },
-],
+ ],
initialValue: 'per-user-notes',
});
const user = ref(result.user);
@@ -344,7 +344,7 @@ async function resetPassword() {
}
}
-async function toggleSuspend(v) {
+async function toggleSuspend(v: boolean) {
const confirm = await os.confirm({
type: 'warning',
text: v ? i18n.ts.suspendConfirm : i18n.ts.unsuspendConfirm,
@@ -475,7 +475,7 @@ async function assignRole() {
refreshUser();
}
-async function unassignRole(role: typeof info.value.roles[number], ev: MouseEvent) {
+async function unassignRole(role: typeof info.value.roles[number], ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.unassign,
icon: 'ti ti-x',
@@ -503,7 +503,7 @@ async function createAnnouncement() {
});
}
-async function editAnnouncement(announcement) {
+async function editAnnouncement(announcement: Misskey.entities.AdminAnnouncementsListResponse[number]) {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkUserAnnouncementEditDialog.vue').then(x => x.default), {
user: user.value,
announcement,
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index 9d9db9158d..384282262d 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div :class="$style.header">
- <MkSelect v-model="type" :items="typeDef" :class="$style.typeSelect">
+ <MkSelect v-model="typeModelForMkSelect" :items="typeDef" :class="$style.typeSelect">
</MkSelect>
- <button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle">
+ <button v-if="draggable" class="_button" :class="$style.dragHandle" :draggable="true" @dragstart.stop="dragStartCallback">
<i class="ti ti-menu-2"></i>
</button>
<button v-if="draggable" class="_button" :class="$style.remove" @click="removeSelf">
@@ -16,55 +16,69 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</div>
- <div v-if="type === 'and' || type === 'or'" class="_gaps">
- <Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5">
- <template #item="{element}">
+ <div v-if="v.type === 'and' || v.type === 'or'" class="_gaps">
+ <MkDraggable
+ v-model="v.values"
+ direction="vertical"
+ withGaps
+ canNest
+ manualDragStart
+ group="roleFormula"
+ >
+ <template #default="{ item, dragStart }">
<div :class="$style.item">
- <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
- <RolesEditorFormula :modelValue="element" draggable @update:modelValue="updated => valuesItemUpdated(updated)" @remove="removeItem(element)"/>
+ <!-- divが無いとエラーになる -->
+ <RolesEditorFormula
+ :modelValue="item"
+ :dragStartCallback="dragStart"
+ draggable
+ @update:modelValue="updated => childValuesItemUpdated(updated)"
+ @remove="removeChildItem(item.id)"
+ />
</div>
</template>
- </Sortable>
- <MkButton rounded style="margin: 0 auto;" @click="addValue"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+ </MkDraggable>
+ <MkButton rounded style="margin: 0 auto;" @click="addChildValue"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
</div>
- <div v-else-if="type === 'not'" :class="$style.item">
+ <div v-else-if="v.type === 'not'" :class="$style.item">
<RolesEditorFormula v-model="v.value"/>
</div>
- <MkInput v-else-if="type === 'createdLessThan' || type === 'createdMoreThan'" v-model="v.sec" type="number">
+ <MkInput v-else-if="v.type === 'createdLessThan' || v.type === 'createdMoreThan'" v-model="v.sec" type="number">
<template #suffix>sec</template>
</MkInput>
- <MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
+ <MkInput v-else-if="v.type === 'followersLessThanOrEq' || v.type === 'followersMoreThanOrEq' || v.type === 'followingLessThanOrEq' || v.type === 'followingMoreThanOrEq' || v.type === 'notesLessThanOrEq' || v.type === 'notesMoreThanOrEq'" v-model="v.value" type="number">
</MkInput>
- <MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId" :items="assignedToDef">
+ <MkSelect v-else-if="v.type === 'roleAssignedTo'" v-model="v.roleId" :items="assignedToDef">
</MkSelect>
</div>
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
+import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
import { genId } from '@/utility/id.js';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
-import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
import { i18n } from '@/i18n.js';
import { deepClone } from '@/utility/clone.js';
import { rolesCache } from '@/cache.js';
-const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
-
const emit = defineEmits<{
- (ev: 'update:modelValue', value: any): void;
+ (ev: 'update:modelValue', value: Misskey.entities.Role['condFormula']): void;
(ev: 'remove'): void;
}>();
const props = defineProps<{
- modelValue: any;
+ modelValue: Misskey.entities.Role['condFormula'];
draggable?: boolean;
+ dragStartCallback?: (ev: DragEvent) => void;
}>();
const v = ref(deepClone(props.modelValue));
@@ -102,38 +116,51 @@ const typeDef = [
{ label: i18n.ts._role._condition.not, value: 'not' },
] as const satisfies MkSelectItem[];
-const type = computed<GetMkSelectValueTypesFromDef<typeof typeDef>>({
+type KeyOfUnion<T> = T extends T ? keyof T : never;
+
+type DistributiveOmit<T, K extends KeyOfUnion<T>> = T extends T
+ ? Omit<T, K>
+ : never;
+
+const typeModelForMkSelect = computed<GetMkSelectValueTypesFromDef<typeof typeDef>>({
get: () => v.value.type,
set: (t) => {
- if (t === 'and') v.value.values = [];
- if (t === 'or') v.value.values = [];
- if (t === 'not') v.value.value = { id: genId(), type: 'isRemote' };
- if (t === 'roleAssignedTo') v.value.roleId = '';
- if (t === 'createdLessThan') v.value.sec = 86400;
- if (t === 'createdMoreThan') v.value.sec = 86400;
- if (t === 'followersLessThanOrEq') v.value.value = 10;
- if (t === 'followersMoreThanOrEq') v.value.value = 10;
- if (t === 'followingLessThanOrEq') v.value.value = 10;
- if (t === 'followingMoreThanOrEq') v.value.value = 10;
- if (t === 'notesLessThanOrEq') v.value.value = 10;
- if (t === 'notesMoreThanOrEq') v.value.value = 10;
- v.value.type = t;
+ let newValue: DistributiveOmit<Misskey.entities.Role['condFormula'], 'id'>;
+ switch (t) {
+ case 'and': newValue = { type: 'and', values: [] }; break;
+ case 'or': newValue = { type: 'or', values: [] }; break;
+ case 'not': newValue = { type: 'not', value: { id: genId(), type: 'isRemote' } }; break;
+ case 'roleAssignedTo': newValue = { type: 'roleAssignedTo', roleId: '' }; break;
+ case 'createdLessThan': newValue = { type: 'createdLessThan', sec: 86400 }; break;
+ case 'createdMoreThan': newValue = { type: 'createdMoreThan', sec: 86400 }; break;
+ case 'followersLessThanOrEq': newValue = { type: 'followersLessThanOrEq', value: 10 }; break;
+ case 'followersMoreThanOrEq': newValue = { type: 'followersMoreThanOrEq', value: 10 }; break;
+ case 'followingLessThanOrEq': newValue = { type: 'followingLessThanOrEq', value: 10 }; break;
+ case 'followingMoreThanOrEq': newValue = { type: 'followingMoreThanOrEq', value: 10 }; break;
+ case 'notesLessThanOrEq': newValue = { type: 'notesLessThanOrEq', value: 10 }; break;
+ case 'notesMoreThanOrEq': newValue = { type: 'notesMoreThanOrEq', value: 10 }; break;
+ default: newValue = { type: t }; break;
+ }
+ v.value = { id: v.value.id, ...newValue };
},
});
const assignedToDef = computed(() => roles.filter(r => r.target === 'manual').map(r => ({ label: r.name, value: r.id })) satisfies MkSelectItem[]);
-function addValue() {
+function addChildValue() {
+ if (v.value.type !== 'and' && v.value.type !== 'or') return;
v.value.values.push({ id: genId(), type: 'isRemote' });
}
-function valuesItemUpdated(item) {
+function childValuesItemUpdated(item: Misskey.entities.Role['condFormula']) {
+ if (v.value.type !== 'and' && v.value.type !== 'or') return;
const i = v.value.values.findIndex(_item => _item.id === item.id);
v.value.values[i] = item;
}
-function removeItem(item) {
- v.value.values = v.value.values.filter(_item => _item.id !== item.id);
+function removeChildItem(itemId: string) {
+ if (v.value.type !== 'and' && v.value.type !== 'or') return;
+ v.value.values = v.value.values.filter(_item => _item.id !== itemId);
}
function removeSelf() {
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index 7c3f736506..591d8fa736 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -37,8 +37,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
</MkSelect>
<MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked">
- <span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/>
- <span v-else class="ti ti-settings" style="line-height: normal"/>
+ <span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"></span>
+ <span v-else class="ti ti-settings" style="line-height: normal"></span>
</MkButton>
</div>
</div>
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
index 36d586bd23..ba5830c2e8 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root" class="_panel _gaps_s">
- <div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"/> {{ methodName }}</div>
+ <div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"></span> {{ methodName }}</div>
<div :class="$style.rightDivider" style="flex: 0.5">{{ entity.name }}</div>
<div :class="$style.rightDivider" style="flex: 1">
<div v-if="method === 'email' && user">
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.recipientButtons" style="margin-left: auto">
<button :class="$style.recipientButton" @click="onEditButtonClicked()">
- <span class="ti ti-settings"/>
+ <span class="ti ti-settings"></span>
</button>
<button :class="$style.recipientButton" @click="onDeleteButtonClicked()">
- <span class="ti ti-trash"/>
+ <span class="ti ti-trash"></span>
</button>
</div>
</div>
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
index 893bd8d6d3..a9cf372c0e 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root" class="_gaps_m">
<div :class="$style.addButton">
<MkButton primary @click="onAddButtonClicked">
- <span class="ti ti-plus"/> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
+ <span class="ti ti-plus"></span> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
</MkButton>
</div>
<div :class="$style.subMenus" class="_gaps_s">
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index 76bf20b409..2d204987cb 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -105,7 +105,7 @@ const paginator = markRaw(new Paginator('admin/abuse-user-reports', {
})),
}));
-function resolved(reportId) {
+function resolved(reportId: string) {
paginator.removeItem(reportId);
}
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index 94940a84ae..0efd1a2e28 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -22,22 +22,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
- <MkRadios v-model="ad.place">
+ <MkRadios
+ v-model="ad.place"
+ :options="[
+ { value: 'square' },
+ { value: 'horizontal' },
+ { value: 'horizontal-big' },
+ ]"
+ >
<template #label>Form</template>
- <option value="square">square</option>
- <option value="horizontal">horizontal</option>
- <option value="horizontal-big">horizontal-big</option>
</MkRadios>
- <!--
- <div style="margin: 32px 0;">
- {{ i18n.ts.priority }}
- <MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
- <MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
- <MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
- </div>
- -->
-
<FormSplit>
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template>
@@ -109,7 +104,11 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
-const ads = ref<Misskey.entities.Ad[]>([]);
+type Ad = Misskey.entities.Ad & {
+ place: 'square' | 'horizontal' | 'horizontal-big';
+};
+
+const ads = ref<Ad[]>([]);
// ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化
const localTime = new Date();
@@ -136,7 +135,7 @@ misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
- ...r,
+ ...(r as Ad),
expiresAt: exdate.toISOString().slice(0, 16),
startsAt: stdate.toISOString().slice(0, 16),
};
@@ -239,7 +238,7 @@ function more() {
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
- ...r,
+ ...(r as Ad),
expiresAt: exdate.toISOString().slice(0, 16),
startsAt: stdate.toISOString().slice(0, 16),
};
@@ -256,7 +255,7 @@ function refresh() {
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
- ...r,
+ ...(r as Ad),
expiresAt: exdate.toISOString().slice(0, 16),
startsAt: stdate.toISOString().slice(0, 16),
};
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index b90a724b17..87fc6e70f4 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_spacer" style="--MI_SPACER-w: 900px;">
<div class="_gaps">
<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
- <MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
+ <MkInfo v-if="announcementsStatus === 'active' && announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
<MkSelect v-model="announcementsStatus" :items="announcementsStatusDef">
<template #label>{{ i18n.ts.filter }}</template>
@@ -45,18 +45,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="announcement.imageUrl" type="url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
- <MkRadios v-model="announcement.icon">
+ <MkRadios
+ v-model="announcement.icon"
+ :options="[
+ { value: 'info', icon: 'ti ti-info-circle' },
+ { value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
+ { value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
+ { value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
+ ]"
+ >
<template #label>{{ i18n.ts.icon }}</template>
- <option value="info"><i class="ti ti-info-circle"></i></option>
- <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
- <option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
- <option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
</MkRadios>
- <MkRadios v-model="announcement.display">
+ <MkRadios
+ v-model="announcement.display"
+ :options="[
+ { value: 'normal', label: i18n.ts.normal },
+ { value: 'banner', label: i18n.ts.banner },
+ { value: 'dialog', label: i18n.ts.dialog },
+ ]"
+ >
<template #label>{{ i18n.ts.display }}</template>
- <option value="normal">{{ i18n.ts.normal }}</option>
- <option value="banner">{{ i18n.ts.banner }}</option>
- <option value="dialog">{{ i18n.ts.dialog }}</option>
</MkRadios>
<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
@@ -83,6 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
+import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
@@ -112,7 +121,12 @@ const {
const loading = ref(true);
const loadingMore = ref(false);
-const announcements = ref<any[]>([]);
+const announcements = ref<(Omit<Misskey.entities.AdminAnnouncementsListResponse[number], 'id' | 'createdAt' | 'updatedAt' | 'reads' | 'isActive'> & {
+ id: string | null;
+ _id?: string;
+ isActive?: Misskey.entities.AdminAnnouncementsListResponse[number]['isActive'];
+ reads?: Misskey.entities.AdminAnnouncementsListResponse[number]['reads'];
+})[]>([]);
watch(announcementsStatus, (to) => {
loading.value = true;
@@ -136,42 +150,55 @@ function add() {
forExistingUsers: false,
silence: false,
needConfirmationToRead: false,
+ userId: null,
});
}
-function del(announcement) {
- os.confirm({
+async function del(announcement: (typeof announcements)['value'][number]) {
+ if (announcement.id == null) return;
+ const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.deleteAreYouSure({ x: announcement.title }),
- }).then(({ canceled }) => {
- if (canceled) return;
- announcements.value = announcements.value.filter(x => x !== announcement);
- misskeyApi('admin/announcements/delete', announcement);
+ });
+ if (canceled) return;
+ announcements.value = announcements.value.filter(x => x !== announcement);
+ misskeyApi('admin/announcements/delete', {
+ id: announcement.id,
});
}
-async function archive(announcement) {
+async function archive(announcement: (typeof announcements)['value'][number]) {
+ if (announcement.id == null) return;
+ const { _id, ...data } = announcement; // _idを消す
await os.apiWithDialog('admin/announcements/update', {
- ...announcement,
+ ...data,
+ id: announcement.id, // TSを黙らすため
isActive: false,
});
refresh();
}
-async function unarchive(announcement) {
+async function unarchive(announcement: (typeof announcements)['value'][number]) {
+ if (announcement.id == null) return;
+ const { _id, ...data } = announcement; // _idを消す
await os.apiWithDialog('admin/announcements/update', {
- ...announcement,
+ ...data,
+ id: announcement.id, // TSを黙らすため
isActive: true,
});
refresh();
}
-async function save(announcement) {
+async function save(announcement: (typeof announcements)['value'][number]) {
+ const { _id, ...data } = announcement; // _idを消す
if (announcement.id == null) {
- await os.apiWithDialog('admin/announcements/create', announcement);
+ await os.apiWithDialog('admin/announcements/create', data);
refresh();
} else {
- os.apiWithDialog('admin/announcements/update', announcement);
+ os.apiWithDialog('admin/announcements/update', {
+ ...data,
+ id: announcement.id, // TSを黙らすため
+ });
}
}
@@ -179,7 +206,7 @@ function more() {
loadingMore.value = true;
misskeyApi('admin/announcements/list', {
status: announcementsStatus.value,
- untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id,
+ untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id!,
}).then(announcementResponse => {
announcements.value = announcements.value.concat(announcementResponse);
loadingMore.value = false;
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 7ed280358a..481969e1a3 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -19,13 +19,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_gaps_m">
- <MkRadios v-model="botProtectionForm.state.provider">
- <option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
- <option value="hcaptcha">hCaptcha</option>
- <option value="mcaptcha">mCaptcha</option>
- <option value="recaptcha">reCAPTCHA</option>
- <option value="turnstile">Turnstile</option>
- <option value="testcaptcha">testCaptcha</option>
+ <MkRadios
+ v-model="botProtectionForm.state.provider"
+ :options="[
+ { value: 'none', label: `${i18n.ts.none} (${i18n.ts.notRecommended})` },
+ { value: 'hcaptcha', label: 'hCaptcha' },
+ { value: 'mcaptcha', label: 'mCaptcha' },
+ { value: 'recaptcha', label: 'reCAPTCHA' },
+ { value: 'turnstile', label: 'Turnstile' },
+ { value: 'testcaptcha', label: 'testCaptcha' },
+ ]"
+ >
</MkRadios>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index e5e0f087e1..016d1b6a89 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -9,10 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
<div class="_gaps_m">
<SearchMarker :keywords="['entrance', 'welcome', 'landing', 'front', 'home', 'page', 'style']">
- <MkRadios v-model="entrancePageStyle">
+ <MkRadios
+ v-model="entrancePageStyle"
+ :options="[
+ { value: 'classic' },
+ { value: 'simple' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts._serverSettings.entrancePageStyle }}</SearchLabel></template>
- <option value="classic">Classic</option>
- <option value="simple">Simple</option>
</MkRadios>
</SearchMarker>
@@ -151,8 +155,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
import JSON5 from 'json5';
+import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
-import type { ClientOptions } from '@/instance.js';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import * as os from '@/os.js';
@@ -168,11 +172,11 @@ import MkSwitch from '@/components/MkSwitch.vue';
const meta = await misskeyApi('admin/meta');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-const entrancePageStyle = ref<ClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
+const entrancePageStyle = ref<Misskey.entities.MetaClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-const showTimelineForVisitor = ref<ClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
+const showTimelineForVisitor = ref<Misskey.entities.MetaClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-const showActivitiesForVisitor = ref<ClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
+const showActivitiesForVisitor = ref<Misskey.entities.MetaClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
const iconUrl = ref(meta.iconUrl);
const app192IconUrl = ref(meta.app192IconUrl);
@@ -191,11 +195,11 @@ const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.
function save() {
os.apiWithDialog('admin/update-meta', {
- clientOptions: ({
+ clientOptions: {
entrancePageStyle: entrancePageStyle.value,
showTimelineForVisitor: showTimelineForVisitor.value,
showActivitiesForVisitor: showActivitiesForVisitor.value,
- } as ClientOptions) as any,
+ },
iconUrl: iconUrl.value,
app192IconUrl: app192IconUrl.value,
app512IconUrl: app512IconUrl.value,
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
index 250abeebe2..6f58ab9857 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
@@ -51,6 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
import type { SortOrder } from '@/components/MkSortOrderEditor.define.js';
import type { GridSortOrderKey } from './custom-emojis-manager.impl.js';
+import type { PageHeaderItem } from '@/types/page-header.js';
export type EmojiSearchQuery = {
name: string | null;
@@ -250,7 +251,7 @@ function setupGrid(): GridSetting {
icon: 'ti ti-trash',
action: () => {
removeDataFromGrid(context, (cell) => {
- gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
+ (gridItems.value[cell.row.index] as any)[cell.column.setting.bindTo] = undefined;
});
},
},
@@ -454,7 +455,7 @@ function onGridCellValidation(event: GridCellValidationEvent) {
function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
- gridItems.value[row.index][column.setting.bindTo] = newValue;
+ (gridItems.value[row.index] as any)[column.setting.bindTo] = newValue;
}
}
@@ -525,7 +526,7 @@ const headerPageMetadata = computed(() => ({
icon: 'ti ti-icons',
}));
-const headerActions = computed(() => [{
+const headerActions = computed<PageHeaderItem[]>(() => [{
icon: 'ti ti-search',
text: i18n.ts.search,
handler: async () => {
@@ -552,7 +553,7 @@ const headerActions = computed(() => [{
}, {
icon: 'ti ti-list-numbers',
text: i18n.ts._customEmojisManager._gridCommon.searchLimit,
- handler: (ev: MouseEvent) => {
+ handler: (ev) => {
async function changeSearchLimit(to: number) {
if (updatedItemsCount.value > 0) {
const { canceled } = await os.confirm({
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
index c343d88eb1..7ccb166481 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
@@ -58,7 +58,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
-/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as Misskey from 'misskey-js';
import { computed, onMounted, ref, useCssModule } from 'vue';
import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
@@ -339,7 +338,7 @@ function onGridCellValidation(event: GridCellValidationEvent) {
function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
- gridItems.value[row.index][column.setting.bindTo] = newValue;
+ (gridItems.value[row.index] as any)[column.setting.bindTo] = newValue;
}
}
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
index 6317fc0b47..d5bfdffe34 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
@@ -306,7 +306,7 @@ function onGridEvent(event: GridEvent) {
function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
- gridItems.value[row.index][column.setting.bindTo] = newValue;
+ (gridItems.value[row.index] as any)[column.setting.bindTo] = newValue;
}
}
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
index 14773d7f04..c947dc3256 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
@@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader v-model:tab="headerTab" :tabs="headerTabs">
<XGridLocalComponent v-if="headerTab === 'local'" :class="$style.local"/>
- <XGridRemoteComponent v-else-if="headerTab === 'remote'" :class="$style.remote"/>
- <XRegisterComponent v-else-if="headerTab === 'register'" :class="$style.register"/>
+ <XGridRemoteComponent v-else-if="headerTab === 'remote'"/>
+ <XRegisterComponent v-else-if="headerTab === 'register'"/>
</PageWithHeader>
</template>
diff --git a/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue b/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue
index 420219c22c..04de781a28 100644
--- a/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue
+++ b/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue
@@ -28,7 +28,7 @@ const { handler: externalTooltipHandler } = useChartTooltip();
let chartInstance: Chart | null = null;
-function setData(values) {
+function setData(values: number[]) {
if (chartInstance == null || chartInstance.data.labels == null) return;
for (const value of values) {
chartInstance.data.labels.push('');
@@ -41,7 +41,7 @@ function setData(values) {
chartInstance.update();
}
-function pushData(value) {
+function pushData(value: number) {
if (chartInstance == null || chartInstance.data.labels == null) return;
chartInstance.data.labels.push('');
chartInstance.data.datasets[0].data.push(value);
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 94994dc94c..b3a929faf4 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -294,7 +294,7 @@ function invite() {
});
}
-function adminLookup(ev: MouseEvent) {
+function adminLookup(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.user,
icon: 'ti ti-user',
diff --git a/packages/frontend/src/pages/admin/job-queue.vue b/packages/frontend/src/pages/admin/job-queue.vue
index b18049cb11..97b6c2bc67 100644
--- a/packages/frontend/src/pages/admin/job-queue.vue
+++ b/packages/frontend/src/pages/admin/job-queue.vue
@@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>
<MkTabs
v-model:tab="jobState"
- :class="$style.jobsTabs" :tabs="[{
+ :tabs="[{
key: 'all',
title: 'All',
icon: 'ti ti-code-asterisk',
@@ -359,8 +359,4 @@ definePage(() => ({
font-size: 85%;
margin: 6px 0;
}
-
-.jobsTabs {
-
-}
</style>
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 32a5a6976e..9854ca7fc6 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -47,7 +47,7 @@ async function renderChart() {
return new Date(y, m, d - ago);
};
- const format = (arr) => {
+ const format = (arr: number[]) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v,
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index 2c550bd9c3..90799647ff 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -104,7 +104,7 @@ const filesPagination = {
noPaging: true,
};
-function onInstanceClick(i) {
+function onInstanceClick(i: Misskey.entities.FederationInstance) {
os.pageWindow(`/instance-info/${i.host}`);
}
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index b24b640527..e806f68162 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -21,8 +21,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import { genId } from '@/utility/id.js';
import XEditor from './roles.editor.vue';
+import { genId } from '@/utility/id.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
@@ -37,8 +37,13 @@ const props = defineProps<{
id?: string;
}>();
+type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
+ condFormula: any;
+ policies: any;
+};
+
const role = ref<Misskey.entities.Role | null>(null);
-const data = ref<any>(null);
+const data = ref<RoleLike | null>(null);
if (props.id) {
role.value = await misskeyApi('admin/roles/show', {
@@ -61,11 +66,13 @@ if (props.id) {
asBadge: false,
canEditMembersByModerator: false,
displayOrder: 0,
+ preserveAssignmentOnMoveAccount: false,
policies: {},
};
}
async function save() {
+ if (data.value === null) return;
rolesCache.delete();
if (role.value) {
os.apiWithDialog('admin/roles/update', {
@@ -75,7 +82,7 @@ async function save() {
router.push('/admin/roles/:id', {
params: {
id: role.value.id,
- }
+ },
});
} else {
const created = await os.apiWithDialog('admin/roles/create', {
@@ -84,7 +91,7 @@ async function save() {
router.push('/admin/roles/:id', {
params: {
id: created.id,
- }
+ },
});
}
}
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 5f8950f07e..7de973a394 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
- <MkInput v-if="readonly" :modelValue="role.id" :readonly="true">
+ <MkInput v-if="readonly && role.id != null" :modelValue="role.id" :readonly="true">
<template #label>ID</template>
</MkInput>
@@ -866,12 +866,18 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/utility/clone.js';
+type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
+ id?: Misskey.entities.Role['id'] | null;
+ condFormula: any;
+ policies: any;
+};
+
const emit = defineEmits<{
- (ev: 'update:modelValue', v: any): void;
+ (ev: 'update:modelValue', v: RoleLike): void;
}>();
const props = defineProps<{
- modelValue: any;
+ modelValue: RoleLike;
readonly?: boolean;
}>();
@@ -910,7 +916,7 @@ const rolePermission = computed<GetMkSelectValueTypesFromDef<typeof rolePermissi
const q = ref('');
-function getPriorityIcon(option) {
+function getPriorityIcon(option: { priority: number }): string {
if (option.priority === 2) return 'ti ti-arrows-up';
if (option.priority === 1) return 'ti ti-arrow-narrow-up';
return 'ti ti-point';
@@ -936,6 +942,7 @@ const save = throttle(100, () => {
isExplorable: role.value.isExplorable,
asBadge: role.value.asBadge,
canEditMembersByModerator: role.value.canEditMembersByModerator,
+ preserveAssignmentOnMoveAccount: role.value.preserveAssignmentOnMoveAccount,
policies: role.value.policies,
};
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 2e249eee50..7fc51979af 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -28,15 +28,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items }">
<div class="_gaps_s">
- <div v-for="item in items" :key="item.user.id" :class="[$style.userItem, { [$style.userItemOpened]: expandedItems.includes(item.id) }]">
+ <div v-for="item in items" :key="item.user.id" :class="[$style.userItem, { [$style.userItemOpened]: expandedItemIds.includes(item.id) }]">
<div :class="$style.userItemMain">
<MkA :class="$style.userItemMainBody" :to="`/admin/user/${item.user.id}`">
<MkUserCardMini :user="item.user"/>
</MkA>
- <button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
- <button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button>
+ <button class="_button" :class="$style.userToggle" @click="toggleItem(item.id)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+ <button class="_button" :class="$style.unassign" @click="unassign(item.user.id, $event)"><i class="ti ti-x"></i></button>
</div>
- <div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub">
+ <div v-if="expandedItemIds.includes(item.id)" :class="$style.userItemSub">
<div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div>
<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
@@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, markRaw, reactive, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import XEditor from './roles.editor.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
@@ -81,7 +82,7 @@ const usersPaginator = markRaw(new Paginator('admin/roles/users', {
}) : undefined),
}));
-const expandedItems = ref<string[]>([]);
+const expandedItemIds = ref<Misskey.entities.AdminRolesUsersResponse[number]['id'][]>([]);
const role = reactive(await misskeyApi('admin/roles/show', {
roleId: props.id,
@@ -91,7 +92,7 @@ function edit() {
router.push('/admin/roles/:id/edit', {
params: {
id: role.id,
- }
+ },
});
}
@@ -140,23 +141,23 @@ async function assign() {
//role.users.push(user);
}
-async function unassign(user, ev) {
+async function unassign(userId: Misskey.entities.User['id'], ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.unassign,
icon: 'ti ti-x',
danger: true,
action: async () => {
- await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id });
- //role.users = role.users.filter(u => u.id !== user.id);
+ await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: userId });
+ //role.users = role.users.filter(u => u.id !== userId);
},
}], ev.currentTarget ?? ev.target);
}
-async function toggleItem(item) {
- if (expandedItems.value.includes(item.id)) {
- expandedItems.value = expandedItems.value.filter(x => x !== item.id);
+async function toggleItem(itemId: string) {
+ if (expandedItemIds.value.includes(itemId)) {
+ expandedItemIds.value = expandedItemIds.value.filter(x => x !== itemId);
} else {
- expandedItems.value.push(item.id);
+ expandedItemIds.value.push(itemId);
}
}
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index fa93124daa..f310f26107 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -25,11 +25,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
- <MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
- <option value="none">{{ i18n.ts.none }}</option>
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.localOnly }}</option>
- <option value="remote">{{ i18n.ts.remoteOnly }}</option>
+ <MkRadios
+ v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection"
+ :options="[
+ { value: 'none', label: i18n.ts.none },
+ { value: 'all', label: i18n.ts.all },
+ { value: 'local', label: i18n.ts.localOnly },
+ { value: 'remote', label: i18n.ts.remoteOnly },
+ ]"
+ >
</MkRadios>
<SearchMarker :keywords="['sensitivity']">
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index d26f02b41c..02aad732f6 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -12,28 +12,25 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<div><SearchText>{{ i18n.ts._serverRules.description }}</SearchText></div>
- <Sortable
+ <MkDraggable
v-model="serverRules"
- class="_gaps_m"
- :itemKey="(_, i) => i"
- :animation="150"
- :handle="'.' + $style.itemHandle"
- @start="e => e.item.classList.add('active')"
- @end="e => e.item.classList.remove('active')"
+ direction="vertical"
+ withGaps
+ manualDragStart
>
- <template #item="{element,index}">
+ <template #default="{ item, index, dragStart }">
<div :class="$style.item">
<div :class="$style.itemHeader">
- <div :class="$style.itemNumber" v-text="String(index + 1)"/>
- <span :class="$style.itemHandle"><i class="ti ti-menu"/></span>
- <button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ti ti-x"></i></button>
+ <div :class="$style.itemNumber">{{ index + 1 }}</div>
+ <span :class="$style.itemHandle" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"></i></span>
+ <button class="_button" :class="$style.itemRemove" @click="remove(item.id)"><i class="ti ti-x"></i></button>
</div>
- <MkInput v-model="serverRules[index]"/>
+ <MkInput :modelValue="item.text" @update:modelValue="serverRules[index].text = $event"/>
</div>
</template>
- </Sortable>
+ </MkDraggable>
<div :class="$style.commands">
- <MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+ <MkButton rounded @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
@@ -42,28 +39,31 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, ref, computed } from 'vue';
+import { ref } from 'vue';
import * as os from '@/os.js';
import { fetchInstance, instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkFolder from '@/components/MkFolder.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
-const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
+const serverRules = ref<{ text: string; id: string; }[]>(instance.serverRules.map(text => ({ text, id: Math.random().toString() })));
-const serverRules = ref<string[]>(instance.serverRules);
-
-const save = async () => {
+async function save() {
await os.apiWithDialog('admin/update-meta', {
- serverRules: serverRules.value,
+ serverRules: serverRules.value.map(r => r.text),
});
fetchInstance(true);
-};
+}
-const remove = (index: number): void => {
- serverRules.value.splice(index, 1);
-};
+function add(): void {
+ serverRules.value.push({ text: '', id: Math.random().toString() });
+}
+
+function remove(id: string): void {
+ serverRules.value = serverRules.value.filter(r => r.id !== id);
+}
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 541ee7c0cd..99d4455939 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -258,11 +258,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<SearchMarker>
- <MkRadios v-model="federationForm.state.federation">
+ <MkRadios
+ v-model="federationForm.state.federation"
+ :options="[
+ { value: 'all', label: i18n.ts.all },
+ { value: 'specified', label: i18n.ts.specifyHost },
+ { value: 'none', label: i18n.ts.none },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="specified">{{ i18n.ts.specifyHost }}</option>
- <option value="none">{{ i18n.ts.none }}</option>
</MkRadios>
</SearchMarker>
diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue
index b53667e98c..9807cbb313 100644
--- a/packages/frontend/src/pages/admin/system-webhook.item.vue
+++ b/packages/frontend/src/pages/admin/system-webhook.item.vue
@@ -8,14 +8,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ entity.name || entity.url }}</template>
<template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template>
<template #icon>
- <i v-if="!entity.isActive" class="ti ti-player-pause"/>
- <i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>
+ <i v-if="!entity.isActive" class="ti ti-player-pause"></i>
+ <i v-else-if="entity.latestStatus === null" class="ti ti-circle"></i>
<i
v-else-if="[200, 201, 204].includes(entity.latestStatus)"
class="ti ti-check"
:style="{ color: 'var(--MI_THEME-success)' }"
- />
- <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"/>
+ ></i>
+ <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"></i>
</template>
<template #suffix>
<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index 2f7ecca521..eb9806d668 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -46,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, markRaw, ref, watchEffect } from 'vue';
+import * as Misskey from 'misskey-js';
import { defaultMemoryStorage } from '@/memory-storage';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -146,7 +147,7 @@ async function addUser() {
});
}
-function show(user) {
+function show(user: Misskey.entities.UserDetailed) {
os.pageWindow(`/admin/user/${user.id}`);
}
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 4c34c3c74b..150808fcbd 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkA>
</div>
- <div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
+ <div v-if="tab !== 'past' && $i != null && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
</div>
</section>
@@ -45,6 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, markRaw } from 'vue';
+import * as Misskey from 'misskey-js';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@@ -65,7 +66,9 @@ const paginator = markRaw(new Paginator('announcements', {
const tab = ref('current');
-async function read(target) {
+async function read(target: Misskey.entities.Announcement) {
+ if ($i == null) return;
+
if (target.needConfirmationToRead) {
const confirm = await os.confirm({
type: 'question',
@@ -81,7 +84,7 @@ async function read(target) {
}));
misskeyApi('i/read-announcement', { announcementId: target.id });
updateCurrentAccountPartial({
- unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
+ unreadAnnouncements: $i.unreadAnnouncements.filter(a => a.id !== target.id),
});
}
diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue
index f436fc72fa..8377dc074d 100644
--- a/packages/frontend/src/pages/api-console.vue
+++ b/packages/frontend/src/pages/api-console.vue
@@ -73,7 +73,7 @@ function onEndpointChange() {
return;
}
- const endpointBody = {};
+ const endpointBody = {} as Record<string, unknown>;
for (const p of resp.params) {
endpointBody[p.name] =
p.type === 'String' ? '' :
diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue
index 1a0c9b36c4..bc585950b4 100644
--- a/packages/frontend/src/pages/auth.form.vue
+++ b/packages/frontend/src/pages/auth.form.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<section>
- <div v-if="app.permission.length > 0">
+ <div v-if="permissions.length > 0">
<p>{{ i18n.tsx._auth.permission({ name }) }}</p>
<ul>
- <li v-for="p in app.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
+ <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] ?? p }}</li>
</ul>
</div>
<div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div>
@@ -37,6 +37,10 @@ const emit = defineEmits<{
const app = computed(() => props.session.app);
+const permissions = computed(() => {
+ return props.session.app.permission.filter((p): p is typeof Misskey.permissions[number] => typeof p === 'string');
+});
+
const name = computed(() => {
const el = window.document.createElement('div');
el.textContent = app.value.name;
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index 83bf7221d0..14b13e511a 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -67,7 +67,7 @@ function accepted() {
}
}
-function onLogin(res) {
+function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
login(res.i);
}
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
index a8ce527523..68e8d6a4d0 100644
--- a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -78,7 +78,7 @@ import { ensureSignin } from '@/i.js';
const $i = ensureSignin();
const props = defineProps<{
- avatarDecoration?: any,
+ avatarDecoration?: Misskey.entities.AdminAvatarDecorationsListResponse[number],
}>();
const emit = defineEmits<{
@@ -109,7 +109,7 @@ async function addRole() {
rolesThatCanBeUsedThisDecoration.value.push(roles.find(r => r.id === roleId)!);
}
-async function removeRole(role, ev) {
+async function removeRole(role: Misskey.entities.Role, ev: PointerEvent) {
rolesThatCanBeUsedThisDecoration.value = rolesThatCanBeUsedThisDecoration.value.filter(x => x.id !== role.id);
}
@@ -147,6 +147,8 @@ async function done() {
}
async function del() {
+ if (props.avatarDecoration == null) return;
+
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: name.value }),
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index f96c02a567..4c5457504e 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -45,7 +45,7 @@ function load() {
load();
-async function add(ev: MouseEvent) {
+async function add(ev: PointerEvent) {
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
}, {
done: result => {
@@ -57,7 +57,7 @@ async function add(ev: MouseEvent) {
});
}
-async function edit(avatarDecoration) {
+async function edit(avatarDecoration: Misskey.entities.AdminAvatarDecorationsListResponse[number]) {
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
avatarDecoration: avatarDecoration,
}, {
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index 251f5d557d..4b73b6c6b3 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -41,20 +41,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkButton primary rounded @click="addPinnedNote()"><i class="ti ti-plus"></i></MkButton>
- <Sortable
- v-model="pinnedNotes"
- itemKey="id"
- :handle="'.' + $style.pinnedNoteHandle"
- :animation="150"
+ <MkDraggable
+ :modelValue="pinnedNoteIds.map(id => ({ id }))"
+ direction="vertical"
+ @update:modelValue="v => pinnedNoteIds = v.map(x => x.id)"
>
- <template #item="{element,index}">
+ <template #default="{ item }">
<div :class="$style.pinnedNote">
<button class="_button" :class="$style.pinnedNoteHandle"><i class="ti ti-menu"></i></button>
- {{ element.id }}
- <button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ti ti-x"></i></button>
+ {{ item.id }}
+ <button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(item.id)"><i class="ti ti-x"></i></button>
</div>
</template>
- </Sortable>
+ </MkDraggable>
</div>
</MkFolder>
@@ -68,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, ref, watch, defineAsyncComponent } from 'vue';
+import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -81,10 +80,9 @@ import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
import { useRouter } from '@/router.js';
-const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
-
const router = useRouter();
const props = defineProps<{
@@ -99,7 +97,7 @@ const bannerId = ref<string | null>(null);
const color = ref('#000');
const isSensitive = ref(false);
const allowRenoteToExternal = ref(true);
-const pinnedNotes = ref<{ id: Misskey.entities.Note['id'] }[]>([]);
+const pinnedNoteIds = ref<Misskey.entities.Note['id'][]>([]);
watch(() => bannerId.value, async () => {
if (bannerId.value == null) {
@@ -123,9 +121,7 @@ async function fetchChannel() {
bannerId.value = result.bannerId;
bannerUrl.value = result.bannerUrl;
isSensitive.value = result.isSensitive;
- pinnedNotes.value = result.pinnedNoteIds.map(id => ({
- id,
- }));
+ pinnedNoteIds.value = result.pinnedNoteIds;
color.value = result.color;
allowRenoteToExternal.value = result.allowRenoteToExternal;
@@ -143,13 +139,11 @@ async function addPinnedNote() {
const note = await os.apiWithDialog('notes/show', {
noteId: fromUrl ?? value,
});
- pinnedNotes.value = [{
- id: note.id,
- }, ...pinnedNotes.value];
+ pinnedNoteIds.value.unshift(note.id);
}
-function removePinnedNote(index: number) {
- pinnedNotes.value.splice(index, 1);
+function removePinnedNote(id: string) {
+ pinnedNoteIds.value = pinnedNoteIds.value.filter(x => x !== id);
}
function save() {
@@ -166,7 +160,7 @@ function save() {
os.apiWithDialog('channels/update', {
...params,
channelId: props.channelId,
- pinnedNoteIds: pinnedNotes.value.map(x => x.id),
+ pinnedNoteIds: pinnedNoteIds.value,
});
} else {
os.apiWithDialog('channels/create', params).then(created => {
@@ -197,7 +191,7 @@ async function archive() {
});
}
-function setBannerImage(evt) {
+function setBannerImage(evt: PointerEvent) {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: false,
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index f77034e318..26dc5b80df 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -11,9 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
- <MkRadios v-model="searchType" @update:modelValue="search()">
- <option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
- <option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
+ <MkRadios
+ v-model="searchType"
+ :options="[
+ { value: 'nameAndDescription', label: i18n.ts._channel.nameAndDescription },
+ { value: 'nameOnly', label: i18n.ts._channel.nameOnly },
+ ]"
+ @update:modelValue="search()"
+ >
</MkRadios>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
@@ -72,15 +77,17 @@ import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
+type SearchType = 'nameAndDescription' | 'nameOnly';
+
const props = defineProps<{
query: string;
- type?: string;
+ type?: SearchType;
}>();
const key = ref('');
const tab = ref('featured');
const searchQuery = ref('');
-const searchType = ref('nameAndDescription');
+const searchType = ref<SearchType>('nameAndDescription');
const channelPaginator = shallowRef();
onMounted(() => {
diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue
index 613c4e4dcc..f759e45e48 100644
--- a/packages/frontend/src/pages/chat/XMessage.vue
+++ b/packages/frontend/src/pages/chat/XMessage.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:enableEmojiMenu="true"
:enableEmojiMenuReaction="true"
/>
- <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
+ <MkMediaList v-if="message.file" :mediaList="[message.file]"/>
</MkFukidashi>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
<div :class="$style.footer">
@@ -94,7 +94,7 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
});
});
-function react(ev: MouseEvent) {
+function react(ev: PointerEvent) {
if ($i.policies.chatAvailability !== 'available') return;
const targetEl = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
@@ -128,14 +128,14 @@ function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) {
}
}
-function onContextmenu(ev: MouseEvent) {
+function onContextmenu(ev: PointerEvent) {
if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
showMenu(ev, true);
}
-function showMenu(ev: MouseEvent, contextmenu = false) {
+function showMenu(ev: PointerEvent, contextmenu = false) {
const menu: MenuItem[] = [];
if (!isMe.value && $i.policies.chatAvailability === 'available') {
diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue
index 756bf8a342..ed04253046 100644
--- a/packages/frontend/src/pages/chat/home.home.vue
+++ b/packages/frontend/src/pages/chat/home.home.vue
@@ -64,7 +64,7 @@ const searchQuery = ref('');
const searched = ref(false);
const searchResults = ref<Misskey.entities.ChatMessage[]>([]);
-function start(ev: MouseEvent) {
+function start(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts._chat.individualChat,
caption: i18n.ts._chat.individualChat_description,
@@ -89,7 +89,7 @@ async function startUser() {
router.push('/chat/user/:userId', {
params: {
userId: user.id,
- }
+ },
});
});
}
@@ -108,7 +108,7 @@ async function createRoom() {
router.push('/chat/room/:roomId', {
params: {
roomId: room.id,
- }
+ },
});
}
diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue
index 17b68d6eb9..72aeba0a45 100644
--- a/packages/frontend/src/pages/chat/room.form.vue
+++ b/packages/frontend/src/pages/chat/room.form.vue
@@ -167,7 +167,7 @@ function onKeydown(ev: KeyboardEvent) {
}
}
-function chooseFile(ev: MouseEvent) {
+function chooseFile(ev: PointerEvent) {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue
index ef9191b4a5..a4204435b3 100644
--- a/packages/frontend/src/pages/chat/room.vue
+++ b/packages/frontend/src/pages/chat/room.vue
@@ -391,7 +391,7 @@ async function leaveRoom() {
router.push('/chat');
}
-function showMenu(ev: MouseEvent) {
+function showMenu(ev: PointerEvent) {
const menuItems: MenuItem[] = [];
if (room.value) {
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 8176fb519b..8feddf70b0 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -34,6 +34,7 @@ import { computed, watch, provide, ref, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
+import type { PageHeaderItem } from '@/types/page-header.js';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
@@ -105,7 +106,7 @@ async function unfavorite() {
});
}
-const headerActions = computed(() => clip.value && isOwned.value ? [{
+const headerActions = computed<PageHeaderItem[] | null>(() => clip.value && isOwned.value ? [{
icon: 'ti ti-pencil',
text: i18n.ts.edit,
handler: async (): Promise<void> => {
@@ -144,7 +145,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
}, ...(clip.value.isPublic ? [{
icon: 'ti ti-share',
text: i18n.ts.share,
- handler: (ev: MouseEvent): void => {
+ handler: (ev): void => {
const menuItems: MenuItem[] = [];
menuItems.push({
@@ -177,7 +178,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
},
-}] : []), {
+}] satisfies PageHeaderItem[] : []), {
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
@@ -196,7 +197,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
clipsCache.delete();
},
-}] : null);
+}] satisfies PageHeaderItem[] : null);
definePage(() => ({
title: clip.value ? clip.value.name : i18n.ts.clip,
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 0f306896c9..5cb88f0b1a 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
<template #default="{items}">
<div class="ldhfsamy">
- <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
+ <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji as RemoteEmoji, $event)">
<img :src="getProxiedImageUrl(emoji.url, 'emoji')" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
@@ -71,7 +71,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, markRaw, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { computed, markRaw, ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -93,6 +94,8 @@ const host = ref<string | null>(null);
const selectMode = ref(false);
const selectedEmojis = ref<string[]>([]);
+type RemoteEmoji = Misskey.entities.AdminEmojiListRemoteResponse[number] & { host: string };
+
const paginator = markRaw(new Paginator('admin/emoji/list', {
limit: 30,
computedParams: computed(() => ({
@@ -116,7 +119,7 @@ const selectAll = () => {
}
};
-const toggleSelect = (emoji) => {
+const toggleSelect = (emoji: Misskey.entities.EmojiDetailed) => {
if (selectedEmojis.value.includes(emoji.id)) {
selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
} else {
@@ -124,19 +127,23 @@ const toggleSelect = (emoji) => {
}
};
-const add = async (ev: MouseEvent) => {
+const add = async () => {
const { dispose } = await os.popupAsyncWithDialog(import('./emoji-edit-dialog.vue').then(x => x.default), {
}, {
done: result => {
if (result.created) {
- paginator.prepend(result.created);
+ const nowIso = (new Date()).toISOString();
+ paginator.prepend({
+ ...result.created,
+ createdAt: nowIso,
+ });
}
},
closed: () => dispose(),
});
};
-const edit = async (emoji) => {
+const edit = async (emoji: Misskey.entities.EmojiDetailed) => {
const { dispose } = await os.popupAsyncWithDialog(import('./emoji-edit-dialog.vue').then(x => x.default), {
emoji: emoji,
}, {
@@ -154,7 +161,13 @@ const edit = async (emoji) => {
});
};
-const detailRemoteEmoji = (emoji) => {
+const detailRemoteEmoji = (emoji: {
+ id: string,
+ name: string,
+ host: string,
+ license: string | null,
+ url: string
+}) => {
const { dispose } = os.popup(MkRemoteEmojiEditDialog, {
emoji: emoji,
}, {
@@ -167,13 +180,19 @@ const detailRemoteEmoji = (emoji) => {
});
};
-const importEmoji = (emoji) => {
+const importEmoji = (emojiId: string) => {
os.apiWithDialog('admin/emoji/copy', {
- emojiId: emoji.id,
+ emojiId: emojiId,
});
};
-const remoteMenu = (emoji, ev: MouseEvent) => {
+const remoteMenu = (emoji: {
+ id: string,
+ name: string,
+ host: string,
+ license: string | null,
+ url: string
+}, ev: PointerEvent) => {
os.popupMenu([{
type: 'label',
text: ':' + emoji.name + ':',
@@ -184,11 +203,11 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
}, {
text: i18n.ts.import,
icon: 'ti ti-plus',
- action: () => { importEmoji(emoji); },
+ action: () => { importEmoji(emoji.id); },
}], ev.currentTarget ?? ev.target);
};
-const menu = (ev: MouseEvent) => {
+const menu = (ev: PointerEvent) => {
os.popupMenu([{
icon: 'ti ti-download',
text: i18n.ts.export,
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index e3cc1d988e..6b57684188 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -131,10 +131,11 @@ function move() {
const f = file.value;
- selectDriveFolder(null).then(folder => {
+ selectDriveFolder(null).then(({ canceled, folders }) => {
+ if (canceled) return;
misskeyApi('drive/files/update', {
fileId: f.id,
- folderId: folder[0] ? folder[0].id : null,
+ folderId: folders[0] ? folders[0].id : null,
}).then(async () => {
await _fetch_();
});
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index 88300d8a74..21e4657b2c 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
<img v-if="store.s.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
- <canvas ref="canvasEl" :class="$style.canvas"/>
+ <canvas ref="canvasEl" :class="$style.canvas"></canvas>
<Transition
:enterActiveClass="$style.transition_combo_enterActive"
:leaveActiveClass="$style.transition_combo_leaveActive"
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</Transition>
<template v-if="dropReady && currentPick">
<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow"/>
- <div :class="$style.dropGuide"/>
+ <div :class="$style.dropGuide"></div>
</template>
</div>
<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
@@ -729,7 +729,7 @@ async function start() {
}, 1500);
}
-function onClick(ev: MouseEvent) {
+function onClick(ev: PointerEvent) {
if (!containerElRect) return;
if (replaying.value) return;
const x = (ev.clientX - containerElRect.left) / viewScale;
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index ea4863950d..edd3987524 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
- <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
+ <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role)"><i class="ti ti-x"></i></button>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
</div>
@@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo>
</div>
</MkFolder>
- <MkSwitch v-model="isSensitive">isSensitive</MkSwitch>
+ <MkSwitch v-model="isSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
<MkButton v-if="emoji" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
@@ -99,7 +99,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
- (ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
+ (ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.EmojiDetailed; created?: Misskey.entities.EmojiDetailed }): void,
(ev: 'closed'): void
}>();
@@ -120,7 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null);
-async function changeImage(ev: Event) {
+async function changeImage(ev: PointerEvent) {
file.value = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -143,7 +143,7 @@ async function addRole() {
rolesThatCanBeUsedThisEmojiAsReaction.value.push(roles.find(r => r.id === roleId)!);
}
-async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
+async function removeRole(role: Misskey.entities.RoleLite) {
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
}
@@ -157,19 +157,29 @@ async function done() {
localOnly: localOnly.value,
roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id),
fileId: file.value ? file.value.id : undefined,
- };
+ } satisfies Misskey.entities.AdminEmojiUpdateRequest;
if (props.emoji) {
+ const emojiDetailed = {
+ id: props.emoji.id,
+ aliases: params.aliases,
+ name: params.name,
+ category: params.category,
+ host: props.emoji.host,
+ url: file.value ? file.value.url : props.emoji.url,
+ license: params.license,
+ isSensitive: params.isSensitive,
+ localOnly: params.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: params.roleIdsThatCanBeUsedThisEmojiAsReaction,
+ } satisfies Misskey.entities.EmojiDetailed;
+
await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
...params,
});
emit('done', {
- updated: {
- id: props.emoji.id,
- ...params,
- },
+ updated: emojiDetailed,
});
windowEl.value?.close();
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index aaf433e78e..bed7f2166a 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -15,7 +15,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { defineAsyncComponent } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { misskeyApiGet } from '@/utility/misskey-api.js';
@@ -28,7 +27,7 @@ const props = defineProps<{
emoji: Misskey.entities.EmojiSimple;
}>();
-function menu(ev) {
+function menu(ev: PointerEvent) {
const menuItems: MenuItem[] = [];
menuItems.push({
type: 'label',
@@ -57,22 +56,21 @@ function menu(ev) {
menuItems.push({
text: i18n.ts.edit,
icon: 'ti ti-pencil',
- action: () => {
- edit(props.emoji);
+ action: async () => {
+ const detailedEmoji = await misskeyApiGet('emoji', {
+ name: props.emoji.name,
+ });
+ const { dispose } = await os.popupAsyncWithDialog(import('@/pages/emoji-edit-dialog.vue').then(x => x.default), {
+ emoji: detailedEmoji,
+ }, {
+ closed: () => dispose(),
+ });
},
});
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
-
-const edit = async (emoji) => {
- const { dispose } = await os.popupAsyncWithDialog(import('@/pages/emoji-edit-dialog.vue').then(x => x.default), {
- emoji: emoji,
- }, {
- closed: () => dispose(),
- });
-};
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index b3e8e88c23..3d9de0584a 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -395,7 +395,7 @@ const {
});
const script = ref(flash.value?.script ?? PRESET_DEFAULT);
-function selectPreset(ev: MouseEvent) {
+function selectPreset(ev: PointerEvent) {
os.popupMenu([{
text: 'Omikuji',
action: () => {
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index efc9ee014f..449f1af60a 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -104,7 +104,7 @@ function fetchFlash() {
});
}
-function share(ev: MouseEvent) {
+function share(ev: PointerEvent) {
if (!flash.value) return;
const menuItems: MenuItem[] = [];
@@ -151,9 +151,11 @@ function shareWithNote() {
});
}
-function like() {
+async function like() {
if (!flash.value) return;
- pleaseLogin();
+
+ const isLoggedIn = await pleaseLogin();
+ if (!isLoggedIn) return;
os.apiWithDialog('flash/like', {
flashId: flash.value.id,
@@ -165,7 +167,9 @@ function like() {
async function unlike() {
if (!flash.value) return;
- pleaseLogin();
+
+ const isLoggedIn = await pleaseLogin();
+ if (!isLoggedIn) return;
const confirm = await os.confirm({
type: 'warning',
@@ -208,7 +212,7 @@ async function run() {
const version = utils.getLangVersion(flash.value.script);
const isLegacy = getIsLegacy(version);
- const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');
+ const { Interpreter, Parser, values } = (isLegacy ? (await import('@syuilo/aiscript-0-19-0')) : await import('@syuilo/aiscript')) as typeof import('@syuilo/aiscript');
const parser = new Parser();
@@ -225,10 +229,10 @@ async function run() {
THIS_URL: values.STR(`${url}/play/${flash.value.id}`),
}, {
in: aiScriptReadline,
- out: (value) => {
+ out: () => {
// nop
},
- log: (type, params) => {
+ log: () => {
// nop
},
});
@@ -269,7 +273,7 @@ async function reportAbuse() {
});
}
-function showMenu(ev: MouseEvent) {
+function showMenu(ev: PointerEvent) {
if (!flash.value) return;
const menu: MenuItem[] = [
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index ba24d7abc6..2404fd9744 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<p class="acct">@{{ acct(displayUser(req)) }}</p>
</div>
<div v-if="tab === 'list'" class="commands">
- <MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
- <MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
+ <MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"></i> {{ i18n.ts.accept }}</MkButton>
+ <MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"></i> {{ i18n.ts.reject }}</MkButton>
</div>
<div v-else class="commands">
- <MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.cancel }}</MkButton>
+ <MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
</div>
</div>
</div>
@@ -89,7 +89,7 @@ async function cancel(user: Misskey.entities.UserLite) {
});
}
-function displayUser(req) {
+function displayUser(req: Misskey.entities.FollowingRequestsListResponse[number]) {
return tab.value === 'list' ? req.follower : req.followee;
}
diff --git a/packages/frontend/src/pages/gallery/edit.root.vue b/packages/frontend/src/pages/gallery/edit.root.vue
index 45493ab561..ec0a293494 100644
--- a/packages/frontend/src/pages/gallery/edit.root.vue
+++ b/packages/frontend/src/pages/gallery/edit.root.vue
@@ -58,7 +58,7 @@ const description = ref(props.post?.description ?? null);
const title = ref(props.post?.title ?? '');
const isSensitive = ref(props.post?.isSensitive ?? false);
-function chooseFile(evt) {
+function chooseFile(evt: MouseEvent) {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: true,
@@ -67,7 +67,7 @@ function chooseFile(evt) {
});
}
-function remove(file) {
+function remove(file: NonNullable<Misskey.entities.GalleryPost['files']>[number]) {
files.value = files.value.filter(f => f.id !== file.id);
}
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index f60bbc0b74..92cb663ee1 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
- <button v-if="$i && $i.id !== post.user.id" v-click-anime class="_button" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button>
+ <button v-if="$i && $i.id !== post.user.id" v-click-anime class="_button" @click="showMenu"><i class="ti ti-dots ti-fw"></i></button>
</div>
</div>
<div class="user">
@@ -175,7 +175,7 @@ async function reportAbuse() {
});
}
-function showMenu(ev: MouseEvent) {
+function showMenu(ev: PointerEvent) {
if (!post.value) return;
const menuItems: MenuItem[] = [];
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 132c55571a..92a5d25983 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -110,7 +110,7 @@ function addUser() {
});
}
-async function removeUser(item, ev) {
+async function removeUser(item: Misskey.entities.UsersListsGetMembershipsResponse[number], ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.remove,
icon: 'ti ti-x',
@@ -127,7 +127,7 @@ async function removeUser(item, ev) {
}], ev.currentTarget ?? ev.target);
}
-async function showMembershipMenu(item, ev) {
+async function showMembershipMenu(item: Misskey.entities.UsersListsGetMembershipsResponse[number], ev: PointerEvent) {
const withRepliesRef = ref(item.withReplies);
os.popupMenu([{
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index 5d308e6b29..37ec6284a3 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, markRaw, ref } from 'vue';
import { notificationTypes } from 'misskey-js';
+import type { PageHeaderItem } from '@/types/page-header.js';
import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import * as os from '@/os.js';
@@ -44,7 +45,7 @@ const directNotesPaginator = markRaw(new Paginator('notes/mentions', {
},
}));
-function setFilter(ev) {
+function setFilter(ev: PointerEvent) {
const typeItems = notificationTypes.map(t => ({
text: i18n.ts._notification._types[t],
active: (includeTypes.value && includeTypes.value.includes(t)) ?? false,
@@ -62,7 +63,7 @@ function setFilter(ev) {
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
-const headerActions = computed(() => [tab.value === 'all' ? {
+const headerActions = computed<PageHeaderItem[]>(() => ([tab.value === 'all' ? {
text: i18n.ts.filter,
icon: 'ti ti-filter',
highlighted: includeTypes.value != null,
@@ -73,7 +74,7 @@ const headerActions = computed(() => [tab.value === 'all' ? {
handler: () => {
os.apiWithDialog('notifications/mark-all-as-read', {});
},
-} : undefined].filter(x => x !== undefined));
+} : undefined] as (PageHeaderItem | undefined)[]).filter(x => x !== undefined));
const headerTabs = computed(() => [{
key: 'all',
diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
index f191320180..18f6c40013 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
@@ -4,36 +4,41 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
- <template #item="{element}">
- <div :class="$style.item">
- <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
- <component :is="getComponent(element.type)" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/>
+<MkDraggable
+ :modelValue="modelValue"
+ direction="vertical"
+ withGaps
+ canNest
+ group="pageBlocks"
+ @update:modelValue="v => emit('update:modelValue', v)"
+>
+ <template #default="{ item }">
+ <div>
+ <!-- divが無いとエラーになる -->
+ <component :is="getComponent(item.type) as any" :modelValue="item" @update:modelValue="updateItem" @remove="() => removeItem(item)"/>
</div>
</template>
-</Sortable>
+</MkDraggable>
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import XSection from './els/page-editor.el.section.vue';
import XText from './els/page-editor.el.text.vue';
import XImage from './els/page-editor.el.image.vue';
import XNote from './els/page-editor.el.note.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
-function getComponent(type: string) {
+function getComponent(type: Misskey.entities.Page['content'][number]['type']) {
switch (type) {
case 'section': return XSection;
case 'text': return XText;
case 'image': return XImage;
case 'note': return XNote;
- default: return null;
+ default: return XText;
}
}
-const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
-
const props = defineProps<{
modelValue: Misskey.entities.Page['content'];
}>();
@@ -42,7 +47,7 @@ const emit = defineEmits<{
(ev: 'update:modelValue', value: Misskey.entities.Page['content']): void;
}>();
-function updateItem(v) {
+function updateItem(v: Misskey.entities.PageBlock) {
const i = props.modelValue.findIndex(x => x.id === v.id);
const newValue = [
...props.modelValue.slice(0, i),
@@ -52,8 +57,8 @@ function updateItem(v) {
emit('update:modelValue', newValue);
}
-function removeItem(el) {
- const i = props.modelValue.findIndex(x => x.id === el.id);
+function removeItem(v: Misskey.entities.PageBlock) {
+ const i = props.modelValue.findIndex(x => x.id === v.id);
const newValue = [
...props.modelValue.slice(0, i),
...props.modelValue.slice(i + 1),
@@ -61,11 +66,3 @@ function removeItem(el) {
emit('update:modelValue', newValue);
}
</script>
-
-<style lang="scss" module>
-.item {
- & + .item {
- margin-top: 16px;
- }
-}
-</style>
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index 3b36f7fa2d..85871c993c 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -247,9 +247,9 @@ async function add() {
}
}
-function setEyeCatchingImage(img: Event) {
+function setEyeCatchingImage(ev: PointerEvent) {
selectFile({
- anchorElement: img.currentTarget ?? img.target,
+ anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
}).then(file => {
eyeCatchingImageId.value = file.id;
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index c3b52a24fd..212c8140c8 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA>
<button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
- <button v-if="$i" v-click-anime class="_button" :class="$style.generalActionButton" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button>
+ <button v-if="$i" v-click-anime class="_button" :class="$style.generalActionButton" @click="showMenu"><i class="ti ti-dots ti-fw"></i></button>
</div>
</div>
<div :class="$style.pageUser">
@@ -163,7 +163,7 @@ function fetchPage() {
});
}
-function share(ev: MouseEvent) {
+function share(ev: PointerEvent) {
if (!page.value) return;
const menuItems: MenuItem[] = [];
@@ -237,7 +237,7 @@ async function unlike() {
});
}
-function pin(pin) {
+function pin(pin: boolean) {
if (!page.value) return;
os.apiWithDialog('i/update', {
@@ -258,7 +258,7 @@ async function reportAbuse() {
});
}
-function showMenu(ev: MouseEvent) {
+function showMenu(ev: PointerEvent) {
if (!page.value) return;
const menuItems: MenuItem[] = [];
diff --git a/packages/frontend/src/pages/qr.read.vue b/packages/frontend/src/pages/qr.read.vue
index 251dccd0f0..5e3633c052 100644
--- a/packages/frontend/src/pages/qr.read.vue
+++ b/packages/frontend/src/pages/qr.read.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>
<div :class="$style.view">
<video ref="videoEl" :class="$style.video" autoplay muted playsinline></video>
- <div ref="overlayEl" :class="$style.overlay"></div>
+ <div ref="overlayEl"></div>
<div :class="$style.controls">
<MkButton v-tooltip="i18n.ts._qr.scanFile" iconOnly @click="upload"><i class="ti ti-photo-plus"></i></MkButton>
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index aae638641a..5988604652 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -151,7 +151,7 @@ import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { deepClone } from '@/utility/clone.js';
-import { ensureSignin } from '@/i.js';
+import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { userPage } from '@/filters/user.js';
@@ -160,8 +160,6 @@ import * as os from '@/os.js';
import { confetti } from '@/utility/confetti.js';
import { genId } from '@/utility/id.js';
-const $i = ensureSignin();
-
const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed;
connection?: Misskey.IChannelConnection<Misskey.Channels['reversiGame']> | null;
@@ -182,13 +180,13 @@ const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
}));
const iAmPlayer = computed(() => {
- return game.value.user1Id === $i.id || game.value.user2Id === $i.id;
+ return game.value.user1Id === $i?.id || game.value.user2Id === $i?.id;
});
const myColor = computed(() => {
if (!iAmPlayer.value) return null;
- if (game.value.user1Id === $i.id && game.value.black === 1) return true;
- if (game.value.user2Id === $i.id && game.value.black === 2) return true;
+ if (game.value.user1Id === $i?.id && game.value.black === 1) return true;
+ if (game.value.user2Id === $i?.id && game.value.black === 2) return true;
return false;
});
@@ -219,7 +217,7 @@ const isMyTurn = computed(() => {
if (!iAmPlayer.value) return false;
const u = turnUser.value;
if (u == null) return false;
- return u.id === $i.id;
+ return u.id === $i?.id;
});
const cellsStyle = computed(() => {
@@ -308,7 +306,7 @@ if (!props.game.isEnded) {
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
}
-async function onStreamLog(log) {
+async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
game.value.logs = Reversi.Serializer.serializeLogs([
...Reversi.Serializer.deserializeLogs(game.value.logs),
log,
@@ -348,10 +346,13 @@ async function onStreamLog(log) {
}
}
-function onStreamEnded(x) {
+function onStreamEnded(x: {
+ winnerId: Misskey.entities.User['id'] | null;
+ game: Misskey.entities.ReversiGameDetailed;
+}) {
game.value = deepClone(x.game);
- if (game.value.winnerId === $i.id) {
+ if (game.value.winnerId === $i?.id) {
confetti({
duration: 1000 * 3,
});
@@ -384,7 +385,7 @@ function checkEnd() {
}
}
-function restoreGame(_game) {
+function restoreGame(_game: Misskey.entities.ReversiGameDetailed) {
game.value = deepClone(_game);
engine.value = Reversi.Serializer.restoreGame({
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 1e01496bbb..f3f89d163b 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -35,22 +35,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
- <MkRadios v-model="game.bw">
- <option value="random">{{ i18n.ts.random }}</option>
- <option :value="'1'">
+ <MkRadios
+ v-model="game.bw"
+ :options="[
+ { value: 'random', label: i18n.ts.random },
+ { value: '1', slotId: 'user1' },
+ { value: '2', slotId: 'user2' },
+ ]"
+ >
+ <template #option-user1>
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
<template #name>
<b><MkUserName :user="game.user1"/></b>
</template>
</I18n>
- </option>
- <option :value="'2'">
+ </template>
+ <template #option-user2>
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
<template #name>
<b><MkUserName :user="game.user2"/></b>
</template>
</I18n>
- </option>
+ </template>
</MkRadios>
</MkFolder>
@@ -58,15 +64,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
- <MkRadios v-model="game.timeLimitForEachTurn">
- <option :value="5">5{{ i18n.ts._time.second }}</option>
- <option :value="10">10{{ i18n.ts._time.second }}</option>
- <option :value="30">30{{ i18n.ts._time.second }}</option>
- <option :value="60">60{{ i18n.ts._time.second }}</option>
- <option :value="90">90{{ i18n.ts._time.second }}</option>
- <option :value="120">120{{ i18n.ts._time.second }}</option>
- <option :value="180">180{{ i18n.ts._time.second }}</option>
- <option :value="3600">3600{{ i18n.ts._time.second }}</option>
+ <MkRadios
+ v-model="game.timeLimitForEachTurn"
+ :options="gameTurnOptionsDef"
+ >
</MkRadios>
</MkFolder>
@@ -110,22 +111,21 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
+import { computed, watch, ref, onUnmounted } from 'vue';
import * as Misskey from 'misskey-js';
import * as Reversi from 'misskey-reversi';
+import type { MenuItem } from '@/types/menu.js';
import { i18n } from '@/i18n.js';
-import { ensureSignin } from '@/i.js';
+import { $i } from '@/i.js';
import { deepClone } from '@/utility/clone.js';
import MkButton from '@/components/MkButton.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
-import type { MenuItem } from '@/types/menu.js';
+import type { MkRadiosOption } from '@/components/MkRadios.vue';
import { useRouter } from '@/router.js';
-const $i = ensureSignin();
-
const router = useRouter();
const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category)));
@@ -139,19 +139,30 @@ const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false }
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
+const gameTurnOptionsDef = [
+ { value: 5, label: '5' + i18n.ts._time.second },
+ { value: 10, label: '10' + i18n.ts._time.second },
+ { value: 30, label: '30' + i18n.ts._time.second },
+ { value: 60, label: '60' + i18n.ts._time.second },
+ { value: 90, label: '90' + i18n.ts._time.second },
+ { value: 120, label: '120' + i18n.ts._time.second },
+ { value: 180, label: '180' + i18n.ts._time.second },
+ { value: 3600, label: '3600' + i18n.ts._time.second },
+] as MkRadiosOption<number>[];
+
const mapName = computed(() => {
if (game.value.map == null) return 'Random';
const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join(''));
return found ? found.name! : '-Custom-';
});
const isReady = computed(() => {
- if (game.value.user1Id === $i.id && game.value.user1Ready) return true;
- if (game.value.user2Id === $i.id && game.value.user2Ready) return true;
+ if (game.value.user1Id === $i?.id && game.value.user1Ready) return true;
+ if (game.value.user2Id === $i?.id && game.value.user2Ready) return true;
return false;
});
const isOpReady = computed(() => {
- if (game.value.user1Id !== $i.id && game.value.user1Ready) return true;
- if (game.value.user2Id !== $i.id && game.value.user2Ready) return true;
+ if (game.value.user1Id !== $i?.id && game.value.user1Ready) return true;
+ if (game.value.user2Id !== $i?.id && game.value.user2Ready) return true;
return false;
});
@@ -165,7 +176,7 @@ watch(() => game.value.timeLimitForEachTurn, () => {
updateSettings('timeLimitForEachTurn');
});
-function chooseMap(ev: MouseEvent) {
+function chooseMap(ev: PointerEvent) {
const menu: MenuItem[] = [];
for (const c of mapCategories) {
@@ -212,7 +223,10 @@ function unready() {
props.connection.send('ready', false);
}
-function onChangeReadyStates(states) {
+function onChangeReadyStates(states: {
+ user1: boolean;
+ user2: boolean;
+}) {
game.value.user1Ready = states.user1;
game.value.user2Ready = states.user2;
}
@@ -225,7 +239,7 @@ function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
}
function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
- if (userId === $i.id) return;
+ if (userId === $i?.id) return;
if (game.value[key] === value) return;
game.value[key] = value;
if (isReady.value) {
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index b1ba4da247..926d825b66 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -17,15 +17,13 @@ import GameBoard from './game.board.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
import { useStream } from '@/stream.js';
-import { ensureSignin } from '@/i.js';
+import { $i } from '@/i.js';
import { useRouter } from '@/router.js';
import * as os from '@/os.js';
import { url } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@@/js/use-interval.js';
-const $i = ensureSignin();
-
const router = useRouter();
const props = defineProps<{
@@ -74,7 +72,7 @@ async function fetchGame() {
connection.value.on('canceled', x => {
connection.value?.dispose();
- if (x.userId !== $i.id) {
+ if (x.userId !== $i?.id) {
os.alert({
type: 'warning',
text: i18n.ts._reversi.gameCanceled,
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index 0ae374649d..9a737e93ac 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -197,7 +197,8 @@ async function matchHeatbeat() {
}
async function matchUser() {
- pleaseLogin();
+ const isLoggedIn = await pleaseLogin();
+ if (!isLoggedIn) return;
const user = await os.selectUser({ includeSelf: false, localOnly: true });
if (user == null) return;
@@ -207,8 +208,9 @@ async function matchUser() {
matchHeatbeat();
}
-function matchAny(ev: MouseEvent) {
- pleaseLogin();
+async function matchAny(ev: PointerEvent) {
+ const isLoggedIn = await pleaseLogin();
+ if (!isLoggedIn) return;
os.popupMenu([{
text: i18n.ts._reversi.allowIrregularRules,
@@ -237,11 +239,11 @@ function cancelMatching() {
}
}
-async function accept(user) {
+async function accept(user: Misskey.entities.UserLite) {
const game = await misskeyApi('reversi/match', {
userId: user.id,
});
- if (game) {
+ if (game != null) {
startGame(game);
}
}
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 4e02556c83..b3b899517e 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -97,7 +97,7 @@ watch(code, () => {
miLocalStorage.setItem('scratchpad', code.value);
});
-function stringifyUiProps(uiProps) {
+function stringifyUiProps(uiProps: AsUiComponent) {
return JSON.stringify(
{ ...uiProps, type: undefined, id: undefined },
(k, v) => typeof v === 'function' ? '<function>' : v,
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index fb34d592a6..ab36f2e6c5 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -19,11 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>{{ i18n.ts.options }}</template>
<div class="_gaps_m">
- <MkRadios v-model="searchScope">
- <option v-if="instance.federation !== 'none' && noteSearchableScope === 'global'" value="all">{{ i18n.ts._search.searchScopeAll }}</option>
- <option value="local">{{ instance.federation === 'none' ? i18n.ts._search.searchScopeAll : i18n.ts._search.searchScopeLocal }}</option>
- <option v-if="instance.federation !== 'none' && noteSearchableScope === 'global'" value="server">{{ i18n.ts._search.searchScopeServer }}</option>
- <option value="user">{{ i18n.ts._search.searchScopeUser }}</option>
+ <MkRadios
+ v-model="searchScope"
+ :options="searchScopeDef"
+ >
</MkRadios>
<div v-if="instance.federation !== 'none' && searchScope === 'server'" :class="$style.subOptionRoot">
@@ -71,7 +70,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserCardMini
:user="user"
:withChart="false"
- :class="$style.userSelectedCard"
/>
</div>
<div>
@@ -128,6 +126,7 @@ import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { Paginator } from '@/utility/paginator.js';
+import type { MkRadiosOption } from '@/components/MkRadios.vue';
const props = withDefaults(defineProps<{
query?: string;
@@ -184,6 +183,24 @@ const searchScope = ref<'all' | 'local' | 'server' | 'user'>((() => {
return 'all';
})());
+const searchScopeDef = computed<MkRadiosOption[]>(() => {
+ const options: MkRadiosOption[] = [];
+
+ if (instance.federation !== 'none' && noteSearchableScope === 'global') {
+ options.push({ value: 'all', label: i18n.ts._search.searchScopeAll });
+ }
+
+ options.push({ value: 'local', label: instance.federation === 'none' ? i18n.ts._search.searchScopeAll : i18n.ts._search.searchScopeLocal });
+
+ if (instance.federation !== 'none' && noteSearchableScope === 'global') {
+ options.push({ value: 'server', label: i18n.ts._search.searchScopeServer });
+ }
+
+ options.push({ value: 'user', label: i18n.ts._search.searchScopeUser });
+
+ return options;
+});
+
type SearchParams = {
readonly query: string;
readonly host?: string;
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index 5110fca10c..cc91adb63d 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -9,10 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
- <MkRadios v-if="instance.federation !== 'none'" v-model="searchOrigin" @update:modelValue="search()">
- <option value="combined">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.local }}</option>
- <option value="remote">{{ i18n.ts.remote }}</option>
+ <MkRadios
+ v-if="instance.federation !== 'none'"
+ v-model="searchOrigin"
+ :options="[
+ { value: 'combined', label: i18n.ts.all },
+ { value: 'local', label: i18n.ts.local },
+ { value: 'remote', label: i18n.ts.remote },
+ ]"
+ @update:modelValue="search()"
+ >
</MkRadios>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 2cc13744b1..bf71845a38 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
- <div v-text="i18n.ts._2fa.alreadyRegistered"/>
+ <div>{{ i18n.ts._2fa.alreadyRegistered }}</div>
<template v-if="$i.securityKeysList!.length > 0">
<MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
<MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
@@ -85,6 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, computed } from 'vue';
import { supported as webAuthnSupported, create as webAuthnCreate, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -156,7 +157,7 @@ function renewTOTP(): void {
});
}
-async function unregisterKey(key) {
+async function unregisterKey(key: NonNullable<Misskey.entities.MeDetailedOnly['securityKeysList']>[number]) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts._2fa.removeKey,
@@ -175,7 +176,7 @@ async function unregisterKey(key) {
os.success();
}
-async function renameKey(key) {
+async function renameKey(key: NonNullable<Misskey.entities.MeDetailedOnly['securityKeysList']>[number]) {
const name = await os.inputText({
title: i18n.ts.rename,
default: key.name,
diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue
index c75667b06b..b07515a49a 100644
--- a/packages/frontend/src/pages/settings/account-data.vue
+++ b/packages/frontend/src/pages/settings/account-data.vue
@@ -189,7 +189,7 @@ const onImportSuccess = () => {
});
};
-const onError = (ev) => {
+const onError = (ev: Error) => {
os.alert({
type: 'error',
text: ev.message,
@@ -232,7 +232,7 @@ const exportAntennas = () => {
misskeyApi('i/export-antennas', {}).then(onExportSuccess).catch(onError);
};
-const importFollowing = async (ev) => {
+const importFollowing = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -243,7 +243,7 @@ const importFollowing = async (ev) => {
}).then(onImportSuccess).catch(onError);
};
-const importUserLists = async (ev) => {
+const importUserLists = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -251,7 +251,7 @@ const importUserLists = async (ev) => {
misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
-const importMuting = async (ev) => {
+const importMuting = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -259,7 +259,7 @@ const importMuting = async (ev) => {
misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
-const importBlocking = async (ev) => {
+const importBlocking = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -267,7 +267,7 @@ const importBlocking = async (ev) => {
misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
-const importAntennas = async (ev) => {
+const importAntennas = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 764ec72652..55a81bbf38 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -38,7 +38,7 @@ function refreshAllAccounts() {
// TODO
}
-function showMenu(host: string, id: string, ev: MouseEvent) {
+function showMenu(host: string, id: string, ev: PointerEvent) {
let menu: MenuItem[];
menu = [{
@@ -54,7 +54,7 @@ function showMenu(host: string, id: string, ev: MouseEvent) {
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
-function addAccount(ev: MouseEvent) {
+function addAccount(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.existingAccount,
action: () => { addExistingAccount(); },
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 10901f737b..e9857b1e0b 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.permission }}</template>
<template #suffix>{{ Object.keys(token.permission).length === 0 ? i18n.ts.none : Object.keys(token.permission).length }}</template>
<ul>
- <li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
+ <li v-for="p in token.permission" :key="p">{{ (i18n.ts._permissions as any)[p] ?? p }}</li>
</ul>
</MkFolder>
</div>
@@ -68,7 +68,7 @@ const paginator = markRaw(new Paginator('i/apps', {
},
}));
-function revoke(token) {
+function revoke(token: Misskey.entities.IAppsResponse[number]) {
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
paginator.reload();
});
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index 7a19b0495b..40fee6caaf 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -40,31 +40,43 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['column', 'align']">
<MkPreferenceContainer k="deck.columnAlign">
- <MkRadios v-model="columnAlign">
+ <MkRadios
+ v-model="columnAlign"
+ :options="[
+ { value: 'left', label: i18n.ts.left },
+ { value: 'center', label: i18n.ts.center },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts._deck.columnAlign }}</SearchLabel></template>
- <option value="left">{{ i18n.ts.left }}</option>
- <option value="center">{{ i18n.ts.center }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['menu', 'position']">
<MkPreferenceContainer k="deck.menuPosition">
- <MkRadios v-model="menuPosition">
+ <MkRadios
+ v-model="menuPosition"
+ :options="[
+ { value: 'right', label: i18n.ts.right },
+ { value: 'bottom', label: i18n.ts.bottom },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts._deck.deckMenuPosition }}</SearchLabel></template>
- <option value="right">{{ i18n.ts.right }}</option>
- <option value="bottom">{{ i18n.ts.bottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['navbar', 'position']">
<MkPreferenceContainer k="deck.navbarPosition">
- <MkRadios v-model="navbarPosition">
+ <MkRadios
+ v-model="navbarPosition"
+ :options="[
+ { value: 'left', label: i18n.ts.left },
+ { value: 'top', label: i18n.ts.top },
+ { value: 'bottom', label: i18n.ts.bottom },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts._deck.navbarPosition }}</SearchLabel></template>
- <option value="left">{{ i18n.ts.left }}</option>
- <option value="top">{{ i18n.ts.top }}</option>
- <option value="bottom">{{ i18n.ts.bottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@@ -113,7 +125,7 @@ watch(wallpaper, () => {
suggestReload();
});
-function setWallpaper(ev: MouseEvent) {
+function setWallpaper(ev: PointerEvent) {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index 57192c0fb7..7189e19780 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -60,6 +60,7 @@ import bytes from '@/filters/bytes.js';
import { definePage } from '@/page.js';
import MkSelect from '@/components/MkSelect.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
+import { useGlobalEvent } from '@/events.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js';
@@ -115,14 +116,20 @@ function genUsageBar(fsize: number): StyleValue {
};
}
-function onClick(ev: MouseEvent, file) {
+function onClick(ev: PointerEvent, file: Misskey.entities.DriveFile) {
os.popupMenu(getDriveFileMenu(file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
-function onContextMenu(ev: MouseEvent, file): void {
+function onContextMenu(ev: PointerEvent, file: Misskey.entities.DriveFile): void {
os.contextMenu(getDriveFileMenu(file), ev);
}
+useGlobalEvent('driveFilesDeleted', (files) => {
+ for (const f of files) {
+ paginator.removeItem(f.id);
+ }
+});
+
definePage(() => ({
title: i18n.ts.drivecleaner,
icon: 'ti ti-trash',
diff --git a/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue b/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue
index 62922fc964..f92e87375f 100644
--- a/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue
+++ b/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue
@@ -52,7 +52,7 @@ async function edit() {
});
}
-function del(ev: MouseEvent) {
+function del(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.delete,
action: () => {
diff --git a/packages/frontend/src/pages/settings/drive.WatermarkItem.vue b/packages/frontend/src/pages/settings/drive.WatermarkItem.vue
index 0c03a4493a..9e80d719de 100644
--- a/packages/frontend/src/pages/settings/drive.WatermarkItem.vue
+++ b/packages/frontend/src/pages/settings/drive.WatermarkItem.vue
@@ -52,7 +52,7 @@ async function edit() {
});
}
-function del(ev: MouseEvent) {
+function del(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.delete,
action: () => {
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 8d443921a9..b170d17a5a 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -296,8 +296,9 @@ if (prefer.s.uploadFolder) {
}
function chooseUploadFolder() {
- selectDriveFolder(null).then(async folder => {
- prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
+ selectDriveFolder(null).then(async ({ canceled, folders }) => {
+ if (canceled) return;
+ prefer.commit('uploadFolder', folders[0] ? folders[0].id : null);
os.success();
if (prefer.s.uploadFolder) {
uploadFolder.value = await misskeyApi('drive/folders/show', {
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index 469a3c2f1c..85fea7ae66 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -76,11 +76,11 @@ const $i = ensureSignin();
const emailAddress = ref($i.email ?? '');
-const onChangeReceiveAnnouncementEmail = (v) => {
+function onChangeReceiveAnnouncementEmail(v: boolean) {
misskeyApi('i/update', {
receiveAnnouncementEmail: v,
});
-};
+}
async function saveEmailAddress() {
const auth = await os.authenticateDialog();
diff --git a/packages/frontend/src/pages/settings/emoji-palette.palette.vue b/packages/frontend/src/pages/settings/emoji-palette.palette.vue
index b624d424f3..d8a5f16b7d 100644
--- a/packages/frontend/src/pages/settings/emoji-palette.palette.vue
+++ b/packages/frontend/src/pages/settings/emoji-palette.palette.vue
@@ -18,19 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<div v-panel style="border-radius: 6px;">
- <Sortable
- v-model="emojis"
+ <MkDraggable
+ :modelValue="emojis.map(emoji => ({ id: emoji, emoji }))"
+ direction="horizontal"
:class="$style.emojis"
- :itemKey="item => item"
- :animation="150"
- :delay="100"
- :delayOnTouchOnly="true"
- :group="{ name: 'SortableEmojiPalettes' }"
+ group="emojiPalettes"
+ @update:modelValue="v => emojis = v.map(x => x.emoji)"
>
- <template #item="{element}">
- <button class="_button" :class="$style.emojisItem" @click="remove(element, $event)">
- <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
- <MkEmoji v-else :emoji="element" :normal="true"/>
+ <template #default="{ item }">
+ <button class="_button" :class="$style.emojisItem" @click="remove(item.emoji, $event)">
+ <!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
+ <MkCustomEmoji v-if="item.emoji[0] === ':'" style="pointer-events: none;" :name="item.emoji" :normal="true" :fallbackToImage="true"/>
+ <MkEmoji v-else style="pointer-events: none;" :emoji="item.emoji" :normal="true"/>
</button>
</template>
<template #footer>
@@ -38,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-plus"></i>
</button>
</template>
- </Sortable>
+ </MkDraggable>
</div>
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
</div>
@@ -47,7 +46,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
-import Sortable from 'vuedraggable';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -55,6 +53,7 @@ import { deepClone } from '@/utility/clone.js';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkFolder from '@/components/MkFolder.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
const props = defineProps<{
@@ -77,7 +76,7 @@ watch(emojis, () => {
emit('updateEmojis', emojis.value);
}, { deep: true });
-function remove(reaction: string, ev: MouseEvent) {
+function remove(reaction: string, ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.remove,
action: () => {
@@ -86,7 +85,7 @@ function remove(reaction: string, ev: MouseEvent) {
}], getHTMLElement(ev));
}
-function pick(ev: MouseEvent) {
+function pick(ev: PointerEvent) {
os.pickEmoji(getHTMLElement(ev), {
showPinned: false,
}).then(it => {
@@ -97,7 +96,7 @@ function pick(ev: MouseEvent) {
});
}
-function getHTMLElement(ev: MouseEvent): HTMLElement {
+function getHTMLElement(ev: PointerEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
@@ -125,7 +124,7 @@ function paste() {
});
}
-function del(ev: MouseEvent) {
+function del(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.delete,
action: () => {
diff --git a/packages/frontend/src/pages/settings/emoji-palette.vue b/packages/frontend/src/pages/settings/emoji-palette.vue
index 7f31699ed1..cb665554cd 100644
--- a/packages/frontend/src/pages/settings/emoji-palette.vue
+++ b/packages/frontend/src/pages/settings/emoji-palette.vue
@@ -63,38 +63,33 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<SearchMarker :keywords="['emoji', 'picker', 'scale', 'size']">
<MkPreferenceContainer k="emojiPickerScale">
- <MkRadios v-model="emojiPickerScale">
+ <MkRadios
+ v-model="emojiPickerScale"
+ :options="emojiPickerScaleDef"
+ >
<template #label><SearchLabel>{{ i18n.ts.size }}</SearchLabel></template>
- <option :value="1">{{ i18n.ts.small }}</option>
- <option :value="2">{{ i18n.ts.medium }}</option>
- <option :value="3">{{ i18n.ts.large }}</option>
- <option :value="4">{{ i18n.ts.large }}+</option>
- <option :value="5">{{ i18n.ts.large }}++</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['emoji', 'picker', 'width', 'column', 'size']">
<MkPreferenceContainer k="emojiPickerWidth">
- <MkRadios v-model="emojiPickerWidth">
+ <MkRadios
+ v-model="emojiPickerWidth"
+ :options="emojiPickerWidthDef"
+ >
<template #label><SearchLabel>{{ i18n.ts.numberOfColumn }}</SearchLabel></template>
- <option :value="1">5</option>
- <option :value="2">6</option>
- <option :value="3">7</option>
- <option :value="4">8</option>
- <option :value="5">9</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['emoji', 'picker', 'height', 'size']">
<MkPreferenceContainer k="emojiPickerHeight">
- <MkRadios v-model="emojiPickerHeight">
+ <MkRadios
+ v-model="emojiPickerHeight"
+ :options="emojiPickerHeightDef"
+ >
<template #label><SearchLabel>{{ i18n.ts.height }}</SearchLabel></template>
- <option :value="1">{{ i18n.ts.small }}</option>
- <option :value="2">{{ i18n.ts.medium }}</option>
- <option :value="3">{{ i18n.ts.large }}</option>
- <option :value="4">{{ i18n.ts.large }}+</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@@ -126,6 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref, watch } from 'vue';
import XPalette from './emoji-palette.palette.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
+import type { MkRadiosOption } from '@/components/MkRadios.vue';
import { genId } from '@/utility/id.js';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import MkRadios from '@/components/MkRadios.vue';
@@ -158,8 +154,31 @@ const emojiPaletteForMainDef = computed<MkSelectItem[]>(() => [
})),
]);
const emojiPickerScale = prefer.model('emojiPickerScale');
+const emojiPickerScaleDef = [
+ { label: i18n.ts.small, value: 1 },
+ { label: i18n.ts.medium, value: 2 },
+ { label: i18n.ts.large, value: 3 },
+ { label: i18n.ts.large + '+', value: 4 },
+ { label: i18n.ts.large + '++', value: 5 },
+] as MkRadiosOption<number>[];
+
const emojiPickerWidth = prefer.model('emojiPickerWidth');
+const emojiPickerWidthDef = [
+ { label: '5', value: 1 },
+ { label: '6', value: 2 },
+ { label: '7', value: 3 },
+ { label: '8', value: 4 },
+ { label: '9', value: 5 },
+] as MkRadiosOption<number>[];
+
const emojiPickerHeight = prefer.model('emojiPickerHeight');
+const emojiPickerHeightDef = [
+ { label: i18n.ts.small, value: 1 },
+ { label: i18n.ts.medium, value: 2 },
+ { label: i18n.ts.large, value: 3 },
+ { label: i18n.ts.large + '+', value: 4 },
+] as MkRadiosOption<number>[];
+
const emojiPickerStyle = prefer.model('emojiPickerStyle');
const palettesSyncEnabled = ref(prefer.isSyncEnabled('emojiPalettes'));
@@ -226,12 +245,12 @@ function delPalette(id: string) {
}
}
-function getHTMLElement(ev: MouseEvent): HTMLElement {
+function getHTMLElement(ev: PointerEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
-function previewPicker(ev: MouseEvent) {
+function previewPicker(ev: PointerEvent) {
emojiPicker.show(getHTMLElement(ev));
}
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 39c32d347f..abfac37275 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
<div class="_gaps_s">
<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="!storagePersisted && store.r.showStoragePersistenceSuggestion.value" class="info">
+ <MkInfo v-if="storagePersistenceSupported && !storagePersisted && store.r.showStoragePersistenceSuggestion.value" class="info">
<div>{{ i18n.ts._settings.settingsPersistence_description1 }}</div>
<div>{{ i18n.ts._settings.settingsPersistence_description2 }}</div>
<div><button class="_textButton" @click="enableStoragePersistence">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipStoragePersistence">{{ i18n.ts.skip }}</button></div>
@@ -51,10 +51,12 @@ import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utili
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
-import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
+import { enableStoragePersistence, getStoragePersistenceStatusRef, storagePersistenceSupported, skipStoragePersistence } from '@/utility/storage.js';
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
+const storagePersisted = await getStoragePersistenceStatusRef();
+
const indexInfo = {
title: i18n.ts.settings,
icon: 'ti ti-settings',
@@ -166,7 +168,7 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
type: 'button',
icon: 'ti ti-settings-2',
text: i18n.ts.preferencesProfile,
- action: async (ev: MouseEvent) => {
+ action: async (ev) => {
os.popupMenu(getPreferencesProfileMenu(), ev.currentTarget ?? ev.target);
},
}, {
diff --git a/packages/frontend/src/pages/settings/mute-block.emoji-mute.vue b/packages/frontend/src/pages/settings/mute-block.emoji-mute.vue
index ea131381a1..37cd9fa67d 100644
--- a/packages/frontend/src/pages/settings/mute-block.emoji-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.emoji-mute.vue
@@ -55,12 +55,12 @@ import {
const emojis = prefer.model('mutingEmojis');
-function getHTMLElement(ev: MouseEvent): HTMLElement {
+function getHTMLElement(ev: PointerEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
-function add(ev: MouseEvent) {
+function add(ev: PointerEvent) {
os.pickEmoji(getHTMLElement(ev), { showPinned: false }).then((emoji) => {
if (emoji) {
muteEmoji(emoji);
@@ -68,7 +68,7 @@ function add(ev: MouseEvent) {
});
}
-function onEmojiClick(ev: MouseEvent, emoji: string) {
+function onEmojiClick(ev: PointerEvent, emoji: string) {
const menuItems : MenuItem[] = [{
type: 'label',
text: emoji,
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 6fd9f07a47..433969f474 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -173,6 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch, markRaw } from 'vue';
+import * as Misskey from 'misskey-js';
import XEmojiMute from './mute-block.emoji-mute.vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
@@ -218,7 +219,7 @@ watch([
suggestReload();
});
-async function unrenoteMute(user, ev) {
+async function unrenoteMute(user: Misskey.entities.UserDetailed, ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.renoteUnmute,
icon: 'ti ti-x',
@@ -229,7 +230,7 @@ async function unrenoteMute(user, ev) {
}], ev.currentTarget ?? ev.target);
}
-async function unmute(user, ev) {
+async function unmute(user: Misskey.entities.UserDetailed, ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.unmute,
icon: 'ti ti-x',
@@ -240,7 +241,7 @@ async function unmute(user, ev) {
}], ev.currentTarget ?? ev.target);
}
-async function unblock(user, ev) {
+async function unblock(user: Misskey.entities.UserDetailed, ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.unblock,
icon: 'ti ti-x',
diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
index f5837abe98..49d8ecd92d 100644
--- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
@@ -30,7 +30,7 @@ const emit = defineEmits<{
(ev: 'save', value: (string[] | string)[]): void;
}>();
-const render = (mutedWords) => mutedWords.map(x => {
+const render = (mutedWords: (string | string[])[]) => mutedWords.map(x => {
if (Array.isArray(x)) {
return x.join(' ');
} else {
@@ -46,13 +46,13 @@ watch(mutedWords, () => {
});
async function save() {
- const parseMutes = (mutes) => {
+ const parseMutes = (mutes: string) => {
// split into lines, remove empty lines and unnecessary whitespace
- let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
+ let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '') as (string | string[])[];
// check each line if it is a RegExp or not
for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
+ const line = lines[i] as string;
const regexp = line.match(/^\/(.+)\/(.*)$/);
if (regexp) {
// check that the RegExp is valid
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index d25708dcb4..997a9f00c2 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -9,25 +9,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSlot>
<template #label>{{ i18n.ts.navbar }}</template>
<MkContainer :showHeader="false">
- <Sortable
+ <MkDraggable
v-model="items"
- itemKey="id"
- :animation="150"
- :handle="'.' + $style.itemHandle"
- @start="e => e.item.classList.add('active')"
- @end="e => e.item.classList.remove('active')"
+ direction="vertical"
>
- <template #item="{element,index}">
+ <template #default="{ item }">
<div
- v-if="element.type === '-' || navbarItemDef[element.type]"
+ v-if="item.type === '-' || navbarItemDef[item.type]"
:class="$style.item"
>
<button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button>
- <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span>
- <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button>
+ <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item.type]?.title ?? i18n.ts.divider }}</span>
+ <button class="_button" :class="$style.itemRemove" @click="removeItem(item.id)"><i class="ti ti-x"></i></button>
</div>
</template>
- </Sortable>
+ </MkDraggable>
</MkContainer>
</FormSlot>
<div class="_buttons">
@@ -36,10 +32,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
- <MkRadios v-model="menuDisplay">
+ <MkRadios
+ v-model="menuDisplay"
+ :options="[
+ { value: 'sideFull', label: i18n.ts._menuDisplay.sideFull },
+ { value: 'sideIcon', label: i18n.ts._menuDisplay.sideIcon },
+ ]"
+ >
<template #label>{{ i18n.ts.display }}</template>
- <option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
- <option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
</MkRadios>
<SearchMarker :keywords="['navbar', 'sidebar', 'toggle', 'button', 'sub']">
@@ -54,13 +54,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import MkContainer from '@/components/MkContainer.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { store } from '@/store.js';
@@ -70,15 +71,13 @@ import { prefer } from '@/preferences.js';
import { getInitialPrefValue } from '@/preferences/manager.js';
import { genId } from '@/utility/id.js';
-const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
-
const items = ref(prefer.s.menu.map(x => ({
id: genId(),
type: x,
})));
const itemTypeValues = computed(() => items.value.map(x => x.type));
-const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
+const menuDisplay = store.model('menuDisplay');
const showNavbarSubButtons = prefer.model('showNavbarSubButtons');
async function addItem() {
@@ -98,8 +97,8 @@ async function addItem() {
}];
}
-function removeItem(index: number) {
- items.value.splice(index, 1);
+function removeItem(itemId: string) {
+ items.value = items.value.filter(i => i.id !== itemId);
}
function save() {
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 2802d3263e..3787e07626 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -13,16 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection first>
<template #label>{{ i18n.ts.notificationRecieveConfig }}</template>
<div class="_gaps_s">
- <MkFolder v-for="type in notificationTypes.filter(x => !nonConfigurableNotificationTypes.includes(x))" :key="type">
+ <MkFolder v-for="type in configurableNotificationTypes" :key="type">
<template #label>{{ i18n.ts._notification._types[type] }}</template>
<template #suffix>
{{
- $i.notificationRecieveConfig[type]?.type === 'never' ? i18n.ts.none :
- $i.notificationRecieveConfig[type]?.type === 'following' ? i18n.ts.following :
- $i.notificationRecieveConfig[type]?.type === 'follower' ? i18n.ts.followers :
- $i.notificationRecieveConfig[type]?.type === 'mutualFollow' ? i18n.ts.mutualFollow :
- $i.notificationRecieveConfig[type]?.type === 'followingOrFollower' ? i18n.ts.followingOrFollower :
- $i.notificationRecieveConfig[type]?.type === 'list' ? i18n.ts.userList :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'never' ? i18n.ts.none :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'following' ? i18n.ts.following :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'follower' ? i18n.ts.followers :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'mutualFollow' ? i18n.ts.mutualFollow :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'followingOrFollower' ? i18n.ts.followingOrFollower :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'list' ? i18n.ts.userList :
i18n.ts.all
}}
</template>
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XNotificationConfig
:userLists="userLists"
:value="$i.notificationRecieveConfig[type] ?? { type: 'all' }"
- :configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined"
+ :configurableTypes="(onlyOnOrOffNotificationTypes as string[]).includes(type) ? ['all', 'never'] : undefined"
@update="(res) => updateReceiveConfig(type, res)"
/>
</MkFolder>
@@ -83,9 +83,11 @@ import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
const $i = ensureSignin();
-const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
+const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[];
-const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'] satisfies (typeof notificationTypes[number])[] as string[];
+const configurableNotificationTypes = notificationTypes.filter(type => !nonConfigurableNotificationTypes.includes(type as any)) as Exclude<typeof notificationTypes[number], typeof nonConfigurableNotificationTypes[number]>[];
+
+const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'] as const satisfies (typeof notificationTypes[number])[];
const allowButton = useTemplateRef('allowButton');
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index d4097bde94..4facc696a4 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<div v-for="policy in Object.keys($i.policies)" :key="policy">
- {{ policy }} ... {{ $i.policies[policy] }}
+ {{ policy }} ... {{ $i.policies[policy as keyof typeof $i.policies] }}
</div>
</div>
</MkFolder>
@@ -142,7 +142,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr>
</template>
- <MkButton v-if="!storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
+ <MkButton v-if="storagePersistenceSupported && !storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
@@ -165,7 +165,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os.js';
-import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
+import { enableStoragePersistence, getStoragePersistenceStatusRef, storagePersistenceSupported } from '@/utility/storage.js';
import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@@ -180,6 +180,8 @@ import { cloudBackup } from '@/preferences/utility.js';
const $i = ensureSignin();
+const storagePersisted = await getStoragePersistenceStatusRef();
+
const reportError = prefer.model('reportError');
const enableCondensedLine = prefer.model('enableCondensedLine');
const skipNoteRender = prefer.model('skipNoteRender');
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 7c6ce90e7e..89f457cf69 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>{{ i18n.ts.permission }}</template>
<template #value>
<ul style="margin-top: 0; margin-bottom: 0;">
- <li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
+ <li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] ?? permission }}</li>
<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
</ul>
</template>
@@ -96,6 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { nextTick, ref, computed } from 'vue';
+import { isSafeMode } from '@@/js/config.js';
import type { Plugin } from '@/plugin.js';
import FormLink from '@/components/form/link.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -110,7 +111,6 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin, reloadPlugin } from '@/plugin.js';
import { prefer } from '@/preferences.js';
-import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js';
const plugins = prefer.r.plugins;
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 972b50f8cd..1a613466db 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -31,12 +31,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
- <MkRadios v-model="overridedDeviceKind">
+ <MkRadios
+ v-model="overridedDeviceKind"
+ :options="[
+ { value: null, label: i18n.ts.auto },
+ { value: 'smartphone', label: i18n.ts.smartphone, icon: 'ti ti-device-mobile' },
+ { value: 'tablet', label: i18n.ts.tablet, icon: 'ti ti-device-tablet' },
+ { value: 'desktop', label: i18n.ts.desktop, icon: 'ti ti-device-desktop' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
- <option :value="null">{{ i18n.ts.auto }}</option>
- <option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
- <option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
- <option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
</MkRadios>
</SearchMarker>
@@ -121,11 +125,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
<MkPreferenceContainer k="emojiStyle">
<div>
- <MkRadios v-model="emojiStyle">
+ <MkRadios
+ v-model="emojiStyle"
+ :options="[
+ { value: 'native', label: i18n.ts.native },
+ { value: 'fluentEmoji', label: 'Fluent Emoji' },
+ { value: 'twemoji', label: 'Twemoji' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
- <option value="native">{{ i18n.ts.native }}</option>
- <option value="fluentEmoji">Fluent Emoji</option>
- <option value="twemoji">Twemoji</option>
</MkRadios>
<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
</div>
@@ -240,11 +248,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
<MkPreferenceContainer k="reactionsDisplaySize">
- <MkRadios v-model="reactionsDisplaySize">
+ <MkRadios
+ v-model="reactionsDisplaySize"
+ :options="[
+ { value: 'small', label: i18n.ts.small },
+ { value: 'medium', label: i18n.ts.medium },
+ { value: 'large', label: i18n.ts.large },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
- <option value="small">{{ i18n.ts.small }}</option>
- <option value="medium">{{ i18n.ts.medium }}</option>
- <option value="large">{{ i18n.ts.large }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@@ -259,16 +271,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
<MkPreferenceContainer k="mediaListWithOneImageAppearance">
- <MkRadios v-model="mediaListWithOneImageAppearance">
+ <MkRadios
+ v-model="mediaListWithOneImageAppearance"
+ :options="[
+ { value: 'expand', label: i18n.ts.default },
+ { value: '16_9', label: i18n.tsx.limitTo({ x: '16:9' }) },
+ { value: '1_1', label: i18n.tsx.limitTo({ x: '1:1' }) },
+ { value: '2_3', label: i18n.tsx.limitTo({ x: '2:3' }) },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
- <option value="expand">{{ i18n.ts.default }}</option>
- <option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
- <option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
- <option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
+ <SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'grid', 'wide', 'area']">
+ <MkPreferenceContainer k="showMediaListByGridInWideArea">
+ <MkSwitch v-model="showMediaListByGridInWideArea">
+ <template #label><SearchLabel>{{ i18n.ts.showMediaListByGridInWideArea }}</SearchLabel></template>
+ </MkSwitch>
+ </MkPreferenceContainer>
+ </SearchMarker>
+
<SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']">
<MkPreferenceContainer k="instanceTicker">
<MkSelect
@@ -386,22 +410,30 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['position']">
<MkPreferenceContainer k="notificationPosition">
- <MkRadios v-model="notificationPosition">
+ <MkRadios
+ v-model="notificationPosition"
+ :options="[
+ { value: 'leftTop', label: i18n.ts.leftTop, icon: 'ti ti-align-box-left-top' },
+ { value: 'rightTop', label: i18n.ts.rightTop, icon: 'ti ti-align-box-right-top' },
+ { value: 'leftBottom', label: i18n.ts.leftBottom, icon: 'ti ti-align-box-left-bottom' },
+ { value: 'rightBottom', label: i18n.ts.rightBottom, icon: 'ti ti-align-box-right-bottom' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
- <option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
- <option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
- <option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
- <option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['stack', 'axis', 'direction']">
<MkPreferenceContainer k="notificationStackAxis">
- <MkRadios v-model="notificationStackAxis">
+ <MkRadios
+ v-model="notificationStackAxis"
+ :options="[
+ { value: 'vertical', label: i18n.ts.vertical, icon: 'ti ti-carousel-vertical' },
+ { value: 'horizontal', label: i18n.ts.horizontal, icon: 'ti ti-carousel-horizontal' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
- <option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
- <option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@@ -570,12 +602,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['font', 'size']">
- <MkRadios v-model="fontSize">
+ <MkRadios
+ v-model="fontSize"
+ :options="[
+ { value: null, label: 'Aa', labelStyle: 'font-size: 14px;' },
+ { value: '1', label: 'Aa', labelStyle: 'font-size: 15px;' },
+ { value: '2', label: 'Aa', labelStyle: 'font-size: 16px;' },
+ { value: '3', label: 'Aa', labelStyle: 'font-size: 17px;' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template>
- <option :value="null"><span style="font-size: 14px;">Aa</span></option>
- <option value="1"><span style="font-size: 15px;">Aa</span></option>
- <option value="2"><span style="font-size: 16px;">Aa</span></option>
- <option value="3"><span style="font-size: 17px;">Aa</span></option>
</MkRadios>
</SearchMarker>
@@ -784,10 +820,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker>
<MkPreferenceContainer k="hemisphere">
- <MkRadios v-model="hemisphere">
+ <MkRadios
+ v-model="hemisphere"
+ :options="[
+ { value: 'N', label: i18n.ts._hemisphere.N },
+ { value: 'S', label: i18n.ts._hemisphere.S },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template>
- <option value="N">{{ i18n.ts._hemisphere.N }}</option>
- <option value="S">{{ i18n.ts._hemisphere.S }}</option>
<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
</MkRadios>
</MkPreferenceContainer>
@@ -855,7 +895,7 @@ const $i = ensureSignin();
const lang = ref(miLocalStorage.getItem('lang'));
const dataSaver = ref(prefer.s.dataSaver);
-const realtimeMode = computed(store.makeGetterSetter('realtimeMode'));
+const realtimeMode = store.model('realtimeMode');
const overridedDeviceKind = prefer.model('overridedDeviceKind');
const pollingInterval = prefer.model('pollingInterval');
@@ -890,6 +930,7 @@ const notificationStackAxis = prefer.model('notificationStackAxis');
const instanceTicker = prefer.model('instanceTicker');
const highlightSensitiveMedia = prefer.model('highlightSensitiveMedia');
const mediaListWithOneImageAppearance = prefer.model('mediaListWithOneImageAppearance');
+const showMediaListByGridInWideArea = prefer.model('showMediaListByGridInWideArea');
const reactionsDisplaySize = prefer.model('reactionsDisplaySize');
const limitWidthOfReaction = prefer.model('limitWidthOfReaction');
const squareAvatars = prefer.model('squareAvatars');
@@ -916,7 +957,7 @@ const contextMenu = prefer.model('contextMenu');
const menuStyle = prefer.model('menuStyle');
const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable');
-const fontSize = ref(miLocalStorage.getItem('fontSize'));
+const fontSize = ref(miLocalStorage.getItem('fontSize') as '1' | '2' | '3' | null);
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
watch(lang, () => {
@@ -1042,7 +1083,7 @@ function removePinnedList() {
function enableAllDataSaver() {
const g = { ...prefer.s.dataSaver };
- Object.keys(g).forEach((key) => { g[key] = true; });
+ (Object.keys(g) as (keyof typeof g)[]).forEach((key) => { g[key] = true; });
dataSaver.value = g;
}
@@ -1050,7 +1091,7 @@ function enableAllDataSaver() {
function disableAllDataSaver() {
const g = { ...prefer.s.dataSaver };
- Object.keys(g).forEach((key) => { g[key] = false; });
+ (Object.keys(g) as (keyof typeof g)[]).forEach((key) => { g[key] = false; });
dataSaver.value = g;
}
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 7d3da470d6..a7aea9bde4 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -75,30 +75,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.metadataRoot" class="_gaps_s">
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
- <Sortable
+ <MkDraggable
v-model="fields"
- class="_gaps_s"
- itemKey="id"
- :animation="150"
- :handle="'.' + $style.dragItemHandle"
- @start="e => e.item.classList.add('active')"
- @end="e => e.item.classList.remove('active')"
+ direction="vertical"
+ withGaps
+ manualDragStart
>
- <template #item="{element, index}">
+ <template #default="{ item, dragStart }">
<div v-panel :class="$style.fieldDragItem">
- <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
- <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
+ <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"></i></button>
+ <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(item.id)"><i class="ti ti-x"></i></button>
<div :class="$style.dragItemForm">
<FormSplit :minWidth="200">
- <MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
+ <MkInput v-model="item.name" small :placeholder="i18n.ts._profile.metadataLabel">
</MkInput>
- <MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
+ <MkInput v-model="item.value" small :placeholder="i18n.ts._profile.metadataContent">
</MkInput>
</FormSplit>
</div>
</div>
</template>
- </Sortable>
+ </MkDraggable>
</div>
</MkFolder>
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
@@ -165,7 +162,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
+import { computed, reactive, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -174,6 +172,7 @@ import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue';
import FormSlot from '@/components/form/slot.vue';
import FormLink from '@/components/form/link.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
import { chooseDriveFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -188,9 +187,7 @@ import { genId } from '@/utility/id.js';
const $i = ensureSignin();
-const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
-
-const reactionAcceptance = computed(store.makeGetterSetter('reactionAcceptance'));
+const reactionAcceptance = store.model('reactionAcceptance');
function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
return lang != null && lang in langmap;
@@ -228,8 +225,8 @@ while (fields.value.length < 4) {
addField();
}
-function deleteField(index: number) {
- fields.value.splice(index, 1);
+function deleteField(itemId: string) {
+ fields.value = fields.value.filter(f => f.id !== itemId);
}
function saveFields() {
@@ -270,8 +267,8 @@ function save() {
}
}
-function changeAvatar(ev) {
- async function done(driveFile) {
+function changeAvatar(ev: PointerEvent) {
+ async function done(driveFile: Misskey.entities.DriveFile) {
const i = await os.apiWithDialog('i/update', {
avatarId: driveFile.id,
});
@@ -319,8 +316,8 @@ function changeAvatar(ev) {
}], ev.currentTarget ?? ev.target);
}
-function changeBanner(ev) {
- async function done(driveFile) {
+function changeBanner(ev: PointerEvent) {
+ async function done(driveFile: Misskey.entities.DriveFile) {
const i = await os.apiWithDialog('i/update', {
bannerId: driveFile.id,
});
diff --git a/packages/frontend/src/pages/settings/profiles.vue b/packages/frontend/src/pages/settings/profiles.vue
index 4804c11f7a..b3d02ba3fe 100644
--- a/packages/frontend/src/pages/settings/profiles.vue
+++ b/packages/frontend/src/pages/settings/profiles.vue
@@ -15,21 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, computed } from 'vue';
-import type { MenuItem } from '@/types/menu.js';
+import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
-import { prefer } from '@/preferences.js';
import { deleteCloudBackup, listCloudBackups } from '@/preferences/utility.js';
const backups = await listCloudBackups();
-function del(backup) {
+function del(backup: { name: string }): void {
deleteCloudBackup(backup.name);
}
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 31fe9a64db..050586c2e1 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import type { SoundType } from '@/utility/sound.js';
+import type { SoundStore } from '@/preferences/def.js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
@@ -41,7 +42,6 @@ import { useMkSelect } from '@/composables/use-mkselect.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js';
import { selectFile } from '@/utility/drive.js';
-import type { SoundStore } from '@/preferences/def.js';
const props = defineProps<{
def: SoundStore;
@@ -100,7 +100,7 @@ const friendlyFileName = computed<string>(() => {
return i18n.ts._soundSettings.driveFileWarn;
});
-function selectSound(ev) {
+function selectSound(ev: PointerEvent) {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 1b851825d6..0d0623f11f 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -100,11 +100,14 @@ function getSoundTypeName(f: SoundType): string {
}
}
-async function updated(type: keyof typeof sounds.value, sound) {
- const v: SoundStore = {
+async function updated(type: keyof typeof sounds.value, sound: { type: SoundType; fileId?: string; fileUrl?: string; volume: number; }) {
+ const v: SoundStore = sound.type === '_driveFile_' ? {
+ type: sound.type,
+ fileId: sound.fileId!,
+ fileUrl: sound.fileUrl!,
+ volume: sound.volume,
+ } : {
type: sound.type,
- fileId: sound.fileId,
- fileUrl: sound.fileUrl,
volume: sound.volume,
};
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index b69fd2596d..83c8a7b9a7 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -17,13 +17,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>Black</template>
</MkSwitch>
- <MkRadios v-model="statusbar.size">
+ <MkRadios
+ v-model="statusbar.size"
+ :options="[
+ { value: 'verySmall', label: i18n.ts.small + '+' },
+ { value: 'small', label: i18n.ts.small },
+ { value: 'medium', label: i18n.ts.medium },
+ { value: 'large', label: i18n.ts.large },
+ { value: 'veryLarge', label: i18n.ts.large + '+' },
+ ]"
+ >
<template #label>{{ i18n.ts.size }}</template>
- <option value="verySmall">{{ i18n.ts.small }}+</option>
- <option value="small">{{ i18n.ts.small }}</option>
- <option value="medium">{{ i18n.ts.medium }}</option>
- <option value="large">{{ i18n.ts.large }}</option>
- <option value="veryLarge">{{ i18n.ts.large }}+</option>
</MkRadios>
<template v-if="statusbar.type === 'rss'">
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 0129aebe94..46b537f866 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -306,7 +306,7 @@ function changeThemesSyncEnabled(value: boolean) {
}
}
-function onThemeContextmenu(theme: Theme, ev: MouseEvent) {
+function onThemeContextmenu(theme: Theme, ev: PointerEvent) {
os.contextMenu([{
type: 'label',
text: theme.name,
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 047e68f583..1e268e64d2 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, markRaw, ref } from 'vue';
+import type { PageHeaderItem } from '@/types/page-header.js';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkButton from '@/components/MkButton.vue';
import { definePage } from '@/page.js';
@@ -50,10 +51,10 @@ async function post() {
paginator.reload();
}
-const headerActions = computed(() => [{
+const headerActions = computed<PageHeaderItem[]>(() => [{
icon: 'ti ti-dots',
text: i18n.ts.more,
- handler: (ev: MouseEvent) => {
+ handler: (ev) => {
os.popupMenu([{
text: i18n.ts.embed,
icon: 'ti ti-code',
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index af3891ac8e..2d2b8ed292 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -160,11 +160,11 @@ function setBgColor(color: typeof bgColors[number]) {
}
}
-function setAccentColor(color) {
+function setAccentColor(color: string) {
theme.value.props.accent = color;
}
-function setFgColor(color) {
+function setFgColor(color: typeof fgColors[number]) {
theme.value.props.fg = theme.value.base === 'light' ? color.forLight : color.forDark;
}
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 89d0991bc0..64c2b2eee3 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -31,6 +31,7 @@ import { computed, watch, provide, useTemplateRef, ref, onMounted, onActivated }
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import type { MenuItem } from '@/types/menu.js';
import type { BasicTimelineType } from '@/timelines.js';
+import type { PageHeaderItem } from '@/types/page-header.js';
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import * as os from '@/os.js';
@@ -105,7 +106,7 @@ const withSensitive = computed<boolean>({
const showFixedPostForm = prefer.model('showFixedPostForm');
-async function chooseList(ev: MouseEvent): Promise<void> {
+async function chooseList(ev: PointerEvent): Promise<void> {
const lists = await userListsCache.fetch();
const items: (MenuItem | undefined)[] = [
...lists.map(list => ({
@@ -124,7 +125,7 @@ async function chooseList(ev: MouseEvent): Promise<void> {
os.popupMenu(items.filter(i => i != null), ev.currentTarget ?? ev.target);
}
-async function chooseAntenna(ev: MouseEvent): Promise<void> {
+async function chooseAntenna(ev: PointerEvent): Promise<void> {
const antennas = await antennasCache.fetch();
const items: (MenuItem | undefined)[] = [
...antennas.map(antenna => ({
@@ -144,7 +145,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
os.popupMenu(items.filter(i => i != null), ev.currentTarget ?? ev.target);
}
-async function chooseChannel(ev: MouseEvent): Promise<void> {
+async function chooseChannel(ev: PointerEvent): Promise<void> {
const channels = await favoritedChannelsCache.fetch();
const items: (MenuItem | undefined)[] = [
...channels.map(channel => {
@@ -203,8 +204,8 @@ onActivated(() => {
switchTlIfNeeded();
});
-const headerActions = computed(() => {
- const items = [{
+const headerActions = computed<PageHeaderItem[]>(() => {
+ const items: PageHeaderItem[] = [{
icon: 'ti ti-dots',
text: i18n.ts.options,
handler: (ev) => {
@@ -254,7 +255,7 @@ const headerActions = computed(() => {
items.unshift({
icon: 'ti ti-refresh',
text: i18n.ts.reload,
- handler: (ev: Event) => {
+ handler: () => {
tlComponent.value?.reloadTimeline();
},
});
diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue
index ec4c854381..75519f2850 100644
--- a/packages/frontend/src/pages/user-tag.vue
+++ b/packages/frontend/src/pages/user-tag.vue
@@ -25,6 +25,7 @@ const props = defineProps<{
const paginator = markRaw(new Paginator('hashtags/users', {
limit: 30,
+ offsetMode: true,
computedParams: computed(() => ({
tag: props.tag,
origin: 'combined',
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index 4310c7ad85..f9a2eed6b9 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -57,7 +57,7 @@ async function renderChart() {
return new Date(y, m, d - ago);
};
- const format = (arr) => {
+ const format = (arr: number[]) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v,
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index 6d9c1bedd9..00bfe25430 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -57,7 +57,7 @@ async function renderChart() {
return new Date(y, m, d - ago);
};
- const format = (arr) => {
+ const format = (arr: number[]) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v,
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index 76df53becd..451f8ba0f7 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -57,7 +57,7 @@ async function renderChart() {
return new Date(y, m, d - ago);
};
- const format = (arr) => {
+ const format = (arr: number[]) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v,
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index b61c84cbbc..64b03bc4bc 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="user.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot"><i class="ti ti-robot"></i></span>
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
- <i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
+ <i class="ti ti-edit"></i> {{ i18n.ts.addMemo }}
</button>
</div>
</div>
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
- <div class="heading" v-text="i18n.ts.memo"/>
+ <div class="heading">{{ i18n.ts.memo }}</div>
<textarea
ref="memoTextareaEl"
v-model="memoDraft"
@@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@focus="isEditingMemo = true"
@blur="updateMemo"
@input="adjustMemoTextarea"
- />
+ ></textarea>
</div>
<div class="description">
<MkOmit>
@@ -186,6 +186,7 @@ import { getStaticImageUrl } from '@/utility/media-proxy.js';
import MkSparkle from '@/components/MkSparkle.vue';
import { prefer } from '@/preferences.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
+import { isBirthday } from '@/utility/is-birthday.js';
function calcAge(birthdate: string): number {
const date = new Date(birthdate);
@@ -251,7 +252,7 @@ const age = computed(() => {
return props.user.birthday ? calcAge(props.user.birthday) : NaN;
});
-function menu(ev: MouseEvent) {
+function menu(ev: PointerEvent) {
const { menu, cleanup } = getUserMenu(user.value, router);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
}
@@ -319,16 +320,10 @@ function disposeBannerParallaxResizeObserver() {
onMounted(() => {
narrow.value = rootEl.value!.clientWidth < 1000;
- if (props.user.birthday) {
- const m = new Date().getMonth() + 1;
- const d = new Date().getDate();
- const bm = parseInt(props.user.birthday.split('-')[1]);
- const bd = parseInt(props.user.birthday.split('-')[2]);
- if (m === bm && d === bd) {
- confetti({
- duration: 1000 * 4,
- });
- }
+ if (isBirthday(user.value)) {
+ confetti({
+ duration: 1000 * 4,
+ });
}
nextTick(() => {
diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue
index 210021618e..10b0582143 100644
--- a/packages/frontend/src/pages/user/index.activity.vue
+++ b/packages/frontend/src/pages/user/index.activity.vue
@@ -36,7 +36,7 @@ const props = withDefaults(defineProps<{
const chartSrc = ref<'per-user-notes' | 'per-user-pv'>('per-user-notes');
-function showMenu(ev: MouseEvent) {
+function showMenu(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.notes,
active: chartSrc.value === 'per-user-notes',
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index 58f6b0ca45..1523e99453 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkButton rounded full @click="emit('showMore')">{{ i18n.ts.showMore }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
- <p v-if="!fetching && notes.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
+ <p v-if="!fetching && notes.length == 0">{{ i18n.ts.nothing }}</p>
</div>
</MkContainer>
</template>
diff --git a/packages/frontend/src/pages/user/notes.vue b/packages/frontend/src/pages/user/notes.vue
index 1e6dba73bd..137c6cb872 100644
--- a/packages/frontend/src/pages/user/notes.vue
+++ b/packages/frontend/src/pages/user/notes.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_spacer" style="--MI_SPACER-w: 800px;">
- <div :class="$style.root">
+ <div>
<MkStickyContainer>
<template #header>
<MkTab
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 393ba98d30..3a4a558605 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Suspense>
<template #default>
- <MkServerSetupWizard :token="token" @finished="onWizardFinished"/>
+ <MkServerSetupWizard :token="token!" @finished="onWizardFinished"/>
</template>
<template #fallback>
<MkLoading/>
@@ -124,8 +124,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, ref } from 'vue';
-import * as Misskey from 'misskey-js';
+import { ref } from 'vue';
import { host, version } from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -143,7 +142,7 @@ const accountCreating = ref(false);
const accountCreated = ref(false);
const step = ref(0);
-let token;
+let token: string | null = null;
function createAccount() {
if (accountCreating.value) return;
@@ -191,6 +190,7 @@ function skipSettings() {
}
function finish() {
+ if (token == null) return;
login(token);
}
</script>