diff --git a/CHANGELOG.md b/CHANGELOG.md index dbf26b115b..5ff6a47184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - ### Client +- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように +- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992` - Enhance: 投稿フォームの絵文字ピッカーに独立したウィンドウを使用できるように ### Server diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc index 845190b5f4..f4bf7a4d2a 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -1,5 +1,5 @@ { - "$schema": "https://json.schemastore.org/swcrc", + "$schema": "https://swc.rs/schema.json", "jsc": { "parser": { "syntax": "typescript", diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index aa986d82b0..3a68560266 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -722,6 +722,14 @@ function deleteDraft() { miLocalStorage.setItem('drafts', JSON.stringify(draftData)); } +function isAnnoying(text: string): boolean { + return text.includes('$[x2') || + text.includes('$[x3') || + text.includes('$[x4') || + text.includes('$[scale') || + text.includes('$[position'); +} + async function post(ev?: MouseEvent) { if (useCw.value && (cw.value == null || cw.value.trim() === '')) { os.alert({ @@ -746,14 +754,10 @@ async function post(ev?: MouseEvent) { if (props.mock) return; - const annoying = - text.value.includes('$[x2') || - text.value.includes('$[x3') || - text.value.includes('$[x4') || - text.value.includes('$[scale') || - text.value.includes('$[position'); - - if (annoying && visibility.value === 'public') { + if (visibility.value === 'public' && ( + (useCw.value && cw.value != null && cw.value.trim() !== '' && isAnnoying(cw.value)) || // CWが迷惑になる場合 + ((!useCw.value || cw.value == null || cw.value.trim() === '') && text.value != null && text.value.trim() !== '' && isAnnoying(text.value)) // CWが無い かつ 本文が迷惑になる場合 + )) { const { canceled, result } = await os.actions({ type: 'warning', text: i18n.ts.thisPostMayBeAnnoying, diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 69cc62ad6e..6e48366092 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -310,6 +310,21 @@ export function inputText(props: { } | { canceled: false; result: string; }>; +// min lengthが指定されてたら result は null になり得ないことを保証する overload function +export function inputText(props: { + type?: 'text' | 'email' | 'password' | 'url'; + title?: string; + text?: string; + placeholder?: string | null; + autocomplete?: string; + default?: string; + minLength: number; + maxLength?: number; +}): Promise<{ + canceled: true; result: undefined; +} | { + canceled: false; result: string; +}>; export function inputText(props: { type?: 'text' | 'email' | 'password' | 'url'; title?: string; diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 71a18fbc66..8e99c457ad 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -188,7 +188,7 @@ const addColumn = async (ev) => { addColumnToStore({ type: column, id: uuid(), - name: i18n.ts._deck._columns[column], + name: null, width: 330, soundSetting: { type: null, volume: 1 }, }); diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index 07ae17b982..b79cd8408b 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <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> + <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name || antennaName || i18n.ts._deck._columns.antenna }}</span> </template> <MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @note="onNote"/> @@ -36,6 +36,7 @@ const props = defineProps<{ const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); +const antennaName = ref<string | null>(null); onMounted(() => { if (props.column.antennaId == null) { @@ -43,6 +44,13 @@ onMounted(() => { } }); +watch([() => props.column.name, () => props.column.antennaId], () => { + if (!props.column.name && props.column.antennaId) { + misskeyApi('antennas/show', { antennaId: props.column.antennaId }) + .then(value => antennaName.value = value.name); + } +}); + watch(soundSetting, v => { updateColumn(props.column.id, { soundSetting: v }); }); diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index d656eec60a..9e07c06639 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <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> + <i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name || channel?.name || i18n.ts._deck._columns.channel }}</span> </template> <template v-if="column.channelId"> @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, watch } from 'vue'; +import { onMounted, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn } from './deck-store.js'; @@ -44,9 +44,18 @@ const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const channel = shallowRef<Misskey.entities.Channel>(); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); -if (props.column.channelId == null) { - setChannel(); -} +onMounted(() => { + if (props.column.channelId == null) { + setChannel(); + } +}); + +watch([() => props.column.name, () => props.column.channelId], () => { + if (!props.column.name && props.column.channelId) { + misskeyApi('channels/show', { channelId: props.column.channelId }) + .then(value => channel.value = value); + } +}); watch(soundSetting, v => { updateColumn(props.column.id, { soundSetting: v }); diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 34d7b2d4b5..f23e33c748 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -129,7 +129,8 @@ function getMenu() { icon: 'ti ti-settings', text: i18n.ts._deck.configureColumn, action: async () => { - const { canceled, result } = await os.form(props.column.name, { + const name = props.column.name ?? i18n.ts._deck._columns[props.column.type]; + const { canceled, result } = await os.form(name, { name: { type: 'string', label: i18n.ts.name, @@ -144,7 +145,7 @@ function getMenu() { flexible: { type: 'boolean', label: i18n.ts._deck.flexible, - default: props.column.flexible, + default: props.column.flexible ?? null, }, }); if (canceled) return; diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 002d409f78..9055ea6d43 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -51,7 +51,7 @@ export type Column = { withReplies?: boolean; withSensitive?: boolean; onlyFiles?: boolean; - soundSetting: SoundStore; + soundSetting?: SoundStore; }; export const deckStore = markRaw(new Storage('deck', { @@ -94,7 +94,7 @@ export const loadDeck = async () => { key: deckStore.state.profile, }); } catch (err) { - if (err.code === 'NO_SUCH_KEY') { + if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') { // 後方互換性のため if (deckStore.state.profile === 'default') { saveDeck(); @@ -180,6 +180,7 @@ export function swapLeftColumn(id: Column['id']) { } return true; } + return false; }); saveDeck(); } @@ -196,6 +197,7 @@ export function swapRightColumn(id: Column['id']) { } return true; } + return false; }); saveDeck(); } @@ -216,6 +218,7 @@ export function swapUpColumn(id: Column['id']) { } return true; } + return false; }); saveDeck(); } @@ -236,6 +239,7 @@ export function swapDownColumn(id: Column['id']) { } return true; } + return false; }); saveDeck(); } @@ -286,7 +290,8 @@ 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 || column.widgets == null) return; + if (column == null) return; + if (column.widgets == null) column.widgets = []; column.widgets = column.widgets.filter(w => w.id !== widget.id); columns[columnIndex] = column; deckStore.set('columns', columns); @@ -308,7 +313,8 @@ 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 || column.widgets == null) return; + if (column == null) return; + if (column.widgets == null) column.widgets = []; 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 4337397c2a..2cecd6c669 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> - <template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.direct }}</template> <MkNotes ref="tlComponent" :pagination="pagination"/> </XColumn> @@ -16,6 +16,7 @@ import { ref } from 'vue'; import XColumn from './column.vue'; import type { Column } from './deck-store.js'; import MkNotes from '@/components/MkNotes.vue'; +import { i18n } from '@/i18n.js'; defineProps<{ column: Column; diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index a22a8743b4..83961d02bc 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <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> + <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ (column.name || listName) ?? i18n.ts._deck._columns.list }}</span> </template> <MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes" @note="onNote"/> @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch, shallowRef, ref } from 'vue'; +import { watch, shallowRef, ref, onMounted } from 'vue'; import type { entities as MisskeyEntities } from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn } from './deck-store.js'; @@ -37,10 +37,20 @@ const props = defineProps<{ const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const withRenotes = ref(props.column.withRenotes ?? true); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); +const listName = ref<string | null>(null); -if (props.column.listId == null) { - setList(); -} +onMounted(() => { + if (props.column.listId == null) { + setList(); + } +}); + +watch([() => props.column.name, () => props.column.listId], () => { + if (!props.column.name && props.column.listId) { + misskeyApi('users/lists/show', { listId: props.column.listId }) + .then(value => listName.value = value.name); + } +}); watch(withRenotes, v => { updateColumn(props.column.id, { diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index fde02eaf5b..233fba554b 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> - <template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.mentions }}</template> <MkNotes ref="tlComponent" :pagination="pagination"/> </XColumn> @@ -16,6 +16,7 @@ import { ref } from 'vue'; import XColumn from './column.vue'; import type { Column } from './deck-store.js'; import MkNotes from '@/components/MkNotes.vue'; +import { i18n } from '../../i18n.js'; defineProps<{ column: Column; diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue index fdf80ea987..c0303e86dc 100644 --- a/packages/frontend/src/ui/deck/notifications-column.vue +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <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> + <template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.notifications }}</template> <XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/> </XColumn> diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index 8fb0729bf2..5b1420570d 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <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> + <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name || roleName || i18n.ts._deck._columns.roleTimeline }}</span> </template> <MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @note="onNote"/> @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef, watch } from 'vue'; +import { computed, onMounted, ref, shallowRef, watch } from 'vue'; import XColumn from './column.vue'; import { updateColumn } from './deck-store.js'; import type { Column } from './deck-store.js'; @@ -34,6 +34,7 @@ const props = defineProps<{ const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); +const roleName = ref<string | null>(null); onMounted(() => { if (props.column.roleId == null) { @@ -41,6 +42,13 @@ onMounted(() => { } }); +watch([() => props.column.name, () => props.column.roleId], () => { + if (!props.column.name && props.column.roleId) { + misskeyApi('roles/show', { roleId: props.column.roleId }) + .then(value => roleName.value = value.name); + } +}); + watch(soundSetting, v => { updateColumn(props.column.id, { soundSetting: v }); }); diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 99f6482457..b9b3746abf 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> <i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"/> - <span style="margin-left: 8px;">{{ column.name }}</span> + <span style="margin-left: 8px;">{{ column.name || (column.tl ? i18n.ts._timelines[column.tl] : null) || i18n.ts._deck._columns.tl }}</span> </template> <div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled"> diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue index a4d72b9255..20284d8c9f 100644 --- a/packages/frontend/src/ui/deck/widgets-column.vue +++ b/packages/frontend/src/ui/deck/widgets-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :menu="menu" :naked="true" :column="column" :isStacked="isStacked"> - <template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns[props.column.type] }}</template> <div :class="$style.root"> <div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>