summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2024-07-30 13:11:06 +0900
committerGitHub <noreply@github.com>2024-07-30 13:11:06 +0900
commit738b3ea43b059b103deca0b1a33071ae256ef79f (patch)
treeb47248996be4aa737cedcb62bdc04aa7aa63fcd2
parentenhance: 管理画面でアーカイブにしたお知らせを表示・編... (diff)
downloadsharkey-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>
-rw-r--r--CHANGELOG.md1
-rw-r--r--locales/index.d.ts12
-rw-r--r--locales/ja-JP.yml3
-rw-r--r--packages/frontend/src/components/MkAntennaEditor.stories.impl.ts62
-rw-r--r--packages/frontend/src/components/MkAntennaEditor.vue (renamed from packages/frontend/src/pages/my-antennas/editor.vue)71
-rw-r--r--packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts63
-rw-r--r--packages/frontend/src/components/MkAntennaEditorDialog.vue63
-rw-r--r--packages/frontend/src/components/MkDialog.vue20
-rw-r--r--packages/frontend/src/os.ts31
-rw-r--r--packages/frontend/src/pages/my-antennas/create.vue32
-rw-r--r--packages/frontend/src/pages/my-antennas/edit.vue17
-rw-r--r--packages/frontend/src/scripts/merge.ts2
-rw-r--r--packages/frontend/src/ui/deck.vue29
-rw-r--r--packages/frontend/src/ui/deck/antenna-column.vue38
-rw-r--r--packages/frontend/src/ui/deck/channel-column.vue3
-rw-r--r--packages/frontend/src/ui/deck/deck-store.ts21
-rw-r--r--packages/frontend/src/ui/deck/direct-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/list-column.vue41
-rw-r--r--packages/frontend/src/ui/deck/mentions-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/notifications-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/role-timeline-column.vue4
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue3
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,
});