From 5dfbce7571d0ecb70a9be6610f7906b702700d25 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 6 May 2023 08:15:17 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=81=E3=83=A3=E3=83=B3=E3=83=8D?=
 =?UTF-8?q?=E3=83=AB=E3=81=AE=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Resolve #7171
Resolve #9935
---
 CHANGELOG.md                                  |  4 +++-
 locales/ja-JP.yml                             |  4 ++++
 .../migration/1683328299359-channelArchive.js | 13 ++++++++++++
 .../src/core/entities/ChannelEntityService.ts |  1 +
 .../backend/src/models/entities/Channel.ts    |  6 ++++++
 .../backend/src/models/json-schema/channel.ts |  4 ++++
 .../server/api/endpoints/channels/featured.ts |  1 +
 .../server/api/endpoints/channels/owned.ts    |  1 +
 .../server/api/endpoints/channels/search.ts   |  3 ++-
 .../server/api/endpoints/channels/update.ts   |  2 ++
 .../src/server/api/endpoints/notes/create.ts  |  2 +-
 .../frontend/src/pages/channel-editor.vue     | 20 ++++++++++++++++++-
 packages/frontend/src/pages/channel.vue       |  3 +++
 13 files changed, 60 insertions(+), 4 deletions(-)
 create mode 100644 packages/backend/migration/1683328299359-channelArchive.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 900f0992df..b2e80ffba4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,8 @@
   * ユーザーメニューから追加できます。  
     (デスクトップ表示ではusernameの右側のボタンからも追加可能)
 - チャンネルに色を設定できるようになりました。各ノートに設定した色のインジケーターが表示されます。
+- チャンネルをアーカイブできるようになりました。
+	* アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。
 - ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。
 	* デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。
 - ロールに強制的にNSFWを付与するポリシーを追加
@@ -46,10 +48,10 @@
 - データセーバーモードを追加
   * 画像が全て隠れた状態で表示されるようになります
 - 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にするように
+- プロフィール設定「追加情報」の項目の削除と並び替えができるように
 - 新しい実績を追加
 - Fix: AiScript APIのMk:dialogで何も返していなかったのをNULLを返すように修正
 - Fix: リアクションをホバーした時のユーザーリストで猫耳が切れてしまっていた問題を修正
-- プロフィール設定「追加情報」の項目の削除と並び替えができるように
 
 ### Server
 - channel/searchのqueryが空の場合に全てのチャンネルを返すように変更
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 2376f8b06b..402eeac16e 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1031,6 +1031,10 @@ continue: "続ける"
 preservedUsernames: "予約ユーザー名"
 preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
 createNoteFromTheFile: "このファイルからノートを作成"
+archive: "アーカイブ"
+channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
+channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
+thisChannelArchived: "このチャンネルはアーカイブされています。"
 
 _serverRules:
   description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
diff --git a/packages/backend/migration/1683328299359-channelArchive.js b/packages/backend/migration/1683328299359-channelArchive.js
new file mode 100644
index 0000000000..83695ff537
--- /dev/null
+++ b/packages/backend/migration/1683328299359-channelArchive.js
@@ -0,0 +1,13 @@
+export class ChannelArchive1683328299359 {
+    name = 'ChannelArchive1683328299359'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "channel" ADD "isArchived" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`CREATE INDEX "IDX_cc7c72974f1b2f385a8921f094" ON "channel" ("isArchived") `);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "public"."IDX_cc7c72974f1b2f385a8921f094"`);
+        await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "isArchived"`);
+    }
+}
diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts
index 05eb836580..15ffd44861 100644
--- a/packages/backend/src/core/entities/ChannelEntityService.ts
+++ b/packages/backend/src/core/entities/ChannelEntityService.ts
@@ -75,6 +75,7 @@ export class ChannelEntityService {
 			bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
 			pinnedNoteIds: channel.pinnedNoteIds,
 			color: channel.color,
+			isArchived: channel.isArchived,
 			usersCount: channel.usersCount,
 			notesCount: channel.notesCount,
 
diff --git a/packages/backend/src/models/entities/Channel.ts b/packages/backend/src/models/entities/Channel.ts
index ebbfc439ad..d7c4583da3 100644
--- a/packages/backend/src/models/entities/Channel.ts
+++ b/packages/backend/src/models/entities/Channel.ts
@@ -70,6 +70,12 @@ export class Channel {
 	})
 	public color: string;
 
