From 3c5b1280c7097bc66e33f5ea27eadfd0fe9684bd Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 12 May 2023 00:46:26 -0400
Subject: [PATCH] add initial button

---
 locales/en-US.yml                             |  3 ++
 .../src/components/MkSubNoteContent.vue       | 47 ++++++++++++++++++-
 packages/client/src/scripts/extract-mfm.ts    | 16 +++++++
 3 files changed, 65 insertions(+), 1 deletion(-)
 create mode 100644 packages/client/src/scripts/extract-mfm.ts

diff --git a/locales/en-US.yml b/locales/en-US.yml
index bd32c836a..4c73c5b8f 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1193,6 +1193,9 @@ _nsfw:
   ignore: "Don't hide NSFW media"
   force: "Hide all media"
 _mfm:
+  play: "Play MFM"
+  stop: "Stop MFM"
+  warn: "MFM may contain rapidly moving or flashy animations"
   cheatSheet: "MFM Cheatsheet"
   intro: "MFM is a markup language used on Misskey, Calckey, Akkoma, and more that\
     \ can be used in many places. Here you can view a list of all available MFM syntax."
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 3224f2da6..773f7f26e 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -33,7 +33,7 @@
 	<div class="wrmlmaau">
 		<div
 			class="content"
-			:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
+			:class="{ collapsed, isLong, showContent: note.cw && !showContent, disableAnim: disableMfm }"
 		>
 			<XCwButton
 				ref="cwButton"
@@ -120,6 +120,17 @@
 				v-model="collapsed"
 			></XShowMoreButton>
 			<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
+			<MkButton
+				v-if="hasMfm"
+				@click.stop="toggleMfm"
+			>
+				<template v-if="disableMfm">
+					<i class="ph-play ph-bold"></i> {{ i18n.ts._mfm.play }}
+				</template>
+				<template v-else>
+					<i class="ph-stop ph-bold"></i> {{ i18n.ts._mfm.stop }}
+				</template>
+			</MkButton>
 		</div>
 	</div>
 </template>
@@ -128,13 +139,16 @@
 import { ref } from "vue";
 import * as misskey from "calckey-js";
 import * as mfm from "mfm-js";
+import * as os from "@/os";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
 import XMediaList from "@/components/MkMediaList.vue";
 import XPoll from "@/components/MkPoll.vue";
 import MkUrlPreview from "@/components/MkUrlPreview.vue";
 import XShowMoreButton from "@/components/MkShowMoreButton.vue";
 import XCwButton from "@/components/MkCwButton.vue";
+import MkButton from "@/components/MkButton.vue";
 import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
+import { extractMfmWithAnimation } from "@/scripts/extract-mfm";
 import { i18n } from "@/i18n";
 
 const props = defineProps<{
@@ -164,6 +178,26 @@ const urls = props.note.text
 
 let showContent = $ref(false);
 
+const mfms = props.note.text ? extractMfmWithAnimation(mfm.parse(props.note.text)) : null;
+
+const hasMfm = $ref(mfms.length > 0);
+
+let disableMfm = $ref(hasMfm);
+
+async function toggleMfm() {
+	if (disableMfm) {
+		const { canceled } = await os.confirm({
+			type: "warning",
+			text: i18n.ts._mfm.warn,
+		});
+		if (canceled) return;
+
+		disableMfm = false;
+	} else {
+		disableMfm = true;
+	}
+}
+
 function focusFooter(ev) {
 	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
 		emit("focusfooter");
@@ -195,6 +229,12 @@ function focusFooter(ev) {
 		margin-right: 8px;
 	}
 }
+
+.mfm-warning {
+	button {
+		padding: 1em;
+	}
+}
 .wrmlmaau {
 	.content {
 		overflow-wrap: break-word;
@@ -286,6 +326,11 @@ function focusFooter(ev) {
 				}
 			}
 		}
+
+		&.disableAnim :deep(*) {
+			animation: none !important;
+			transition: none !important;
+		}
 	}
 }
 </style>
diff --git a/packages/client/src/scripts/extract-mfm.ts b/packages/client/src/scripts/extract-mfm.ts
new file mode 100644
index 000000000..88b1bb63f
--- /dev/null
+++ b/packages/client/src/scripts/extract-mfm.ts
@@ -0,0 +1,16 @@
+import * as mfm from "mfm-js";
+
+const animatedMfm = ["tada", "jelly", "twitch", "shake", "spin", "jump", "bounce", "rainbow"];
+
+export function extractMfmWithAnimation(
+	nodes: mfm.MfmNode[],
+): string[] {
+	const mfmNodes = mfm.extract(nodes, (node) => {
+		return (
+			node.type === "fn" && animatedMfm.indexOf(node.props.name) > -1
+		);
+	});
+	const mfms = mfmNodes.map((x) => x.props.fn);
+
+	return mfms;
+}