From c5b8766a18d2af25c68e153749dd476a2fd2f869 Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Sat, 5 Aug 2023 13:58:31 +0900
Subject: [PATCH] feat: sensitive channel (#11438)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 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
---
 CHANGELOG.md                                    |  1 +
 .../migration/1690782653311-SensitiveChannel.js | 17 +++++++++++++++++
 .../src/core/entities/ChannelEntityService.ts   |  1 +
 .../src/core/entities/NoteEntityService.ts      |  1 +
 packages/backend/src/models/entities/Channel.ts |  5 +++++
 .../backend/src/models/json-schema/channel.ts   |  4 ++++
 packages/backend/src/models/json-schema/note.ts |  4 ++++
 .../src/server/api/endpoints/channels/create.ts |  2 ++
 .../src/server/api/endpoints/channels/update.ts |  2 ++
 .../src/components/MkChannelPreview.vue         | 14 ++++++++++++++
 packages/frontend/src/pages/channel-editor.vue  |  8 ++++++++
 packages/frontend/src/pages/channel.vue         | 14 ++++++++++++++
 12 files changed, 73 insertions(+)
 create mode 100644 packages/backend/migration/1690782653311-SensitiveChannel.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10ddc33d9..4fa6c98f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,6 +51,7 @@
 - ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
 - identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
 - サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
+- チャンネルをセンシティブ指定できるようになりました
 
 ### Client
 - deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
diff --git a/packages/backend/migration/1690782653311-SensitiveChannel.js b/packages/backend/migration/1690782653311-SensitiveChannel.js
new file mode 100644
index 000000000..e76dda518
--- /dev/null
+++ b/packages/backend/migration/1690782653311-SensitiveChannel.js
@@ -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"`);
+	}
+}
diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts
index f62daa21c..042e7c300 100644
--- a/packages/backend/src/core/entities/ChannelEntityService.ts
+++ b/packages/backend/src/core/entities/ChannelEntityService.ts
@@ -92,6 +92,7 @@ export class ChannelEntityService {
 			isArchived: channel.isArchived,
 			usersCount: channel.usersCount,
 			notesCount: channel.notesCount,
+			isSensitive: channel.isSensitive,
 
 			...(me ? {
 				isFollowing,
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index da1d0a952..7d7183dc8 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -333,6 +333,7 @@ export class NoteEntityService implements OnModuleInit {
 				id: channel.id,
 				name: channel.name,
 				color: channel.color,
+				isSensitive: channel.isSensitive,
 			} : undefined,
 			mentions: note.mentions.length > 0 ? note.mentions : undefined,
 			uri: note.uri ?? undefined,
diff --git a/packages/backend/src/models/entities/Channel.ts b/packages/backend/src/models/entities/Channel.ts
index e04bb5e62..4df8b5aed 100644
--- a/packages/backend/src/models/entities/Channel.ts
+++ b/packages/backend/src/models/entities/Channel.ts
@@ -94,4 +94,9 @@ export class Channel {
 		comment: 'The count of users.',
 	})
 	public usersCount: number;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isSensitive: boolean;
 }
diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts
index ec8bc3237..f1019d146 100644
--- a/packages/backend/src/models/json-schema/channel.ts
+++ b/packages/backend/src/models/json-schema/channel.ts
@@ -72,5 +72,9 @@ export const packedChannelSchema = {
 			type: 'string',
 			optional: false, nullable: false,
 		},
+		isSensitive: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 	},
 } as const;
diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts
index dc7c0462f..eb744aa10 100644
--- a/packages/backend/src/models/json-schema/note.ts
+++ b/packages/backend/src/models/json-schema/note.ts
@@ -139,6 +139,10 @@ export const packedNoteSchema = {
 						type: 'string',
 						optional: false, nullable: true,
 					},
+					isSensitive: {
+						type: 'boolean',
+						optional: true, nullable: false,
+					}
 				},
 			},
 		},
diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts
index a63f99f82..8364fd65d 100644
--- a/packages/backend/src/server/api/endpoints/channels/create.ts
+++ b/packages/backend/src/server/api/endpoints/channels/create.ts
@@ -49,6 +49,7 @@ export const paramDef = {
 		description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
 		bannerId: { type: 'string', format: 'misskey:id', nullable: true },
 		color: { type: 'string', minLength: 1, maxLength: 16 },
+		isSensitive: { type: 'boolean', nullable: true },
 	},
 	required: ['name'],
 } as const;
@@ -86,6 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				name: ps.name,
 				description: ps.description ?? null,
 				bannerId: banner ? banner.id : null,
+				isSensitive: ps.isSensitive ?? false,
 				...(ps.color !== undefined ? { color: ps.color } : {}),
 			} as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
 
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index 701b73148..528e5cb38 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -60,6 +60,7 @@ export const paramDef = {
 			},
 		},
 		color: { type: 'string', minLength: 1, maxLength: 16 },
+		isSensitive: { type: 'boolean', nullable: true },
 	},
 	required: ['channelId'],
 } as const;
@@ -114,6 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				...(ps.color !== undefined ? { color: ps.color } : {}),
 				...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
 				...(banner ? { bannerId: banner.id } : {}),
+				...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}),
 			});
 
 			return await this.channelEntityService.pack(channel.id, me);
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 9c08efb6c..2583ee383 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="banner" :style="bannerStyle">
 		<div class="fade"></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>
 				<i class="ti ti-users ti-fw"></i>
@@ -102,6 +103,19 @@ const bannerStyle = computed(() => {
 			border-radius: 6px;
 			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 {
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index fbf589f24..59d6d14ce 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -20,6 +20,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #label>{{ i18n.ts.color }}</template>
 			</MkColorInput>
 
+			<MkSwitch v-model="isSensitive">
+				<template #label>{{ i18n.ts.sensitive }}</template>
+			</MkSwitch>
+
 			<div>
 				<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
 				<div v-else-if="bannerUrl">
@@ -72,6 +76,7 @@ import { useRouter } from '@/router';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { i18n } from '@/i18n';
 import MkFolder from '@/components/MkFolder.vue';
+import MkSwitch from "@/components/MkSwitch.vue";
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
@@ -87,6 +92,7 @@ let description = $ref(null);
 let bannerUrl = $ref<string | null>(null);
 let bannerId = $ref<string | null>(null);
 let color = $ref('#000');
+let isSensitive = $ref(false);
 const pinnedNotes = ref([]);
 
 watch(() => bannerId, async () => {
@@ -110,6 +116,7 @@ async function fetchChannel() {
 	description = channel.description;
 	bannerId = channel.bannerId;
 	bannerUrl = channel.bannerUrl;
+	isSensitive = channel.isSensitive;
 	pinnedNotes.value = channel.pinnedNoteIds.map(id => ({
 		id,
 	}));
@@ -142,6 +149,7 @@ function save() {
 		bannerId: bannerId,
 		pinnedNoteIds: pinnedNotes.value.map(x => x.id),
 		color: color,
+		isSensitive: isSensitive,
 	};
 
 	if (props.channelId) {
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 5049b6ba2..be0917871 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -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-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 v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
 					<div :class="$style.bannerFade"></div>
 				</div>
 				<div v-if="channel.description" :class="$style.description">
@@ -274,4 +275,17 @@ definePageMetadata(computed(() => channel ? {
 .description {
 	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>