diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2024-07-30 13:11:06 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-30 13:11:06 +0900 |
| commit | 738b3ea43b059b103deca0b1a33071ae256ef79f (patch) | |
| tree | b47248996be4aa737cedcb62bdc04aa7aa63fcd2 | |
| parent | enhance: 管理画面でアーカイブにしたお知らせを表示・編... (diff) | |
| download | sharkey-738b3ea43b059b103deca0b1a33071ae256ef79f.tar.gz sharkey-738b3ea43b059b103deca0b1a33071ae256ef79f.tar.bz2 sharkey-738b3ea43b059b103deca0b1a33071ae256ef79f.zip | |
enhance(frontend): デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように (#14104)
* enhance(frontend): デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
* Update Changelog
* fix
* fix
* lint
* add story
* typo
ねぼけていた
* Update antenna-column.vue
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
22 files changed, 409 insertions, 113 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8da94f3749..f3ea5ca2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Enhance: AiScriptを0.19.0にアップデート - Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`) - Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように +- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: リバーシの対局を正しく共有できないことがある問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index d54b752312..848050583b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -633,6 +633,10 @@ export interface Locale extends ILocale { */ "editAntenna": string; /** + * アンテナを作成 + */ + "createAntenna": string; + /** * ウィジェットを選択 */ "selectWidget": string; @@ -5024,6 +5028,14 @@ export interface Locale extends ILocale { * センシティブなメディアです。表示しますか? */ "sensitiveMediaRevealConfirm": string; + /** + * 作成したリスト + */ + "createdLists": string; + /** + * 作成したアンテナ + */ + "createdAntennas": string; "_delivery": { /** * 配信状態 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d8531070fe..22228f9254 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -154,6 +154,7 @@ editList: "リストを編集" selectChannel: "チャンネルを選択" selectAntenna: "アンテナを選択" editAntenna: "アンテナを編集" +createAntenna: "アンテナを作成" selectWidget: "ウィジェットを選択" editWidgets: "ウィジェットを編集" editWidgetsExit: "編集を終了" @@ -1252,6 +1253,8 @@ inquiry: "お問い合わせ" tryAgain: "もう一度お試しください。" confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する" sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?" +createdLists: "作成したリスト" +createdAntennas: "作成したアンテナ" _delivery: status: "配信状態" diff --git a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts new file mode 100644 index 0000000000..1749e07a4e --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkAntennaEditor from './MkAntennaEditor.vue'; +export const Default = { + render(args) { + return { + components: { + MkAntennaEditor, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + created: action('created'), + updated: action('updated'), + deleted: action('deleted'), + }; + }, + }, + template: '<MkAntennaEditor v-bind="props" v-on="events" />', + }; + }, + args: { + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/antennas/create', async ({ request }) => { + action('POST /api/antennas/create')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/update', async ({ request }) => { + action('POST /api/antennas/update')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/delete', async ({ request }) => { + action('POST /api/antennas/delete')(await request.json()); + return HttpResponse.json(); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkAntennaEditor>; diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/components/MkAntennaEditor.vue index 02e8f98265..cb7ee3d6ca 100644 --- a/packages/frontend/src/pages/my-antennas/editor.vue +++ b/packages/frontend/src/components/MkAntennaEditor.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.actions"> <div class="_buttons"> <MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton v-if="initialAntenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </div> </div> @@ -61,28 +61,53 @@ import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { deepMerge } from '@/scripts/merge.js'; +import type { DeepPartial } from '@/scripts/merge.js'; + +type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & { + id?: string; + createdAt?: string; + updatedAt?: string; +}; const props = defineProps<{ - antenna: Misskey.entities.Antenna + antenna?: DeepPartial<PartialAllowedAntenna>; }>(); +const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, { + name: '', + src: 'all', + userListId: null, + users: [], + keywords: [], + excludeKeywords: [], + excludeBots: false, + withReplies: false, + caseSensitive: false, + localOnly: false, + withFile: false, + isActive: true, + hasUnreadNote: false, + notify: false, +}); + const emit = defineEmits<{ - (ev: 'created'): void, - (ev: 'updated'): void, + (ev: 'created', newAntenna: Misskey.entities.Antenna): void, + (ev: 'updated', editedAntenna: Misskey.entities.Antenna): void, (ev: 'deleted'): void, }>(); -const name = ref<string>(props.antenna.name); -const src = ref<Misskey.entities.AntennasCreateRequest['src']>(props.antenna.src); -const userListId = ref<string | null>(props.antenna.userListId); -const users = ref<string>(props.antenna.users.join('\n')); -const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('\n')); -const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); -const caseSensitive = ref<boolean>(props.antenna.caseSensitive); -const localOnly = ref<boolean>(props.antenna.localOnly); -const excludeBots = ref<boolean>(props.antenna.excludeBots); -const withReplies = ref<boolean>(props.antenna.withReplies); -const withFile = ref<boolean>(props.antenna.withFile); +const name = ref<string>(initialAntenna.name); +const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src); +const userListId = ref<string | null>(initialAntenna.userListId); +const users = ref<string>(initialAntenna.users.join('\n')); +const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n')); +const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n')); +const caseSensitive = ref<boolean>(initialAntenna.caseSensitive); +const localOnly = ref<boolean>(initialAntenna.localOnly); +const excludeBots = ref<boolean>(initialAntenna.excludeBots); +const withReplies = ref<boolean>(initialAntenna.withReplies); +const withFile = ref<boolean>(initialAntenna.withFile); const userLists = ref<Misskey.entities.UserList[] | null>(null); watch(() => src.value, async () => { @@ -106,24 +131,26 @@ async function saveAntenna() { excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')), }; - if (props.antenna.id == null) { - await os.apiWithDialog('antennas/create', antennaData); - emit('created'); + if (initialAntenna.id == null) { + const res = await os.apiWithDialog('antennas/create', antennaData); + emit('created', res); } else { - await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: props.antenna.id }); - emit('updated'); + const res = await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: initialAntenna.id }); + emit('updated', res); } } async function deleteAntenna() { + if (initialAntenna.id == null) return; + const { canceled } = await os.confirm({ type: 'warning', - text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }), + text: i18n.tsx.removeAreYouSure({ x: initialAntenna.name }), }); if (canceled) return; await misskeyApi('antennas/delete', { - antennaId: props.antenna.id, + antennaId: initialAntenna.id, }); os.success(); diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts new file mode 100644 index 0000000000..1c6ca83b47 --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkAntennaEditorDialog from './MkAntennaEditorDialog.vue'; +export const Default = { + render(args) { + return { + components: { + MkAntennaEditorDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + created: action('created'), + updated: action('updated'), + deleted: action('deleted'), + closed: action('closed'), + }; + }, + }, + template: '<MkAntennaEditorDialog v-bind="props" v-on="events" />', + }; + }, + args: { + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/antennas/create', async ({ request }) => { + action('POST /api/antennas/create')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/update', async ({ request }) => { + action('POST /api/antennas/update')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/delete', async ({ request }) => { + action('POST /api/antennas/delete')(await request.json()); + return HttpResponse.json(); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkAntennaEditorDialog>; diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.vue b/packages/frontend/src/components/MkAntennaEditorDialog.vue new file mode 100644 index 0000000000..6d815d29f3 --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditorDialog.vue @@ -0,0 +1,63 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialog" + :withOkButton="false" + :width="500" + :height="550" + @close="close()" + @closed="emit('closed')" +> + <template #header>{{ antenna == null ? i18n.ts.createAntenna : i18n.ts.editAntenna }}</template> + <XAntennaEditor + :antenna="antenna" + @created="onAntennaCreated" + @updated="onAntennaUpdated" + @deleted="onAntennaDeleted" + /> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import XAntennaEditor from '@/components/MkAntennaEditor.vue'; +import { i18n } from '@/i18n.js'; + +defineProps<{ + antenna?: Misskey.entities.Antenna; +}>(); + +const emit = defineEmits<{ + (ev: 'created', newAntenna: Misskey.entities.Antenna): void, + (ev: 'updated', editedAntenna: Misskey.entities.Antenna): void, + (ev: 'deleted'): void, + (ev: 'closed'): void, +}>(); + +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); + +function onAntennaCreated(newAntenna: Misskey.entities.Antenna) { + emit('created', newAntenna); + dialog.value?.close(); +} + +function onAntennaUpdated(editedAntenna: Misskey.entities.Antenna) { + emit('updated', editedAntenna); + dialog.value?.close(); +} + +function onAntennaDeleted() { + emit('deleted'); + dialog.value?.close(); +} + +function close() { + dialog.value?.close(); +} +</script> diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 5c3c6aa51d..16cf5b1b75 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -36,7 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkSelect v-if="select" v-model="selectedValue" autofocus> <template v-if="select.items"> - <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> + <template v-for="item in select.items"> + <optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle"> + <option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option> + </optgroup> + <option v-else :value="item.value">{{ item.text }}</option> + </template> </template> </MkSelect> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> @@ -67,11 +72,16 @@ type Input = { maxLength?: number; }; +type SelectItem = { + value: any; + text: string; +}; + type Select = { - items: { - value: any; - text: string; - }[]; + items: (SelectItem | { + sectionTitle: string; + items: SelectItem[]; + })[]; default: string | null; }; diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 3085f33e21..a8dd99c854 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -447,15 +447,20 @@ export function authenticateDialog(): Promise<{ }); } +type SelectItem<C> = { + value: C; + text: string; +}; + // default が指定されていたら result は null になり得ないことを保証する overload function export function select<C = any>(props: { title?: string; text?: string; default: string; - items: { - value: C; - text: string; - }[]; + items: (SelectItem<C> | { + sectionTitle: string; + items: SelectItem<C>[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { @@ -465,10 +470,10 @@ export function select<C = any>(props: { title?: string; text?: string; default?: string | null; - items: { - value: C; - text: string; - }[]; + items: (SelectItem<C> | { + sectionTitle: string; + items: SelectItem<C>[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { @@ -478,10 +483,10 @@ export function select<C = any>(props: { title?: string; text?: string; default?: string | null; - items: { - value: C; - text: string; - }[]; + items: (SelectItem<C> | { + sectionTitle: string; + items: SelectItem<C>[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { @@ -492,7 +497,7 @@ export function select<C = any>(props: { title: props.title, text: props.text, select: { - items: props.items, + items: props.items.filter(x => x !== undefined), default: props.default ?? null, }, }, { diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index 2d026d2fa9..2b8518747f 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -4,43 +4,33 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div> - <XAntenna :antenna="draft" @created="onAntennaCreated"/> -</div> +<MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + + <MkAntennaEditor @created="onAntennaCreated"/> +</MkStickyContainer> </template> <script lang="ts" setup> -import { ref } from 'vue'; -import XAntenna from './editor.vue'; +import { computed } from 'vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache } from '@/cache.js'; import { useRouter } from '@/router/supplier.js'; +import MkAntennaEditor from '@/components/MkAntennaEditor.vue'; const router = useRouter(); -const draft = ref({ - name: '', - src: 'all', - userListId: null, - users: [], - keywords: [], - excludeKeywords: [], - excludeBots: false, - withReplies: false, - caseSensitive: false, - localOnly: false, - withFile: false, - notify: false, -}); - function onAntennaCreated() { antennasCache.delete(); router.push('/my/antennas'); } +const headerActions = computed(() => []); +const headerTabs = computed(() => []); + definePageMetadata(() => ({ - title: i18n.ts.manageAntennas, + title: i18n.ts.createAntenna, icon: 'ti ti-antenna', })); </script> diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index 9471be8575..9f927cd1a0 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -4,15 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class=""> - <XAntenna v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/> -</div> +<MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + + <MkAntennaEditor v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/> +</MkStickyContainer> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; -import XAntenna from './editor.vue'; +import MkAntennaEditor from '@/components/MkAntennaEditor.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -36,8 +38,11 @@ misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaRespons antenna.value = antennaResponse; }); +const headerActions = computed(() => []); +const headerTabs = computed(() => []); + definePageMetadata(() => ({ - title: i18n.ts.manageAntennas, + title: i18n.ts.editAntenna, icon: 'ti ti-antenna', })); </script> diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 4e39a0fa06..9794a300da 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -6,7 +6,7 @@ import { deepClone } from './clone.js'; import type { Cloneable } from './clone.js'; -type DeepPartial<T> = { +export type DeepPartial<T> = { [P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P]; }; diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index bdb62dca15..af46b0641d 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only :ref="id" :key="id" :class="$style.column" - :column="columns.find(c => c.id === id)" + :column="columns.find(c => c.id === id)!" :isStacked="ids.length > 1" @headerWheel="onWheel" /> @@ -95,7 +95,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue'; import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; -import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; +import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; +import type { ColumnType } from './deck/deck-store.js'; import XSidebar from '@/ui/_common_/navbar.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import MkButton from '@/components/MkButton.vue'; @@ -152,10 +153,12 @@ window.addEventListener('resize', () => { const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet'; const drawerMenuShowing = ref(false); +/* const route = 'TODO'; watch(route, () => { drawerMenuShowing.value = false; }); +*/ const columns = deckStore.reactiveState.columns; const layout = deckStore.reactiveState.layout; @@ -174,32 +177,20 @@ function showSettings() { const columnsEl = shallowRef<HTMLElement>(); const addColumn = async (ev) => { - const columns = [ - 'main', - 'widgets', - 'notifications', - 'tl', - 'antenna', - 'list', - 'channel', - 'mentions', - 'direct', - 'roleTimeline', - ]; - const { canceled, result: column } = await os.select({ title: i18n.ts._deck.addColumn, - items: columns.map(column => ({ + items: columnTypes.map(column => ({ value: column, text: i18n.ts._deck._columns[column], })), }); - if (canceled) return; + if (canceled || column == null) return; addColumnToStore({ type: column, id: uuid(), name: i18n.ts._deck._columns[column], width: 330, + soundSetting: { type: null, volume: 1 }, }); }; @@ -211,7 +202,7 @@ const onContextmenu = (ev) => { }; function onWheel(ev: WheelEvent) { - if (ev.deltaX === 0) { + if (ev.deltaX === 0 && columnsEl.value != null) { columnsEl.value.scrollLeft += ev.deltaY; } } @@ -242,7 +233,7 @@ function changeProfile(ev: MouseEvent) { title: i18n.ts._deck.profile, minLength: 1, }); - if (canceled) return; + if (canceled || name == null) return; deckStore.set('profile', name); unisonReload(); diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index c3dc1e4fce..987bd4db55 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> @@ -14,7 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef, watch } from 'vue'; +import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue'; +import type { entities as MisskeyEntities } from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -22,6 +23,7 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { MenuItem } from '@/types/menu.js'; +import { antennasCache } from '@/cache.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; @@ -46,14 +48,36 @@ watch(soundSetting, v => { async function setAntenna() { const antennas = await misskeyApi('antennas/list'); - const { canceled, result: antenna } = await os.select({ + const { canceled, result: antenna } = await os.select<MisskeyEntities.Antenna | '_CREATE_'>({ title: i18n.ts.selectAntenna, - items: antennas.map(x => ({ - value: x, text: x.name, - })), + items: [ + { value: '_CREATE_', text: i18n.ts.createNew }, + (antennas.length > 0 ? { + sectionTitle: i18n.ts.createdAntennas, + items: antennas.map(x => ({ + value: x, text: x.name, + })), + } : undefined), + ], default: props.column.antennaId, }); - if (canceled) return; + if (canceled || antenna == null) return; + + if (antenna === '_CREATE_') { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAntennaEditorDialog.vue')), {}, { + created: (newAntenna: MisskeyEntities.Antenna) => { + antennasCache.delete(); + updateColumn(props.column.id, { + antennaId: newAntenna.id, + }); + }, + closed: () => { + dispose(); + }, + }); + return; + } + updateColumn(props.column.id, { antennaId: antenna.id, }); diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 7c5b13eaf1..42c07056e7 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> <i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> @@ -68,6 +68,7 @@ async function setChannel() { } async function post() { + if (props.column.channelId == null) return; if (!channel.value || channel.value.id !== props.column.channelId) { channel.value = await misskeyApi('channels/show', { channelId: props.column.channelId, diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index bb3c04cd5c..139621cf57 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -17,9 +17,24 @@ type ColumnWidget = { data: Record<string, any>; }; +export const columnTypes = [ + 'main', + 'widgets', + 'notifications', + 'tl', + 'antenna', + 'list', + 'channel', + 'mentions', + 'direct', + 'roleTimeline', +] as const; + +export type ColumnType = typeof columnTypes[number]; + export type Column = { id: string; - type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'channel' | 'list' | 'mentions' | 'direct'; + type: ColumnType; name: string | null; width: number; widgets?: ColumnWidget[]; @@ -265,7 +280,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { const columns = deepClone(deckStore.state.columns); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; + if (column == null || column.widgets == null) return; column.widgets = column.widgets.filter(w => w.id !== widget.id); columns[columnIndex] = column; deckStore.set('columns', columns); @@ -287,7 +302,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat const columns = deepClone(deckStore.state.columns); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; + if (column == null || column.widgets == null) return; column.widgets = column.widgets.map(w => w.id === widgetId ? { ...w, data: widgetData, diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue index e011de0e3b..d12a18f760 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -34,7 +34,7 @@ const tlComponent = ref<InstanceType<typeof MkNotes>>(); function reloadTimeline() { return new Promise<void>((res) => { - tlComponent.value.pagingComponent?.reload().then(() => { + tlComponent.value?.pagingComponent?.reload().then(() => { res(); }); }); diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 5369112494..9aa8f06476 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> @@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, shallowRef, ref } from 'vue'; +import type { entities as MisskeyEntities } from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -23,6 +24,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; +import { userListsCache } from '@/cache.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; @@ -51,17 +53,38 @@ watch(soundSetting, v => { async function setList() { const lists = await misskeyApi('users/lists/list'); - const { canceled, result: list } = await os.select({ + const { canceled, result: list } = await os.select<MisskeyEntities.UserList | '_CREATE_'>({ title: i18n.ts.selectList, - items: lists.map(x => ({ - value: x, text: x.name, - })), + items: [ + { value: '_CREATE_', text: i18n.ts.createNew }, + (lists.length > 0 ? { + sectionTitle: i18n.ts.createdLists, + items: lists.map(x => ({ + value: x, text: x.name, + })), + } : undefined), + ], default: props.column.listId, }); - if (canceled) return; - updateColumn(props.column.id, { - listId: list.id, - }); + if (canceled || list == null) return; + + if (list === '_CREATE_') { + const { canceled, result: name } = await os.inputText({ + title: i18n.ts.enterListName, + }); + if (canceled || name == null || name === '') return; + + const res = await os.apiWithDialog('users/lists/create', { name: name }); + userListsCache.delete(); + + updateColumn(props.column.id, { + listId: res.id, + }); + } else { + updateColumn(props.column.id, { + listId: list.id, + }); + } } function editList() { diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index 81926dd7cb..7b25a55ec3 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -26,7 +26,7 @@ const tlComponent = ref<InstanceType<typeof MkNotes>>(); function reloadTimeline() { return new Promise<void>((res) => { - tlComponent.value.pagingComponent?.reload().then(() => { + tlComponent.value?.pagingComponent?.reload().then(() => { res(); }); }); diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue index 23b0fd4f7b..19ccfc1f7c 100644 --- a/packages/frontend/src/ui/deck/notifications-column.vue +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()"> +<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="async () => { await notificationsComponent?.reload() }"> <template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template> <XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/> diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index 32ab7527b4..a375e9c574 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> @@ -53,7 +53,7 @@ async function setRole() { })), default: props.column.roleId, }); - if (canceled) return; + if (canceled || role == null) return; updateColumn(props.column.id, { roleId: role.id, }); diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index a967335edf..b4bc8bb748 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> <i v-if="column.tl === 'home'" class="ti ti-home"></i> <i v-else-if="column.tl === 'local'" class="ti ti-planet"></i> @@ -113,6 +113,7 @@ async function setType() { } return; } + if (src == null) return; updateColumn(props.column.id, { tl: src, }); |