From eaa6bc2da37e34ec924cc2684e7a427b7ab5765b Mon Sep 17 00:00:00 2001
From: Kaity A <kaity@theallans.com.au>
Date: Sun, 2 Apr 2023 21:24:12 +1000
Subject: [PATCH] Separate cat-avatar mode and speak-as-cat nyanification

---
 locales/en-US.yml                             |  2 ++
 .../migration/1680426269172-SpeakAsCat.js     | 20 +++++++++++++++++++
 packages/backend/src/models/entities/user.ts  |  6 ++++++
 .../backend/src/models/repositories/note.ts   |  2 +-
 .../backend/src/models/repositories/user.ts   |  1 +
 packages/backend/src/models/schema/user.ts    |  5 +++++
 .../src/server/api/endpoints/i/update.ts      |  2 ++
 .../client/src/pages/settings/profile.vue     |  3 +++
 8 files changed, 40 insertions(+), 1 deletion(-)
 create mode 100644 packages/backend/migration/1680426269172-SpeakAsCat.js

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 880203922..3649e9701 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -146,6 +146,8 @@ flagAsBot: "Mark this account as a bot"
 flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Calckey's internal systems to treat this account as a bot."
 flagAsCat: "Are you a cat? 😺"
 flagAsCatDescription: "You'll get cat ears and speak like a cat!"
+flagSpeakAsCat: "Speak as a cat"
+flagSpeakAsCatDescription: "Your posts will get nyanified when in cat mode"
 flagShowTimelineReplies: "Show replies in timeline"
 flagShowTimelineRepliesDescription: "Shows replies of users to posts of other users in the timeline if turned on."
 autoAcceptFollowed: "Automatically approve follow requests from users you're following"
diff --git a/packages/backend/migration/1680426269172-SpeakAsCat.js b/packages/backend/migration/1680426269172-SpeakAsCat.js
new file mode 100644
index 000000000..3655e2726
--- /dev/null
+++ b/packages/backend/migration/1680426269172-SpeakAsCat.js
@@ -0,0 +1,20 @@
+export class SpeakAsCat1680426269172 {
+    name = 'SpeakAsCat1680426269172'
+
+    async up(queryRunner) {
+        await queryRunner.query(`
+            ALTER TABLE "user"
+            ADD "speakAsCat" boolean NOT NULL DEFAULT true
+        `);
+        await queryRunner.query(`
+            COMMENT ON COLUMN "user"."speakAsCat"
+						IS 'Whether to speak as a cat if isCat.'
+        `);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`
+            ALTER TABLE "user" DROP COLUMN "speakAsCat"
+        `);
+    }
+}
diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts
index c57ad916c..c23f4f28d 100644
--- a/packages/backend/src/models/entities/user.ts
+++ b/packages/backend/src/models/entities/user.ts
@@ -156,6 +156,12 @@ export class User {
 	})
 	public isCat: boolean;
 
+	@Column('boolean', {
+		default: true,
+		comment: 'Whether to speak as a cat if isCat.',
+	})
+	public speakAsCat: boolean;
+
 	@Column('boolean', {
 		default: false,
 		comment: 'Whether the User is the admin.',
diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index b99ce1635..5e56a817b 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -263,7 +263,7 @@ export const NoteRepository = db.getRepository(Note).extend({
 				: {}),
 		});
 
-		if (packed.user.isCat && packed.text) {
+		if (packed.user.isCat && packed.user.speakAsCat && packed.text) {
 			const tokens = packed.text ? mfm.parse(packed.text) : [];
 			function nyaizeNode(node: mfm.MfmNode) {
 				if (node.type === "quote") return;
diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts
index 0bf31b1b3..27b0c78d6 100644
--- a/packages/backend/src/models/repositories/user.ts
+++ b/packages/backend/src/models/repositories/user.ts
@@ -438,6 +438,7 @@ export const UserRepository = db.getRepository(User).extend({
 			isModerator: user.isModerator || falsy,
 			isBot: user.isBot || falsy,
 			isCat: user.isCat || falsy,
+			speakAsCat: user.speakAsCat || falsy,
 			instance: user.host
 				? userInstanceCache
 						.fetch(
diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts
index 9fb5aa8f3..7f7689165 100644
--- a/packages/backend/src/models/schema/user.ts
+++ b/packages/backend/src/models/schema/user.ts
@@ -66,6 +66,11 @@ export const packedUserLiteSchema = {
 			nullable: false,
 			optional: true,
 		},
+		speakAsCat: {
+			type: "boolean",
+			nullable: false,
+			optional: true,
+		},
 		emojis: {
 			type: "array",
 			nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 48868de37..56ed64296 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -104,6 +104,7 @@ export const paramDef = {
 		noCrawle: { type: "boolean" },
 		isBot: { type: "boolean" },
 		isCat: { type: "boolean" },
+		speakAsCat: { type: "boolean" },
 		showTimelineReplies: { type: "boolean" },
 		injectFeaturedNote: { type: "boolean" },
 		receiveAnnouncementEmail: { type: "boolean" },
@@ -191,6 +192,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 		profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
 	if (typeof ps.noCrawle === "boolean") profileUpdates.noCrawle = ps.noCrawle;
 	if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat;
+	if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
 	if (typeof ps.injectFeaturedNote === "boolean")
 		profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
 	if (typeof ps.receiveAnnouncementEmail === "boolean")
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index a2ed6fd41..4e51a339b 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -59,6 +59,7 @@
 	</FormSlot>
 
 	<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
+	<FormSwitch v-if="profile.isCat" v-model="profile.speakAsCat" class="_formBlock">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></FormSwitch>
 	<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch>
 	<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
 	<div v-if="saveButton == true">
@@ -92,6 +93,7 @@ const profile = reactive({
 	lang: $i?.lang,
 	isBot: $i?.isBot,
 	isCat: $i?.isCat,
+	speakAsCat: $i?.speakAsCat,
 	showTimelineReplies: $i?.showTimelineReplies,
 });
 
@@ -135,6 +137,7 @@ function save() {
 		lang: profile.lang || null,
 		isBot: !!profile.isBot,
 		isCat: !!profile.isCat,
+		speakAsCat: !!profile.speakAsCat,
 		showTimelineReplies: !!profile.showTimelineReplies,
 	});
 }