diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2026-03-05 10:56:50 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-05 10:56:50 +0000 |
| commit | fe3dd8edb5f30104cd0a7ed755eb254feda2922d (patch) | |
| tree | af6cf5fa4ca75302ac2de5db742cead00bc13d21 /packages/frontend/src/pages | |
| parent | Merge pull request #16998 from misskey-dev/develop (diff) | |
| parent | Release: 2026.3.0 (diff) | |
| download | misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.gz misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.bz2 misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.zip | |
Merge pull request #17217 from misskey-dev/develop
Release: 2026.3.0
Diffstat (limited to 'packages/frontend/src/pages')
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> |