feat: sensitive channel (#11438)

* feat(backend): add isSensitive to Channel

* feat(backend): support isSensitive in channel endpoints

* feat(frontend/channel-editor): support isSensitive in create/edit channel page

* feat(frontend/channel): show sensitive indicator for sensitive channels

* docs(changelog): add チャンネルをセンシティブ指定できるようになりました

* chore: license header for each file

* chore: add isSensitive of channel to Note object
This commit is contained in:
anatawa12 2023-08-05 13:58:31 +09:00 committed by GitHub
parent 79966d33b5
commit c5b8766a18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 73 additions and 0 deletions

View file

@ -51,6 +51,7 @@
- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました - ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました - identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました - サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
- チャンネルをセンシティブ指定できるようになりました
### Client ### Client
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように - deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SensitiveChannel1690782653311 {
name = 'SensitiveChannel1690782653311'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel"
ADD "isSensitive" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "isSensitive"`);
}
}

View file

@ -92,6 +92,7 @@ export class ChannelEntityService {
isArchived: channel.isArchived, isArchived: channel.isArchived,
usersCount: channel.usersCount, usersCount: channel.usersCount,
notesCount: channel.notesCount, notesCount: channel.notesCount,
isSensitive: channel.isSensitive,
...(me ? { ...(me ? {
isFollowing, isFollowing,

View file

@ -333,6 +333,7 @@ export class NoteEntityService implements OnModuleInit {
id: channel.id, id: channel.id,
name: channel.name, name: channel.name,
color: channel.color, color: channel.color,
isSensitive: channel.isSensitive,
} : undefined, } : undefined,
mentions: note.mentions.length > 0 ? note.mentions : undefined, mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri ?? undefined, uri: note.uri ?? undefined,

View file

@ -94,4 +94,9 @@ export class Channel {
comment: 'The count of users.', comment: 'The count of users.',
}) })
public usersCount: number; public usersCount: number;
@Column('boolean', {
default: false,
})
public isSensitive: boolean;
} }

View file

@ -72,5 +72,9 @@ export const packedChannelSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
isSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
} as const; } as const;

View file

@ -139,6 +139,10 @@ export const packedNoteSchema = {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
}, },
isSensitive: {
type: 'boolean',
optional: true, nullable: false,
}
}, },
}, },
}, },

View file

@ -49,6 +49,7 @@ export const paramDef = {
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true }, bannerId: { type: 'string', format: 'misskey:id', nullable: true },
color: { type: 'string', minLength: 1, maxLength: 16 }, color: { type: 'string', minLength: 1, maxLength: 16 },
isSensitive: { type: 'boolean', nullable: true },
}, },
required: ['name'], required: ['name'],
} as const; } as const;
@ -86,6 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name, name: ps.name,
description: ps.description ?? null, description: ps.description ?? null,
bannerId: banner ? banner.id : null, bannerId: banner ? banner.id : null,
isSensitive: ps.isSensitive ?? false,
...(ps.color !== undefined ? { color: ps.color } : {}), ...(ps.color !== undefined ? { color: ps.color } : {}),
} as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); } as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));

View file

@ -60,6 +60,7 @@ export const paramDef = {
}, },
}, },
color: { type: 'string', minLength: 1, maxLength: 16 }, color: { type: 'string', minLength: 1, maxLength: 16 },
isSensitive: { type: 'boolean', nullable: true },
}, },
required: ['channelId'], required: ['channelId'],
} as const; } as const;
@ -114,6 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
...(ps.color !== undefined ? { color: ps.color } : {}), ...(ps.color !== undefined ? { color: ps.color } : {}),
...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}), ...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
...(banner ? { bannerId: banner.id } : {}), ...(banner ? { bannerId: banner.id } : {}),
...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}),
}); });
return await this.channelEntityService.pack(channel.id, me); return await this.channelEntityService.pack(channel.id, me);

View file

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="banner" :style="bannerStyle"> <div class="banner" :style="bannerStyle">
<div class="fade"></div> <div class="fade"></div>
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div> <div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
<div class="status"> <div class="status">
<div> <div>
<i class="ti ti-users ti-fw"></i> <i class="ti ti-users ti-fw"></i>
@ -102,6 +103,19 @@ const bannerStyle = computed(() => {
border-radius: 6px; border-radius: 6px;
color: #fff; color: #fff;
} }
> .sensitiveIndicator {
position: absolute;
z-index: 1;
bottom: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.7);
color: var(--warn);
border-radius: 6px;
font-weight: bold;
font-size: 1em;
padding: 4px 7px;
}
} }
> article { > article {

View file

@ -20,6 +20,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.color }}</template> <template #label>{{ i18n.ts.color }}</template>
</MkColorInput> </MkColorInput>
<MkSwitch v-model="isSensitive">
<template #label>{{ i18n.ts.sensitive }}</template>
</MkSwitch>
<div> <div>
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton> <MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
<div v-else-if="bannerUrl"> <div v-else-if="bannerUrl">
@ -72,6 +76,7 @@ import { useRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from "@/components/MkSwitch.vue";
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@ -87,6 +92,7 @@ let description = $ref(null);
let bannerUrl = $ref<string | null>(null); let bannerUrl = $ref<string | null>(null);
let bannerId = $ref<string | null>(null); let bannerId = $ref<string | null>(null);
let color = $ref('#000'); let color = $ref('#000');
let isSensitive = $ref(false);
const pinnedNotes = ref([]); const pinnedNotes = ref([]);
watch(() => bannerId, async () => { watch(() => bannerId, async () => {
@ -110,6 +116,7 @@ async function fetchChannel() {
description = channel.description; description = channel.description;
bannerId = channel.bannerId; bannerId = channel.bannerId;
bannerUrl = channel.bannerUrl; bannerUrl = channel.bannerUrl;
isSensitive = channel.isSensitive;
pinnedNotes.value = channel.pinnedNoteIds.map(id => ({ pinnedNotes.value = channel.pinnedNoteIds.map(id => ({
id, id,
})); }));
@ -142,6 +149,7 @@ function save() {
bannerId: bannerId, bannerId: bannerId,
pinnedNoteIds: pinnedNotes.value.map(x => x.id), pinnedNoteIds: pinnedNotes.value.map(x => x.id),
color: color, color: color,
isSensitive: isSensitive,
}; };
if (props.channelId) { if (props.channelId) {

View file

@ -17,6 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> <div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> <div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
</div> </div>
<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
<div :class="$style.bannerFade"></div> <div :class="$style.bannerFade"></div>
</div> </div>
<div v-if="channel.description" :class="$style.description"> <div v-if="channel.description" :class="$style.description">
@ -274,4 +275,17 @@ definePageMetadata(computed(() => channel ? {
.description { .description {
padding: 16px; padding: 16px;
} }
.sensitiveIndicator {
position: absolute;
z-index: 1;
bottom: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.7);
color: var(--warn);
border-radius: 6px;
font-weight: bold;
font-size: 1em;
padding: 4px 7px;
}
</style> </style>