+	@Index()
+	@Column('boolean', {
+		default: false,
+	})
+	public isArchived: boolean;
+
 	@Index()
 	@Column('integer', {
 		default: 0,
diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts
index cb42c782b1..fd61a70c0e 100644
--- a/packages/backend/src/models/json-schema/channel.ts
+++ b/packages/backend/src/models/json-schema/channel.ts
@@ -30,6 +30,10 @@ export const packedChannelSchema = {
 			format: 'url',
 			nullable: true, optional: false,
 		},
+		isArchived: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 		notesCount: {
 			type: 'number',
 			nullable: false, optional: false,
diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts
index d25faae38d..1a8d1164c7 100644
--- a/packages/backend/src/server/api/endpoints/channels/featured.ts
+++ b/packages/backend/src/server/api/endpoints/channels/featured.ts
@@ -38,6 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		super(meta, paramDef, async (ps, me) => {
 			const query = this.channelsRepository.createQueryBuilder('channel')
 				.where('channel.lastNotedAt IS NOT NULL')
+				.andWhere('channel.isArchived = FALSE')
 				.orderBy('channel.lastNotedAt', 'DESC');
 
 			const channels = await query.take(10).getMany();
diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts
index 59df0616be..6556e6e105 100644
--- a/packages/backend/src/server/api/endpoints/channels/owned.ts
+++ b/packages/backend/src/server/api/endpoints/channels/owned.ts
@@ -45,6 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder(), ps.sinceId, ps.untilId)
+				.andWhere('channel.isArchived = FALSE')
 				.andWhere({ userId: me.id });
 
 			const channels = await query
diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts
index 900723ff8a..a3b40b0bbd 100644
--- a/packages/backend/src/server/api/endpoints/channels/search.ts
+++ b/packages/backend/src/server/api/endpoints/channels/search.ts
@@ -46,7 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private queryService: QueryService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId);
+			const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId)
+				.andWhere('channel.isArchived = FALSE');
 
 			if (ps.query !== '') {
 				if (ps.type === 'nameAndDescription') {
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index a4e38d429d..30d7f8b244 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -47,6 +47,7 @@ export const paramDef = {
 		name: { type: 'string', minLength: 1, maxLength: 128 },
 		description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
 		bannerId: { type: 'string', format: 'misskey:id', nullable: true },
+		isArchived: { type: 'boolean', nullable: true },
 		pinnedNoteIds: {
 			type: 'array',
 			items: {
@@ -106,6 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				...(ps.description !== undefined ? { description: ps.description } : {}),
 				...(ps.pinnedNoteIds !== undefined ? { pinnedNoteIds: ps.pinnedNoteIds } : {}),
 				...(ps.color !== undefined ? { color: ps.color } : {}),
+				...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
 				...(banner ? { bannerId: banner.id } : {}),
 			});
 
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index fa2dc447d8..3f7f2cdece 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -262,7 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 			let channel: Channel | null = null;
 			if (ps.channelId != null) {
-				channel = await this.channelsRepository.findOneBy({ id: ps.channelId });
+				channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false });
 
 				if (channel == null) {
 					throw new ApiError(meta.errors.noSuchChannel);
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index 488738f31c..4050c087da 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -46,8 +46,9 @@
 				</div>
 			</MkFolder>
 
-			<div>
+			<div class="_buttons">
 				<MkButton primary @click="save()"><i class="ti ti-device-floppy"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton>
+				<MkButton v-if="channelId" danger @click="archive()"><i class="ti ti-trash"></i> {{ i18n.ts.archive }}</MkButton>
 			</div>
 		</div>
 	</MkSpacer>
@@ -151,6 +152,23 @@ function save() {
 	}
 }
 
+async function archive() {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		title: i18n.t('channelArchiveConfirmTitle', { name: name }),
+		text: i18n.ts.channelArchiveConfirmDescription,
+	});
+
+	if (canceled) return;
+	
+	os.api('channels/update', {
+		channelId: props.channelId,
+		isArchived: true,
+	}).then(() => {
+		os.success();
+	});
+}
+
 function setBannerImage(evt) {
 	selectFile(evt.currentTarget ?? evt.target, null).then(file => {
 		bannerId = file.id;
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 30e18c32ba..0a2f66d4fc 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -28,6 +28,8 @@
 			</MkFoldableSection>
 		</div>
 		<div v-if="channel && tab === 'timeline'" class="_gaps">
+			<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
+
 			<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
 			<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
 
@@ -77,6 +79,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import { defaultStore } from '@/store';
 import MkNote from '@/components/MkNote.vue';
+import MkInfo from '@/components/MkInfo.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 
 const router = useRouter();