From a0c63c8cc4b8035b5498abf438e4be2a01346ebf Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Sat, 16 Mar 2024 00:27:26 +0100
Subject: [PATCH 01/31] added notification dot and it seems to work well

---
 packages/frontend/src/ui/_common_/common.vue | 71 +++++++++++++++++++-
 1 file changed, 70 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 4fe53ae6a3..85d7b201ce 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { swInject } from './sw-inject.js';
 import XNotification from './notification.vue';
@@ -70,6 +70,70 @@ const dev = _DEV_;
 
 const notifications = ref<Misskey.entities.Notification[]>([]);
 
+class NotificationFavIconDot {
+	canvas : HTMLCanvasElement;
+	src : string | null = null;
+	ctx : CanvasRenderingContext2D | null = null;
+	favconImage : HTMLImageElement | null = null;
+
+	constructor() {
+		this.canvas = document.createElement('canvas');
+
+		if (this.faviconEL == null) return;
+
+		this.src = this.faviconEL.getAttribute('href');
+		this.ctx = this.canvas.getContext('2d');
+
+		this.favconImage = document.createElement('img');
+		this.favconImage.src = this.faviconEL.href;
+
+		this.favconImage.onload = () => {
+			if (!this.favconImage) return;
+
+			this.canvas.width = this.favconImage.width;
+			this.canvas.height = this.favconImage.height;
+		};
+	}
+
+	faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
+
+	_createFaviconElem() {
+		const newLink = document.createElement('link');
+		newLink.rel = 'icon';
+		newLink.href = '/favicon.ico';
+		document.head.appendChild(newLink);
+		return newLink;
+	}
+
+	_drawIcon() {
+		if (!this.ctx || !this.favconImage) return;
+		this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height);
+	}
+
+	_drawDot() {
+		if (!this.ctx || !this.favconImage) return;
+		this.ctx.beginPath();
+		this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI);
+		this.ctx.fillStyle = 'red';
+		this.ctx.strokeStyle = this.ctx.fillStyle;
+		this.ctx.fill();
+		this.ctx.stroke();
+	}
+
+	_drawFavicon() {
+		this.faviconEL.href = this.canvas.toDataURL('image/png');
+	}
+
+	setVisible(isVisible : boolean) {
+		this.ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height);
+		this._drawIcon();
+		if (isVisible) this._drawDot();
+		this._drawFavicon();
+	}
+}
+
+const notificationDot = new NotificationFavIconDot();
+
 function onNotification(notification: Misskey.entities.Notification, isClient = false) {
 	if (document.visibilityState === 'visible') {
 		if (!isClient && notification.type !== 'test') {
@@ -93,6 +157,11 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
 if ($i) {
 	const connection = useStream().useChannel('main', null, 'UI');
 	connection.on('notification', onNotification);
+	
+	watch(() => $i?.hasUnreadNotification, (hasAny) => {
+		notificationDot.setVisible(hasAny ?? false);
+	});
+
 	globalEvents.on('clientNotification', notification => onNotification(notification, true));
 
 	//#region Listen message from SW

From 395ea9ab9fd29ac72226e2deda9a0632d46df323 Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Sat, 16 Mar 2024 01:08:08 +0100
Subject: [PATCH 02/31] made notification dot appear if you load the page and
 there are already notifs

---
 packages/frontend/src/ui/_common_/common.vue | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 85d7b201ce..70097d89ab 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -75,6 +75,7 @@ class NotificationFavIconDot {
 	src : string | null = null;
 	ctx : CanvasRenderingContext2D | null = null;
 	favconImage : HTMLImageElement | null = null;
+	loaded = false;
 
 	constructor() {
 		this.canvas = document.createElement('canvas');
@@ -92,6 +93,7 @@ class NotificationFavIconDot {
 
 			this.canvas.width = this.favconImage.width;
 			this.canvas.height = this.favconImage.height;
+			this.loaded = true;
 		};
 	}
 
@@ -120,11 +122,14 @@ class NotificationFavIconDot {
 		this.ctx.stroke();
 	}
 
-	_drawFavicon() {
+	private _drawFavicon() {
 		this.faviconEL.href = this.canvas.toDataURL('image/png');
 	}
 
-	setVisible(isVisible : boolean) {
+	async setVisible(isVisible : boolean) {
+		//Wait for it to have loaded the icon
+		const waiter = (done) => (this.loaded ? done() : setTimeout(() => waiter(done), 500));
+		await new Promise(waiter);
 		this.ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height);
 		this._drawIcon();
 		if (isVisible) this._drawDot();
@@ -158,9 +163,9 @@ if ($i) {
 	const connection = useStream().useChannel('main', null, 'UI');
 	connection.on('notification', onNotification);
 	
-	watch(() => $i?.hasUnreadNotification, (hasAny) => {
-		notificationDot.setVisible(hasAny ?? false);
-	});
+	watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible(hasAny ?? false));
+	
+	if ($i.hasUnreadNotification) notificationDot.setVisible(true);
 
 	globalEvents.on('clientNotification', notification => onNotification(notification, true));
 

From 8f300cf4601776e00ebe72b6f6fa5fb755dec6d3 Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Sat, 16 Mar 2024 01:23:02 +0100
Subject: [PATCH 03/31] added setting

---
 locales/en-US.yml                                      |  1 +
 locales/index.d.ts                                     |  4 ++++
 locales/ja-JP.yml                                      |  1 +
 packages/frontend/src/pages/settings/general.vue       |  2 ++
 .../src/pages/settings/preferences-backups.vue         |  1 +
 packages/frontend/src/store.ts                         |  4 ++++
 packages/frontend/src/ui/_common_/common.vue           | 10 ++++------
 7 files changed, 17 insertions(+), 6 deletions(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 80e45c42d8..e22b92e2f6 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -688,6 +688,7 @@ channel: "Channels"
 create: "Create"
 notificationSetting: "Notification settings"
 notificationSettingDesc: "Select the types of notification to display."
+enableFaviconNotificationDot: "Enable favicon notification dot"
 useGlobalSetting: "Use global settings"
 useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made."
 other: "Other"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index e407d2119b..422f1c8429 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2764,6 +2764,10 @@ export interface Locale extends ILocale {
      * 表示する通知の種別を選択してください。
      */
     "notificationSettingDesc": string;
+    /**
+     * ファビコン通知ドットを有効にする
+     */
+    "enableFaviconNotificationDot": string;
     /**
      * グローバル設定を使う
      */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 57f52c64b2..e20907e6b7 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -687,6 +687,7 @@ channel: "チャンネル"
 create: "作成"
 notificationSetting: "通知設定"
 notificationSettingDesc: "表示する通知の種別を選択してください。"
+enableFaviconNotificationDot: "ファビコン通知ドットを有効にする"
 useGlobalSetting: "グローバル設定を使う"
 useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
 other: "その他"
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 1e4e815d5d..b216622c33 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -180,6 +180,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 		<div class="_gaps_m">
 			<div class="_gaps_s">
+				<MkSwitch v-model="enableFaviconNotificationDot">{{ i18n.ts.enableFaviconNotificationDot }}</MkSwitch>
 				<MkSwitch v-model="warnMissingAltText">{{ i18n.ts.warnForMissingAltText }}</MkSwitch>
 				<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
 				<MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>
@@ -337,6 +338,7 @@ const oneko = computed(defaultStore.makeGetterSetter('oneko'));
 const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
 const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
 const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
+const enableFaviconNotificationDot = computed(defaultStore.makeGetterSetter('enableFaviconNotificationDot'));
 const warnMissingAltText = computed(defaultStore.makeGetterSetter('warnMissingAltText'));
 const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
 const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index f180e0b72c..86b8debe3a 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -72,6 +72,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'advancedMfm',
 	'loadRawImages',
 	'warnMissingAltText',
+	'enableFaviconNotificationDot',
 	'imageNewTab',
 	'dataSaver',
 	'disableShowingAnimatedImages',
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 2cf17b27c5..7f6377613e 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -268,6 +268,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
+	enableFaviconNotificationDot: {
+		where: 'device',
+		default: true,
+	},
 	imageNewTab: {
 		where: 'device',
 		default: false,
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 70097d89ab..77148835f8 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -80,8 +80,6 @@ class NotificationFavIconDot {
 	constructor() {
 		this.canvas = document.createElement('canvas');
 
-		if (this.faviconEL == null) return;
-
 		this.src = this.faviconEL.getAttribute('href');
 		this.ctx = this.canvas.getContext('2d');
 
@@ -162,11 +160,11 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
 if ($i) {
 	const connection = useStream().useChannel('main', null, 'UI');
 	connection.on('notification', onNotification);
-	
-	watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible(hasAny ?? false));
-	
-	if ($i.hasUnreadNotification) notificationDot.setVisible(true);
 
+	watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false));
+
+	if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) notificationDot.setVisible(true);
+	
 	globalEvents.on('clientNotification', notification => onNotification(notification, true));
 
 	//#region Listen message from SW

From ccf5659ac3548b671820c4fe45d6656aca1013f6 Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Sat, 16 Mar 2024 01:34:20 +0100
Subject: [PATCH 04/31] made methods private

---
 packages/frontend/src/ui/_common_/common.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 77148835f8..78fd2d2172 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -97,7 +97,7 @@ class NotificationFavIconDot {
 
 	faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
 
-	_createFaviconElem() {
+	private _createFaviconElem() {
 		const newLink = document.createElement('link');
 		newLink.rel = 'icon';
 		newLink.href = '/favicon.ico';
@@ -105,12 +105,12 @@ class NotificationFavIconDot {
 		return newLink;
 	}
 
-	_drawIcon() {
+	private _drawIcon() {
 		if (!this.ctx || !this.favconImage) return;
 		this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height);
 	}
 
-	_drawDot() {
+	private _drawDot() {
 		if (!this.ctx || !this.favconImage) return;
 		this.ctx.beginPath();
 		this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI);

From 459e6841179979188b119be406e030e1afaf5516 Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Tue, 19 Mar 2024 22:00:28 +0100
Subject: [PATCH 05/31] fixed some of the issues with it

---
 packages/frontend/src/ui/_common_/common.vue | 37 ++++++++++----------
 1 file changed, 19 insertions(+), 18 deletions(-)

diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 78fd2d2172..a5e5d19a35 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -75,28 +75,29 @@ class NotificationFavIconDot {
 	src : string | null = null;
 	ctx : CanvasRenderingContext2D | null = null;
 	favconImage : HTMLImageElement | null = null;
-	loaded = false;
+	faviconEL : HTMLLinkElement;
+	hasLoaded : Promise;
 
 	constructor() {
 		this.canvas = document.createElement('canvas');
+		this.faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
 
 		this.src = this.faviconEL.getAttribute('href');
 		this.ctx = this.canvas.getContext('2d');
-
+		
 		this.favconImage = document.createElement('img');
 		this.favconImage.src = this.faviconEL.href;
+		this.hasLoaded = new Promise((resolve, reject) => {
+			this.favconImage.onload = () => {
+				this.canvas.width = this.favconImage.width;
+				this.canvas.height = this.favconImage.height;
 
-		this.favconImage.onload = () => {
-			if (!this.favconImage) return;
-
-			this.canvas.width = this.favconImage.width;
-			this.canvas.height = this.favconImage.height;
-			this.loaded = true;
-		};
+				// resolve();
+				setTimeout(() => resolve(), 500);
+			};
+		});
 	}
 
-	faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
-
 	private _createFaviconElem() {
 		const newLink = document.createElement('link');
 		newLink.rel = 'icon';
@@ -107,6 +108,7 @@ class NotificationFavIconDot {
 
 	private _drawIcon() {
 		if (!this.ctx || !this.favconImage) return;
+		this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
 		this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height);
 	}
 
@@ -114,24 +116,23 @@ class NotificationFavIconDot {
 		if (!this.ctx || !this.favconImage) return;
 		this.ctx.beginPath();
 		this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI);
-		this.ctx.fillStyle = 'red';
-		this.ctx.strokeStyle = this.ctx.fillStyle;
+		this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator');
+		this.ctx.strokeStyle = 'white';
 		this.ctx.fill();
 		this.ctx.stroke();
 	}
 
-	private _drawFavicon() {
+	private _setFavicon() {
 		this.faviconEL.href = this.canvas.toDataURL('image/png');
 	}
 
 	async setVisible(isVisible : boolean) {
 		//Wait for it to have loaded the icon
-		const waiter = (done) => (this.loaded ? done() : setTimeout(() => waiter(done), 500));
-		await new Promise(waiter);
-		this.ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height);
+		await this.hasLoaded;
+		console.log(this.hasLoaded);
 		this._drawIcon();
 		if (isVisible) this._drawDot();
-		this._drawFavicon();
+		this._setFavicon();
 	}
 }
 

From 6bc258a3e02f9492d361b146afac91055103d9db Mon Sep 17 00:00:00 2001
From: Marie <marie@kaifa.ch>
Date: Fri, 29 Mar 2024 16:52:22 +0000
Subject: [PATCH 06/31] chore: automatically detect RTL on all MFM content

---
 .../frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index f8b5fcfedc..22a02d4f60 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -473,6 +473,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 	}).flat(Infinity) as (VNode | string)[];
 
 	return h('span', {
+		dir: 'auto',
 		// https://codeday.me/jp/qa/20190424/690106.html
 		style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
 	}, genEl(rootAst, props.rootScale ?? 1));

From 6bdb4a7ddcab93571911e5bcfbedef67d07bcd42 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Fri, 29 Mar 2024 18:08:17 +0000
Subject: [PATCH 07/31] dir=auto on post form textarea, too

---
 packages/frontend/src/components/MkPostForm.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index d9e50fbb79..85b6073d9d 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
 	<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
 		<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
-		<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
+		<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
 		<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
 	</div>
 	<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">

From aa11348d007d1d0a9fbf9a28234e444fde2f21ac Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Sat, 30 Mar 2024 10:54:25 +0000
Subject: [PATCH 08/31] `<bid>` all over the place

---
 .../global/MkMisskeyFlavoredMarkdown.ts       | 49 +++++++++----------
 packages/frontend/src/style.scss              |  2 +
 2 files changed, 26 insertions(+), 25 deletions(-)

diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index 22a02d4f60..df55d54269 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -335,67 +335,67 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 			}
 
 			case 'center': {
-				return [h('div', {
+				return [h('bdi',h('div', {
 					style: 'text-align:center;',
-				}, genEl(token.children, scale))];
+				}, genEl(token.children, scale)))];
 			}
 
 			case 'url': {
-				return [h(MkUrl, {
+				return [h('bdi',h(MkUrl, {
 					key: Math.random(),
 					url: token.props.url,
 					rel: 'nofollow noopener',
-				})];
+				}))];
 			}
 
 			case 'link': {
-				return [h(MkLink, {
+				return [h('bdi',h(MkLink, {
 					key: Math.random(),
 					url: token.props.url,
 					rel: 'nofollow noopener',
-				}, genEl(token.children, scale, true))];
+				}, genEl(token.children, scale, true)))];
 			}
 
 			case 'mention': {
-				return [h(MkMention, {
+				return [h('bdi',h(MkMention, {
 					key: Math.random(),
 					host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
 					username: token.props.username,
-				})];
+				}))];
 			}
 
 			case 'hashtag': {
-				return [h(MkA, {
+				return [h('bdi',h(MkA, {
 					key: Math.random(),
 					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
 					style: 'color:var(--hashtag);',
-				}, `#${token.props.hashtag}`)];
+				}, `#${token.props.hashtag}`))];
 			}
 
 			case 'blockCode': {
-				return [h(MkCode, {
+				return [h('bdi',h(MkCode, {
 					key: Math.random(),
 					code: token.props.code,
 					lang: token.props.lang ?? undefined,
-				})];
+				}))];
 			}
 
 			case 'inlineCode': {
-				return [h(MkCodeInline, {
+				return [h('bdi',h(MkCodeInline, {
 					key: Math.random(),
 					code: token.props.code,
-				})];
+				}))];
 			}
 
 			case 'quote': {
 				if (!props.nowrap) {
-					return [h('div', {
+					return [h('bdi',h('div', {
 						style: QUOTE_STYLE,
-					}, genEl(token.children, scale, true))];
+					}, genEl(token.children, scale, true)))];
 				} else {
-					return [h('span', {
+					return [h('bdi',h('span', {
 						style: QUOTE_STYLE,
-					}, genEl(token.children, scale, true))];
+					}, genEl(token.children, scale, true)))];
 				}
 			}
 
@@ -439,17 +439,17 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 			}
 
 			case 'mathInline': {
-				return [h(MkFormula, {
+				return [h('bdi',h(MkFormula, {
 					formula: token.props.formula,
 					block: false,
-				})];
+				}))];
 			}
 
 			case 'mathBlock': {
-				return [h(MkFormula, {
+				return [h('bdi',h(MkFormula, {
 					formula: token.props.formula,
 					block: true,
-				})];
+				}))];
 			}
 
 			case 'search': {
@@ -472,9 +472,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 		}
 	}).flat(Infinity) as (VNode | string)[];
 
-	return h('span', {
-		dir: 'auto',
+	return h('bdi', h('span', {
 		// https://codeday.me/jp/qa/20190424/690106.html
 		style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
-	}, genEl(rootAst, props.rootScale ?? 1));
+	}, genEl(rootAst, props.rootScale ?? 1)));
 }
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index d876009961..8821f3cc7f 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -557,6 +557,8 @@ rt {
 
 // MFM -----------------------------
 
+div > bdi, p > bdi { display: block }
+
 ._mfm_blur_ {
 	filter: blur(6px);
 	transition: filter 0.3s;

From de80727cfc725a2f072c5d8929ad2b87c1cf49ae Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Tue, 2 Apr 2024 23:30:14 +0200
Subject: [PATCH 09/31] Moved class to seperate file and fixed some ts warnings

---
 packages/frontend/src/scripts/favicon-dot.ts | 75 ++++++++++++++++++++
 packages/frontend/src/ui/_common_/common.vue | 74 ++-----------------
 2 files changed, 79 insertions(+), 70 deletions(-)
 create mode 100644 packages/frontend/src/scripts/favicon-dot.ts

diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
new file mode 100644
index 0000000000..3a7887bca9
--- /dev/null
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -0,0 +1,75 @@
+class FavIconDot {
+	canvas : HTMLCanvasElement;
+	src : string | null = null;
+	ctx : CanvasRenderingContext2D | null = null;
+	favconImage : HTMLImageElement | null = null;
+	faviconEL : HTMLLinkElement;
+	hasLoaded : Promise<void>;
+
+	constructor() {
+		this.canvas = document.createElement('canvas');
+		this.faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
+
+		this.src = this.faviconEL.getAttribute('href');
+		this.ctx = this.canvas.getContext('2d');
+		
+		this.favconImage = document.createElement('img');
+		this.hasLoaded = new Promise((resolve, _reject) => {
+			if (this.favconImage != null) {
+				this.favconImage.onload = () => {
+					this.canvas.width = (this.favconImage as HTMLImageElement).width;
+					this.canvas.height = (this.favconImage as HTMLImageElement).height;
+					// resolve();
+					setTimeout(() => resolve(), 200);
+				};
+			}
+		});
+		this.favconImage.src = this.faviconEL.href;
+	}
+
+	private _createFaviconElem() {
+		const newLink = document.createElement('link');
+		newLink.rel = 'icon';
+		newLink.href = '/favicon.ico';
+		document.head.appendChild(newLink);
+		return newLink;
+	}
+
+	private _drawIcon() {
+		if (!this.ctx || !this.favconImage) return;
+		this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+		this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height);
+	}
+
+	private _drawDot() {
+		if (!this.ctx || !this.favconImage) return;
+		this.ctx.beginPath();
+		this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI);
+		this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator');
+		this.ctx.strokeStyle = 'white';
+		this.ctx.fill();
+		this.ctx.stroke();
+	}
+
+	private _setFavicon() {
+		this.faviconEL.href = this.canvas.toDataURL('image/png');
+	}
+
+	async setVisible(isVisible : boolean) {
+		//Wait for it to have loaded the icon
+		await this.hasLoaded;
+		console.log(this.hasLoaded);
+		this._drawIcon();
+		if (isVisible) this._drawDot();
+		this._setFavicon();
+	}
+}
+
+let icon: FavIconDot = new FavIconDot();
+
+export function setFavIconDot(visible: boolean) {
+	if (!icon) {
+		icon = new FavIconDot();
+	}
+	icon.setVisible(visible);
+}
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index a5e5d19a35..63b19dfb26 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -49,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
+import { setFavIconDot } from '../../scripts/favicon-dot';
 import { swInject } from './sw-inject.js';
 import XNotification from './notification.vue';
 import { popups } from '@/os.js';
@@ -70,74 +71,6 @@ const dev = _DEV_;
 
 const notifications = ref<Misskey.entities.Notification[]>([]);
 
-class NotificationFavIconDot {
-	canvas : HTMLCanvasElement;
-	src : string | null = null;
-	ctx : CanvasRenderingContext2D | null = null;
-	favconImage : HTMLImageElement | null = null;
-	faviconEL : HTMLLinkElement;
-	hasLoaded : Promise;
-
-	constructor() {
-		this.canvas = document.createElement('canvas');
-		this.faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
-
-		this.src = this.faviconEL.getAttribute('href');
-		this.ctx = this.canvas.getContext('2d');
-		
-		this.favconImage = document.createElement('img');
-		this.favconImage.src = this.faviconEL.href;
-		this.hasLoaded = new Promise((resolve, reject) => {
-			this.favconImage.onload = () => {
-				this.canvas.width = this.favconImage.width;
-				this.canvas.height = this.favconImage.height;
-
-				// resolve();
-				setTimeout(() => resolve(), 500);
-			};
-		});
-	}
-
-	private _createFaviconElem() {
-		const newLink = document.createElement('link');
-		newLink.rel = 'icon';
-		newLink.href = '/favicon.ico';
-		document.head.appendChild(newLink);
-		return newLink;
-	}
-
-	private _drawIcon() {
-		if (!this.ctx || !this.favconImage) return;
-		this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
-		this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height);
-	}
-
-	private _drawDot() {
-		if (!this.ctx || !this.favconImage) return;
-		this.ctx.beginPath();
-		this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI);
-		this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator');
-		this.ctx.strokeStyle = 'white';
-		this.ctx.fill();
-		this.ctx.stroke();
-	}
-
-	private _setFavicon() {
-		this.faviconEL.href = this.canvas.toDataURL('image/png');
-	}
-
-	async setVisible(isVisible : boolean) {
-		//Wait for it to have loaded the icon
-		await this.hasLoaded;
-		console.log(this.hasLoaded);
-		this._drawIcon();
-		if (isVisible) this._drawDot();
-		this._setFavicon();
-	}
-}
-
-const notificationDot = new NotificationFavIconDot();
-
 function onNotification(notification: Misskey.entities.Notification, isClient = false) {
 	if (document.visibilityState === 'visible') {
 		if (!isClient && notification.type !== 'test') {
@@ -162,9 +95,10 @@ if ($i) {
 	const connection = useStream().useChannel('main', null, 'UI');
 	connection.on('notification', onNotification);
 
-	watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false));
+	//For the favicon notification dot
+	watch(() => $i?.hasUnreadNotification, (hasAny) => setFavIconDot((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false));
 
-	if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) notificationDot.setVisible(true);
+	if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true);
 	
 	globalEvents.on('clientNotification', notification => onNotification(notification, true));
 

From 7730472c7789a2d67ca10d6b3f12e7c7301521fe Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Tue, 2 Apr 2024 23:33:37 +0200
Subject: [PATCH 10/31] moved setting toggle under notifications category

---
 packages/frontend/src/pages/settings/general.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index b216622c33..c96d803d12 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -112,6 +112,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div class="_gaps_m">
 			<MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch>
 
+			<MkSwitch v-model="enableFaviconNotificationDot">{{ i18n.ts.enableFaviconNotificationDot }}</MkSwitch>
+
 			<MkRadios v-model="notificationPosition">
 				<template #label>{{ i18n.ts.position }}</template>
 				<option value="leftTop"><i class="ph-arrow-up-left ph-bold ph-lg"></i> {{ i18n.ts.leftTop }}</option>
@@ -180,7 +182,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 		<div class="_gaps_m">
 			<div class="_gaps_s">
-				<MkSwitch v-model="enableFaviconNotificationDot">{{ i18n.ts.enableFaviconNotificationDot }}</MkSwitch>
 				<MkSwitch v-model="warnMissingAltText">{{ i18n.ts.warnForMissingAltText }}</MkSwitch>
 				<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
 				<MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>

From 590f7abefdadc25f540d40c6962d193d2a47368e Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Thu, 2 May 2024 17:22:30 +0200
Subject: [PATCH 11/31] removed use of settimeout

---
 packages/frontend/src/scripts/favicon-dot.ts | 81 ++++++++++++++------
 1 file changed, 58 insertions(+), 23 deletions(-)

diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
index 3a7887bca9..d54991f067 100644
--- a/packages/frontend/src/scripts/favicon-dot.ts
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -2,29 +2,52 @@ class FavIconDot {
 	canvas : HTMLCanvasElement;
 	src : string | null = null;
 	ctx : CanvasRenderingContext2D | null = null;
-	favconImage : HTMLImageElement | null = null;
-	faviconEL : HTMLLinkElement;
-	hasLoaded : Promise<void>;
+	faviconImage : HTMLImageElement | null = null;
+	faviconEL : HTMLLinkElement | undefined;
+	hasLoaded : Promise<void> | undefined;
 
 	constructor() {
 		this.canvas = document.createElement('canvas');
-		this.faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
+	}
 
+	//MUST BE CALLED BEFORE CALLING ANY OTHER FUNCTIONS
+	public async setup() {
+		const element : HTMLLinkElement = await this.getOrMakeFaviconElement();
+		
+		this.faviconEL = element;
 		this.src = this.faviconEL.getAttribute('href');
 		this.ctx = this.canvas.getContext('2d');
-		
-		this.favconImage = document.createElement('img');
-		this.hasLoaded = new Promise((resolve, _reject) => {
-			if (this.favconImage != null) {
-				this.favconImage.onload = () => {
-					this.canvas.width = (this.favconImage as HTMLImageElement).width;
-					this.canvas.height = (this.favconImage as HTMLImageElement).height;
-					// resolve();
-					setTimeout(() => resolve(), 200);
+			
+		this.faviconImage = document.createElement('img');
+		this.faviconImage.src = this.faviconEL.href;
+	
+		this.hasLoaded = new Promise((resolve, reject) => {
+			(this.faviconImage as HTMLImageElement).onload = () => {
+				this.canvas.width = (this.faviconImage as HTMLImageElement).width;
+				this.canvas.height = (this.faviconImage as HTMLImageElement).height;
+				resolve();
+			};
+	
+			(this.faviconImage as HTMLImageElement).onerror = () => {
+				reject('Failed to create favicon img element');
+			};
+		});
+	}
+
+	private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> {
+		return new Promise((resolve, reject) => {
+			const favicon = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
+			if (favicon === document.querySelector<HTMLLinkElement>('link[rel$=icon]')) {
+				favicon.onload = () => {
+					resolve(favicon);
+				};
+
+				favicon.onerror = () => {
+					reject('Failed to load favicon');
 				};
 			}
+			resolve(favicon);
 		});
-		this.favconImage.src = this.faviconEL.href;
 	}
 
 	private _createFaviconElem() {
@@ -36,15 +59,15 @@ class FavIconDot {
 	}
 
 	private _drawIcon() {
-		if (!this.ctx || !this.favconImage) return;
+		if (!this.ctx || !this.faviconImage) return;
 		this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
-		this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height);
+		this.ctx.drawImage(this.faviconImage, 0, 0, this.faviconImage.width, this.faviconImage.height);
 	}
 
 	private _drawDot() {
-		if (!this.ctx || !this.favconImage) return;
+		if (!this.ctx || !this.faviconImage) return;
 		this.ctx.beginPath();
-		this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI);
+		this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI);
 		this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator');
 		this.ctx.strokeStyle = 'white';
 		this.ctx.fill();
@@ -52,7 +75,7 @@ class FavIconDot {
 	}
 
 	private _setFavicon() {
-		this.faviconEL.href = this.canvas.toDataURL('image/png');
+		if (this.faviconEL) this.faviconEL.href = this.canvas.toDataURL('image/png');
 	}
 
 	async setVisible(isVisible : boolean) {
@@ -65,11 +88,23 @@ class FavIconDot {
 	}
 }
 
-let icon: FavIconDot = new FavIconDot();
+let icon: FavIconDot | undefined = undefined;
 
 export function setFavIconDot(visible: boolean) {
-	if (!icon) {
-		icon = new FavIconDot();
+	const setIconVisibility = async () => {
+		if (!icon) {
+			icon = new FavIconDot();
+			await icon.setup();
+		}
+		
+		(icon as FavIconDot).setVisible(visible);
+	};
+
+	// If document is already loaded, set visibility immediately
+	if (document.readyState === 'complete') {
+		setIconVisibility();
+	} else {
+		// Otherwise, set visibility when window loads
+		window.onload = setIconVisibility;
 	}
-	icon.setVisible(visible);
 }

From 42c29697070f40c994e8661f8b88ea652b46f162 Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Thu, 2 May 2024 17:38:16 +0200
Subject: [PATCH 12/31] fixes

---
 packages/frontend/src/scripts/favicon-dot.ts | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
index d54991f067..2e05171772 100644
--- a/packages/frontend/src/scripts/favicon-dot.ts
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -19,7 +19,6 @@ class FavIconDot {
 		this.ctx = this.canvas.getContext('2d');
 			
 		this.faviconImage = document.createElement('img');
-		this.faviconImage.src = this.faviconEL.href;
 	
 		this.hasLoaded = new Promise((resolve, reject) => {
 			(this.faviconImage as HTMLImageElement).onload = () => {
@@ -32,20 +31,20 @@ class FavIconDot {
 				reject('Failed to create favicon img element');
 			};
 		});
+
+		this.faviconImage.src = this.faviconEL.href;
 	}
 
 	private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> {
 		return new Promise((resolve, reject) => {
 			const favicon = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
-			if (favicon === document.querySelector<HTMLLinkElement>('link[rel$=icon]')) {
-				favicon.onload = () => {
-					resolve(favicon);
-				};
+			favicon.onload = () => {
+				resolve(favicon);
+			};
 
-				favicon.onerror = () => {
-					reject('Failed to load favicon');
-				};
-			}
+			favicon.onerror = () => {
+				reject('Failed to load favicon');
+			};
 			resolve(favicon);
 		});
 	}

From 47d1477ac493ac88d5b97b55de39e86251ab1735 Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Fri, 3 May 2024 11:27:41 +0200
Subject: [PATCH 13/31] did thread fixes

---
 packages/frontend/src/scripts/favicon-dot.ts | 16 +++++++---------
 packages/frontend/src/ui/_common_/common.vue |  2 +-
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
index 2e05171772..c40a08f427 100644
--- a/packages/frontend/src/scripts/favicon-dot.ts
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -21,15 +21,14 @@ class FavIconDot {
 		this.faviconImage = document.createElement('img');
 	
 		this.hasLoaded = new Promise((resolve, reject) => {
-			(this.faviconImage as HTMLImageElement).onload = () => {
+			(this.faviconImage as HTMLImageElement).addEventListener('load', () => {
 				this.canvas.width = (this.faviconImage as HTMLImageElement).width;
 				this.canvas.height = (this.faviconImage as HTMLImageElement).height;
 				resolve();
-			};
-	
-			(this.faviconImage as HTMLImageElement).onerror = () => {
+			});
+			(this.faviconImage as HTMLImageElement).addEventListener('error', () => {
 				reject('Failed to create favicon img element');
-			};
+			});
 		});
 
 		this.faviconImage.src = this.faviconEL.href;
@@ -38,9 +37,9 @@ class FavIconDot {
 	private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> {
 		return new Promise((resolve, reject) => {
 			const favicon = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
-			favicon.onload = () => {
+			favicon.addEventListener('load', () => {
 				resolve(favicon);
-			};
+			});
 
 			favicon.onerror = () => {
 				reject('Failed to load favicon');
@@ -80,7 +79,6 @@ class FavIconDot {
 	async setVisible(isVisible : boolean) {
 		//Wait for it to have loaded the icon
 		await this.hasLoaded;
-		console.log(this.hasLoaded);
 		this._drawIcon();
 		if (isVisible) this._drawDot();
 		this._setFavicon();
@@ -104,6 +102,6 @@ export function setFavIconDot(visible: boolean) {
 		setIconVisibility();
 	} else {
 		// Otherwise, set visibility when window loads
-		window.onload = setIconVisibility;
+		window.addEventListener('load', setIconVisibility);
 	}
 }
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 63b19dfb26..bec8bb6c7c 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -96,7 +96,7 @@ if ($i) {
 	connection.on('notification', onNotification);
 
 	//For the favicon notification dot
-	watch(() => $i?.hasUnreadNotification, (hasAny) => setFavIconDot((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false));
+	watch(() => $i?.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot, (hasAny) => setFavIconDot(hasAny as boolean));
 
 	if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true);
 	

From 975b6b3dd0f34731ad442480fc39e9a741ad76dc Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Fri, 3 May 2024 12:15:47 +0200
Subject: [PATCH 14/31] fixed querySelector that would grab favicon

---
 packages/frontend/src/scripts/favicon-dot.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
index c40a08f427..0229092ca7 100644
--- a/packages/frontend/src/scripts/favicon-dot.ts
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -36,7 +36,7 @@ class FavIconDot {
 
 	private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> {
 		return new Promise((resolve, reject) => {
-			const favicon = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem();
+			const favicon = (document.querySelector('link[rel=icon]') ?? this._createFaviconElem()) as HTMLLinkElement;
 			favicon.addEventListener('load', () => {
 				resolve(favicon);
 			});

From 0117f1896c92843b7cb7525a454ba9d3d6c8cf03 Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Fri, 3 May 2024 12:20:58 +0200
Subject: [PATCH 15/31] potential firefox fix

---
 packages/frontend/src/scripts/favicon-dot.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
index 0229092ca7..143ad50c9a 100644
--- a/packages/frontend/src/scripts/favicon-dot.ts
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -50,8 +50,10 @@ class FavIconDot {
 
 	private _createFaviconElem() {
 		const newLink = document.createElement('link');
-		newLink.rel = 'icon';
-		newLink.href = '/favicon.ico';
+		newLink.setAttribute('rel', 'icon');
+		newLink.setAttribute('href', '/favicon.ico');
+		newLink.setAttribute('type', 'image/x-icon');
+
 		document.head.appendChild(newLink);
 		return newLink;
 	}

From 342eda431f6e7b19742c909fced38fdef772ccea Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Mon, 6 May 2024 13:54:43 +0200
Subject: [PATCH 16/31] fixing a buch of comments

---
 packages/frontend/src/scripts/favicon-dot.ts | 40 ++++++++++----------
 packages/frontend/src/ui/_common_/common.vue |  2 +-
 2 files changed, 22 insertions(+), 20 deletions(-)

diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
index 143ad50c9a..643ee1b76b 100644
--- a/packages/frontend/src/scripts/favicon-dot.ts
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -1,18 +1,20 @@
 class FavIconDot {
-	canvas : HTMLCanvasElement;
-	src : string | null = null;
-	ctx : CanvasRenderingContext2D | null = null;
-	faviconImage : HTMLImageElement | null = null;
-	faviconEL : HTMLLinkElement | undefined;
-	hasLoaded : Promise<void> | undefined;
+	canvas: HTMLCanvasElement;
+	src: string | null = null;
+	ctx: CanvasRenderingContext2D | null = null;
+	faviconImage: HTMLImageElement | null = null;
+	faviconEL: HTMLLinkElement | undefined;
+	hasLoaded: Promise<void> | undefined;
 
 	constructor() {
 		this.canvas = document.createElement('canvas');
 	}
 
-	//MUST BE CALLED BEFORE CALLING ANY OTHER FUNCTIONS
+	/**
+	 * Must be called before calling any other functions
+	 */
 	public async setup() {
-		const element : HTMLLinkElement = await this.getOrMakeFaviconElement();
+		const element: HTMLLinkElement = await this.getOrMakeFaviconElement();
 		
 		this.faviconEL = element;
 		this.src = this.faviconEL.getAttribute('href');
@@ -34,9 +36,9 @@ class FavIconDot {
 		this.faviconImage.src = this.faviconEL.href;
 	}
 
-	private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> {
+	private async getOrMakeFaviconElement(): Promise<HTMLLinkElement> {
 		return new Promise((resolve, reject) => {
-			const favicon = (document.querySelector('link[rel=icon]') ?? this._createFaviconElem()) as HTMLLinkElement;
+			const favicon = (document.querySelector('link[rel=icon]') ?? this.createFaviconElem()) as HTMLLinkElement;
 			favicon.addEventListener('load', () => {
 				resolve(favicon);
 			});
@@ -48,7 +50,7 @@ class FavIconDot {
 		});
 	}
 
-	private _createFaviconElem() {
+	private createFaviconElem() {
 		const newLink = document.createElement('link');
 		newLink.setAttribute('rel', 'icon');
 		newLink.setAttribute('href', '/favicon.ico');
@@ -58,13 +60,13 @@ class FavIconDot {
 		return newLink;
 	}
 
-	private _drawIcon() {
+	private drawIcon() {
 		if (!this.ctx || !this.faviconImage) return;
 		this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
 		this.ctx.drawImage(this.faviconImage, 0, 0, this.faviconImage.width, this.faviconImage.height);
 	}
 
-	private _drawDot() {
+	private drawDot() {
 		if (!this.ctx || !this.faviconImage) return;
 		this.ctx.beginPath();
 		this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI);
@@ -74,16 +76,16 @@ class FavIconDot {
 		this.ctx.stroke();
 	}
 
-	private _setFavicon() {
+	private setFavicon() {
 		if (this.faviconEL) this.faviconEL.href = this.canvas.toDataURL('image/png');
 	}
 
-	async setVisible(isVisible : boolean) {
-		//Wait for it to have loaded the icon
+	async setVisible(isVisible: boolean) {
+		// Wait for it to have loaded the icon
 		await this.hasLoaded;
-		this._drawIcon();
-		if (isVisible) this._drawDot();
-		this._setFavicon();
+		this.drawIcon();
+		if (isVisible) this.drawDot();
+		this.setFavicon();
 	}
 }
 
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index bec8bb6c7c..b1fe8e54fc 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -95,7 +95,7 @@ if ($i) {
 	const connection = useStream().useChannel('main', null, 'UI');
 	connection.on('notification', onNotification);
 
-	//For the favicon notification dot
+	// For the favicon notification dot
 	watch(() => $i?.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot, (hasAny) => setFavIconDot(hasAny as boolean));
 
 	if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true);

From a058c855fc27c17d84dfb1655191cabdcf844c7a Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com>
Date: Mon, 6 May 2024 13:59:42 +0200
Subject: [PATCH 17/31] changed grabbing theme color for dot to match the other
 things in this project

---
 packages/frontend/src/scripts/favicon-dot.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
index 643ee1b76b..e338f55f72 100644
--- a/packages/frontend/src/scripts/favicon-dot.ts
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -1,3 +1,5 @@
+import tinycolor from 'tinycolor2';
+
 class FavIconDot {
 	canvas: HTMLCanvasElement;
 	src: string | null = null;
@@ -70,7 +72,8 @@ class FavIconDot {
 		if (!this.ctx || !this.faviconImage) return;
 		this.ctx.beginPath();
 		this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI);
-		this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator');
+		const computedStyle = getComputedStyle(document.documentElement);
+		this.ctx.fillStyle = tinycolor(computedStyle.getPropertyValue('--navIndicator')).toHexString();
 		this.ctx.strokeStyle = 'white';
 		this.ctx.fill();
 		this.ctx.stroke();

From 3f73251df5967ad47f61f1a6541f9d5ff6563f72 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Sun, 7 Apr 2024 17:49:20 +0100
Subject: [PATCH 18/31] allow computed `offsetMode` in `MkPagination` - #490

---
 packages/frontend/src/components/MkPagination.vue | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 6f6007d432..9a324849e2 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -73,7 +73,7 @@ export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints>
 	 */
 	reversed?: boolean;
 
-	offsetMode?: boolean;
+	offsetMode?: boolean | ComputedRef<boolean>;
 
 	pageEl?: HTMLElement;
 };
@@ -240,10 +240,11 @@ const fetchMore = async (): Promise<void> => {
 	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 	moreFetching.value = true;
 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+	const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false;
 	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 		...params,
 		limit: SECOND_FETCH_LIMIT,
-		...(props.pagination.offsetMode ? {
+		...(offsetMode ? {
 			offset: offset.value,
 		} : {
 			untilId: Array.from(items.value.keys()).at(-1),
@@ -304,10 +305,11 @@ const fetchMoreAhead = async (): Promise<void> => {
 	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 	moreFetching.value = true;
 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+	const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false;
 	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 		...params,
 		limit: SECOND_FETCH_LIMIT,
-		...(props.pagination.offsetMode ? {
+		...(offsetMode ? {
 			offset: offset.value,
 		} : {
 			sinceId: Array.from(items.value.keys()).at(-1),

From 9d91196344ce359fd27dabd778d4d084d1fc6c5e Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Sun, 7 Apr 2024 17:49:53 +0100
Subject: [PATCH 19/31] allow `offset` in `admin/emoji/list` - #490

also, use `skip` + `take` instead of `limit` (the TypeORM docs say so
https://github.com/typeorm/typeorm/blob/master/docs/select-query-builder.md#adding-limit-expression )
---
 .../backend/src/server/api/endpoints/admin/emoji/list.ts   | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 5e21111f9f..f35a6667f4 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -66,6 +66,7 @@ export const paramDef = {
 	properties: {
 		query: { type: 'string', nullable: true, default: null },
 		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+		offset: { type: 'integer', minimum: 1, nullable: true, default: null },
 		sinceId: { type: 'string', format: 'misskey:id' },
 		untilId: { type: 'string', format: 'misskey:id' },
 	},
@@ -91,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
 				//const emojis = await q.limit(ps.limit).getMany();
 
-				emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany();
+				emojis = await q.orderBy('length(emoji.name)', 'ASC').addOrderBy('id', 'DESC').getMany();
 				const queryarry = ps.query.match(/:([\p{Letter}\p{Number}\p{Mark}_+-]*):/ug);
 
 				if (queryarry) {
@@ -105,9 +106,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 						emoji.aliases.some(a => a.includes(queryNfc)) ||
 						emoji.category?.includes(queryNfc));
 				}
-				emojis.splice(ps.limit + 1);
+				emojis = emojis.slice((ps.offset ?? 0), ((ps.offset ?? 0) + ps.limit));
 			} else {
-				emojis = await q.limit(ps.limit).getMany();
+				emojis = await q.take(ps.limit).skip(ps.offset ?? 0).getMany();
 			}
 
 			return this.emojiEntityService.packDetailedMany(emojis);

From a676b0ee6178ad45370864195afa09c87d865265 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Sun, 7 Apr 2024 17:50:53 +0100
Subject: [PATCH 20/31] paginate in offset mode when querying emoji - fixes
 #490

since the backend sorts emojis by name length when a query is present,
the normal pagination with `sinceId` / `untilId` would not work
reliably

`offsetMode` is better in this case, although it will produce
non-stable results if custom emojis that match the query are added or
removed while we paginate
---
 packages/frontend/src/pages/custom-emojis-manager.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 1f9a99d4f5..9357735c82 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -98,6 +98,9 @@ const selectedEmojis = ref<string[]>([]);
 const pagination = {
 	endpoint: 'admin/emoji/list' as const,
 	limit: 30,
+	offsetMode: computed(() => (
+		(query.value && query.value !== '') ? true : false
+	)),
 	params: computed(() => ({
 		query: (query.value && query.value !== '') ? query.value : null,
 	})),

From 42d9da161b56d38a04fb4f25c7d063bdea880ff0 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Tue, 14 May 2024 16:58:06 +0100
Subject: [PATCH 21/31] first basic protection - #524

---
 packages/backend/src/core/NoteCreateService.ts | 8 ++++++++
 packages/backend/src/core/NoteEditService.ts   | 4 ++++
 2 files changed, 12 insertions(+)

diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 631d7074bd..d51315f71f 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -627,6 +627,14 @@ export class NoteCreateService implements OnApplicationShutdown {
 			userHost: user.host,
 		});
 
+		// should really not happen, but better safe than sorry
+		if (data.reply?.id === insert.id) {
+			throw new Error("A note can't reply to itself");
+		}
+		if (data.renote?.id === insert.id) {
+			throw new Error("A note can't renote itself");
+		}
+
 		if (data.uri != null) insert.uri = data.uri;
 		if (data.url != null) insert.url = data.url;
 
diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts
index 72fc01ae3b..435f5f017a 100644
--- a/packages/backend/src/core/NoteEditService.ts
+++ b/packages/backend/src/core/NoteEditService.ts
@@ -299,6 +299,10 @@ export class NoteEditService implements OnApplicationShutdown {
 		}
 
 		if (data.renote) {
+			if (data.renote.id === oldnote.id) {
+				throw new Error("A note can't renote itself");
+			}
+
 			switch (data.renote.visibility) {
 				case 'public':
 					// public noteは無条件にrenote可能

From 2d89b08a0840c6554f4a8af47fbd1f8ed5ea76d3 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Wed, 15 May 2024 16:47:06 +0100
Subject: [PATCH 22/31] use the current resolver for quotes - #524

this might solve the loop problem, if the protection already in place
for replies was enough
---
 packages/backend/src/core/activitypub/models/ApNoteService.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 6d9dc86c16..ad7b3b145c 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -251,7 +251,7 @@ export class ApNoteService {
 			> => {
 				if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
 				try {
-					const res = await this.resolveNote(uri);
+					const res = await this.resolveNote(uri, { resolver });
 					if (res == null) return { status: 'permerror' };
 					return { status: 'ok', res };
 				} catch (e) {
@@ -478,7 +478,7 @@ export class ApNoteService {
 			> => {
 				if (!/^https?:/.test(uri)) return { status: 'permerror' };
 				try {
-					const res = await this.resolveNote(uri);
+					const res = await this.resolveNote(uri, { resolver });
 					if (res == null) return { status: 'permerror' };
 					return { status: 'ok', res };
 				} catch (e) {

From 189c26aa25fa995a57e682c4bb64d83fa0c1d28a Mon Sep 17 00:00:00 2001
From: Sugar <sugar@sylveon.social>
Date: Thu, 30 May 2024 16:01:29 +0200
Subject: [PATCH 23/31] escape \ character in sqlLikeEscape

---
 packages/backend/src/misc/sql-like-escape.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts
index 0c05255674..ffe61670ee 100644
--- a/packages/backend/src/misc/sql-like-escape.ts
+++ b/packages/backend/src/misc/sql-like-escape.ts
@@ -4,5 +4,5 @@
  */
 
 export function sqlLikeEscape(s: string) {
-	return s.replace(/([%_])/g, '\\$1');
+	return s.replace(/([%_\\])/g, '\\$1');
 }

From 145c4ba132e765a07c1800282e7d7adb6921afb6 Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+ChaoticLeah@users.noreply.github.com>
Date: Fri, 31 May 2024 12:19:18 +0200
Subject: [PATCH 24/31] fixed the search url

---
 packages/backend/src/core/MfmService.ts | 284 ++++++++++++------------
 1 file changed, 142 insertions(+), 142 deletions(-)

diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index b7c9064cef..c1dd27fe99 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -480,174 +480,174 @@ export class MfmService {
 		const handlers: {
             [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any;
     } = {
-			async bold(node) {
-				const el = doc.createElement('span');
-				el.textContent = '**';
-				await appendChildren(node.children, el);
-				el.textContent += '**';
-				return el;
-			},
+    	async bold(node) {
+    		const el = doc.createElement('span');
+    		el.textContent = '**';
+    		await appendChildren(node.children, el);
+    		el.textContent += '**';
+    		return el;
+    	},
 
-			async small(node) {
-				const el = doc.createElement('small');
-				await appendChildren(node.children, el);
-				return el;
-			},
+    	async small(node) {
+    		const el = doc.createElement('small');
+    		await appendChildren(node.children, el);
+    		return el;
+    	},
 
-			async strike(node) {
-				const el = doc.createElement('span');
-				el.textContent = '~~';
-				await appendChildren(node.children, el);
-				el.textContent += '~~';
-				return el;
-			},
+    	async strike(node) {
+    		const el = doc.createElement('span');
+    		el.textContent = '~~';
+    		await appendChildren(node.children, el);
+    		el.textContent += '~~';
+    		return el;
+    	},
 
-			async italic(node) {
-				const el = doc.createElement('span');
-				el.textContent = '*';
-				await appendChildren(node.children, el);
-				el.textContent += '*';
-				return el;
-			},
+    	async italic(node) {
+    		const el = doc.createElement('span');
+    		el.textContent = '*';
+    		await appendChildren(node.children, el);
+    		el.textContent += '*';
+    		return el;
+    	},
 
-			async fn(node) {
-				const el = doc.createElement('span');
-				el.textContent = '*';
-				await appendChildren(node.children, el);
-				el.textContent += '*';
-				return el;
-			},
+    	async fn(node) {
+    		const el = doc.createElement('span');
+    		el.textContent = '*';
+    		await appendChildren(node.children, el);
+    		el.textContent += '*';
+    		return el;
+    	},
 
-			blockCode(node) {
-				const pre = doc.createElement('pre');
-				const inner = doc.createElement('code');
+    	blockCode(node) {
+    		const pre = doc.createElement('pre');
+    		const inner = doc.createElement('code');
 
-				const nodes = node.props.code
-					.split(/\r\n|\r|\n/)
-					.map((x) => doc.createTextNode(x));
+    		const nodes = node.props.code
+    			.split(/\r\n|\r|\n/)
+    			.map((x) => doc.createTextNode(x));
 
-				for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
-					inner.appendChild(x === 'br' ? doc.createElement('br') : x);
-				}
+    		for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
+    			inner.appendChild(x === 'br' ? doc.createElement('br') : x);
+    		}
 
-				pre.appendChild(inner);
-				return pre;
-			},
+    		pre.appendChild(inner);
+    		return pre;
+    	},
 
-			async center(node) {
-				const el = doc.createElement('div');
-				await appendChildren(node.children, el);
-				return el;
-			},
+    	async center(node) {
+    		const el = doc.createElement('div');
+    		await appendChildren(node.children, el);
+    		return el;
+    	},
 
-			emojiCode(node) {
-				return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
-			},
+    	emojiCode(node) {
+    		return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
+    	},
 
-			unicodeEmoji(node) {
-				return doc.createTextNode(node.props.emoji);
-			},
+    	unicodeEmoji(node) {
+    		return doc.createTextNode(node.props.emoji);
+    	},
 
-			hashtag: (node) => {
-				const a = doc.createElement('a');
-				a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
-				a.textContent = `#${node.props.hashtag}`;
-				a.setAttribute('rel', 'tag');
-				a.setAttribute('class', 'hashtag');
-				return a;
-			},
+    	hashtag: (node) => {
+    		const a = doc.createElement('a');
+    		a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
+    		a.textContent = `#${node.props.hashtag}`;
+    		a.setAttribute('rel', 'tag');
+    		a.setAttribute('class', 'hashtag');
+    		return a;
+    	},
 
-			inlineCode(node) {
-				const el = doc.createElement('code');
-				el.textContent = node.props.code;
-				return el;
-			},
+    	inlineCode(node) {
+    		const el = doc.createElement('code');
+    		el.textContent = node.props.code;
+    		return el;
+    	},
 
-			mathInline(node) {
-				const el = doc.createElement('code');
-				el.textContent = node.props.formula;
-				return el;
-			},
+    	mathInline(node) {
+    		const el = doc.createElement('code');
+    		el.textContent = node.props.formula;
+    		return el;
+    	},
 
-			mathBlock(node) {
-				const el = doc.createElement('code');
-				el.textContent = node.props.formula;
-				return el;
-			},
+    	mathBlock(node) {
+    		const el = doc.createElement('code');
+    		el.textContent = node.props.formula;
+    		return el;
+    	},
 
-			async link(node) {
-				const a = doc.createElement('a');
-				a.setAttribute('rel', 'nofollow noopener noreferrer');
-				a.setAttribute('target', '_blank');
-				a.setAttribute('href', node.props.url);
-				await appendChildren(node.children, a);
-				return a;
-			},
+    	async link(node) {
+    		const a = doc.createElement('a');
+    		a.setAttribute('rel', 'nofollow noopener noreferrer');
+    		a.setAttribute('target', '_blank');
+    		a.setAttribute('href', node.props.url);
+    		await appendChildren(node.children, a);
+    		return a;
+    	},
 
-			async mention(node) {
-				const { username, host, acct } = node.props;
-				const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
+    	async mention(node) {
+    		const { username, host, acct } = node.props;
+    		const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
 
-				const el = doc.createElement('span');
-				if (!resolved) {
-					el.textContent = acct;
-				} else {
-					el.setAttribute('class', 'h-card');
-					el.setAttribute('translate', 'no');
-					const a = doc.createElement('a');
-					a.setAttribute('href', resolved.url ? resolved.url : resolved.uri);
-					a.className = 'u-url mention';
-					const span = doc.createElement('span');
-					span.textContent = resolved.username || username;
-					a.textContent = '@';
-					a.appendChild(span);
-					el.appendChild(a);
-				}
+    		const el = doc.createElement('span');
+    		if (!resolved) {
+    			el.textContent = acct;
+    		} else {
+    			el.setAttribute('class', 'h-card');
+    			el.setAttribute('translate', 'no');
+    			const a = doc.createElement('a');
+    			a.setAttribute('href', resolved.url ? resolved.url : resolved.uri);
+    			a.className = 'u-url mention';
+    			const span = doc.createElement('span');
+    			span.textContent = resolved.username || username;
+    			a.textContent = '@';
+    			a.appendChild(span);
+    			el.appendChild(a);
+    		}
 
-				return el;
-			},
+    		return el;
+    	},
 
-			async quote(node) {
-				const el = doc.createElement('blockquote');
-				await appendChildren(node.children, el);
-				return el;
-			},
+    	async quote(node) {
+    		const el = doc.createElement('blockquote');
+    		await appendChildren(node.children, el);
+    		return el;
+    	},
 
-			text(node) {
-				const el = doc.createElement('span');
-				const nodes = node.props.text
-					.split(/\r\n|\r|\n/)
-					.map((x) => doc.createTextNode(x));
+    	text(node) {
+    		const el = doc.createElement('span');
+    		const nodes = node.props.text
+    			.split(/\r\n|\r|\n/)
+    			.map((x) => doc.createTextNode(x));
 
-				for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
-					el.appendChild(x === 'br' ? doc.createElement('br') : x);
-				}
+    		for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
+    			el.appendChild(x === 'br' ? doc.createElement('br') : x);
+    		}
 
-				return el;
-			},
+    		return el;
+    	},
 
-			url(node) {
-				const a = doc.createElement('a');
-				a.setAttribute('rel', 'nofollow noopener noreferrer');
-				a.setAttribute('target', '_blank');
-				a.setAttribute('href', node.props.url);
-				a.textContent = node.props.url.replace(/^https?:\/\//, '');
-				return a;
-			},
+    	url(node) {
+    		const a = doc.createElement('a');
+    		a.setAttribute('rel', 'nofollow noopener noreferrer');
+    		a.setAttribute('target', '_blank');
+    		a.setAttribute('href', node.props.url);
+    		a.textContent = node.props.url.replace(/^https?:\/\//, '');
+    		return a;
+    	},
 
-			search: (node) => {
-				const a = doc.createElement('a');
-				a.setAttribute('href', `https"google.com/${node.props.query}`);
-				a.textContent = node.props.content;
-				return a;
-			},
+    	search: (node) => {
+    		const a = doc.createElement('a');
+    		a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
+    		a.textContent = node.props.content;
+    		return a;
+    	},
 
-			async plain(node) {
-				const el = doc.createElement('span');
-				await appendChildren(node.children, el);
-				return el;
-			},
-		};
+    	async plain(node) {
+    		const el = doc.createElement('span');
+    		await appendChildren(node.children, el);
+    		return el;
+    	},
+    };
 
 		await appendChildren(nodes, doc.body);
 

From 1656c02536108e9566ace58809ddc86124b1bfdf Mon Sep 17 00:00:00 2001
From: KevinWh0 <45321184+ChaoticLeah@users.noreply.github.com>
Date: Fri, 31 May 2024 12:21:25 +0200
Subject: [PATCH 25/31] renamed toMastoHtml to toMastoApiHtml to clear up what
 it does

---
 packages/backend/src/core/MfmService.ts                | 4 ++--
 packages/backend/src/server/api/mastodon/converters.ts | 6 +++---
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index c1dd27fe99..819c940dc1 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -459,10 +459,10 @@ export class MfmService {
 		return `<p>${doc.body.innerHTML}</p>`;
 	}
 
-	// the toMastoHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version
+	// the toMastoApiHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version
 
 	@bindThis
-	public async toMastoHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], inline = false, quoteUri: string | null = null) {
+	public async toMastoApiHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], inline = false, quoteUri: string | null = null) {
 		if (nodes == null) {
 			return null;
 		}
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index 326d3a1d5c..6b2c991ff2 100644
--- a/packages/backend/src/server/api/mastodon/converters.ts
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -110,7 +110,7 @@ export class MastoConverters {
 	private async encodeField(f: Entity.Field): Promise<Entity.Field> {
 		return {
 			name: f.name,
-			value: await this.mfmService.toMastoHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
+			value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
 			verified_at: null,
 		};
 	}
@@ -179,7 +179,7 @@ export class MastoConverters {
 			const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
 			const item = {
 				account: noteUser,
-				content: this.mfmService.toMastoHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
+				content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
 				created_at: lastDate.toISOString(),
 				emojis: [],
 				sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
@@ -240,7 +240,7 @@ export class MastoConverters {
 		});
 
 		const content = note.text !== null
-			? quoteUri.then(quoteUri => this.mfmService.toMastoHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
+			? quoteUri.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
 				.then(p => p ?? escapeMFM(note.text!))
 			: '';
 

From 81ed73938019848e4f44ebaedcfc62af7ab88f67 Mon Sep 17 00:00:00 2001
From: Marie <marie@kaifa.ch>
Date: Fri, 31 May 2024 21:18:35 +0000
Subject: [PATCH 26/31] fix: button saying Misskey instead of Sharkey

---
 packages/frontend/src/pages/about-sharkey.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue
index 1bfd90c8f7..f020e043c6 100644
--- a/packages/frontend/src/pages/about-sharkey.vue
+++ b/packages/frontend/src/pages/about-sharkey.vue
@@ -215,7 +215,7 @@ function gravity() {
 
 function iLoveMisskey() {
 	os.post({
-		initialText: 'I $[jelly ❤] #Misskey',
+		initialText: 'I $[jelly ❤] #Sharkey',
 		instant: true,
 	});
 }

From 92cd771e0fb47eadbd37a337004be73e9ca2b3d0 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Mon, 3 Jun 2024 16:28:25 +0000
Subject: [PATCH 27/31] avoid `await` at top-level in setup - fixes frontend
 tests

---
 packages/frontend/src/index.html          |  2 +-
 packages/frontend/src/pages/user/home.vue | 30 ++++++++++++-----------
 2 files changed, 17 insertions(+), 15 deletions(-)

diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
index 54059bfaf4..40e3cdf2ab 100644
--- a/packages/frontend/src/index.html
+++ b/packages/frontend/src/index.html
@@ -22,7 +22,7 @@
 			style-src 'self' 'unsafe-inline';
 			img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com;
 			media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
-			connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
+			connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org;
 			frame-src *;"
 	/>
 	<meta property="og:site_name" content="[DEV BUILD] Misskey" />
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 96ae4824f0..4a00b204c0 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -236,22 +236,24 @@ const moderationNote = ref(props.user.moderationNote);
 const editModerationNote = ref(false);
 const noteview = ref<string | null>(null);
 
-let listenbrainzdata = false;
+const listenbrainzdata = ref(false);
 if (props.user.listenbrainz) {
-	try {
-		const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
-			method: 'GET',
-			headers: {
-				'Content-Type': 'application/json'
-			},
-		});
-		const data = await response.json();
-		if (data.payload.listens && data.payload.listens.length !== 0) {
-			listenbrainzdata = true;
+	(async function() {
+		try {
+			const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
+				method: 'GET',
+				headers: {
+					'Content-Type': 'application/json'
+				},
+			});
+			const data = await response.json();
+			if (data.payload.listens && data.payload.listens.length !== 0) {
+				listenbrainzdata.value = true;
+			}
+		} catch (err) {
+			listenbrainzdata.value = false;
 		}
-	} catch (err) {
-		listenbrainzdata = false;
-	}
+	})()
 }
 
 const background = computed(() => {

From 082e1d1afb44cc866d75c37ab5e0f7ca8701796b Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Mon, 3 Jun 2024 16:29:19 +0000
Subject: [PATCH 28/31] allow setting separate timeout / max size for imports -
 fixes #479

---
 .config/example.yml                           |  5 +++++
 packages/backend/src/config.ts                | 14 +++++++++++++
 packages/backend/src/core/DownloadService.ts  |  8 +++----
 .../processors/ImportNotesProcessorService.ts | 21 +++++++++++++------
 4 files changed, 38 insertions(+), 10 deletions(-)

diff --git a/.config/example.yml b/.config/example.yml
index c037a280b6..c82e744ee1 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -287,5 +287,10 @@ checkActivityPubGetSignature: false
 # Upload or download file size limits (bytes)
 #maxFileSize: 262144000
 
+# timeout and maximum size for imports (e.g. note imports)
+#import:
+#  downloadTimeout: 30
+#  maxFileSize: 262144000
+
 # PID File of master process
 #pidFile: /tmp/misskey.pid
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index f6ce9b3cdf..5974170a6d 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -97,6 +97,12 @@ type Source = {
 	perChannelMaxNoteCacheCount?: number;
 	perUserNotificationsMaxCount?: number;
 	deactivateAntennaThreshold?: number;
+
+	import?: {
+		downloadTimeout: number;
+		maxFileSize: number;
+	};
+
 	pidFile: string;
 };
 
@@ -177,6 +183,12 @@ export type Config = {
 	perChannelMaxNoteCacheCount: number;
 	perUserNotificationsMaxCount: number;
 	deactivateAntennaThreshold: number;
+
+	import: {
+		downloadTimeout: number;
+		maxFileSize: number;
+	} | undefined;
+
 	pidFile: string;
 };
 
@@ -284,6 +296,7 @@ export function loadConfig(): Config {
 		perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
 		perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
 		deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
+		import: config.import,
 		pidFile: config.pidFile,
 	};
 }
@@ -425,4 +438,5 @@ function applyEnvOverrides(config: Source) {
 	_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
 	_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'videoThumbnailGenerator']]);
 	_apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]);
+	_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
 }
diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts
index 21ae798f9f..83452845d4 100644
--- a/packages/backend/src/core/DownloadService.ts
+++ b/packages/backend/src/core/DownloadService.ts
@@ -35,14 +35,14 @@ export class DownloadService {
 	}
 
 	@bindThis
-	public async downloadUrl(url: string, path: string): Promise<{
+	public async downloadUrl(url: string, path: string, options: { timeout?: number, operationTimeout?: number, maxSize?: number} = {} ): Promise<{
 		filename: string;
 	}> {
 		this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`);
 
-		const timeout = 30 * 1000;
-		const operationTimeout = 60 * 1000;
-		const maxSize = this.config.maxFileSize ?? 262144000;
+		const timeout = options.timeout ?? 30 * 1000;
+		const operationTimeout = options.operationTimeout ?? 60 * 1000;
+		const maxSize = options.maxSize ?? this.config.maxFileSize ?? 262144000;
 
 		const urlObj = new URL(url);
 		let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
diff --git a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts
index 7cef858c51..58a0ea10ad 100644
--- a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts
@@ -19,12 +19,16 @@ import { IdService } from '@/core/IdService.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
 import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbNoteWithParentImportToDbJobData } from '../types.js';
+import type { Config } from '@/config.js';
 
 @Injectable()
 export class ImportNotesProcessorService {
 	private logger: Logger;
 
 	constructor(
+		@Inject(DI.config)
+		private config: Config,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -73,6 +77,11 @@ export class ImportNotesProcessorService {
 		}
 	}
 
+	@bindThis
+	private downloadUrl(url: string, path:string): Promise<{filename: string}> {
+		return this.downloadService.downloadUrl(url, path, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize });
+	}
+
 	@bindThis
 	private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise<any[]> {
 		type NotesMap = {
@@ -176,7 +185,7 @@ export class ImportNotesProcessorService {
 
 			try {
 				await fsp.writeFile(destPath, '', 'binary');
-				await this.downloadService.downloadUrl(file.url, destPath);
+				await this.downloadUrl(file.url, destPath);
 			} catch (e) { // TODO: 何度か再試行
 				if (e instanceof Error || typeof e === 'string') {
 					this.logger.error(e);
@@ -206,7 +215,7 @@ export class ImportNotesProcessorService {
 
 			try {
 				await fsp.writeFile(destPath, '', 'binary');
-				await this.downloadService.downloadUrl(file.url, destPath);
+				await this.downloadUrl(file.url, destPath);
 			} catch (e) { // TODO: 何度か再試行
 				if (e instanceof Error || typeof e === 'string') {
 					this.logger.error(e);
@@ -239,7 +248,7 @@ export class ImportNotesProcessorService {
 
 			try {
 				await fsp.writeFile(destPath, '', 'binary');
-				await this.downloadService.downloadUrl(file.url, destPath);
+				await this.downloadUrl(file.url, destPath);
 			} catch (e) { // TODO: 何度か再試行
 				if (e instanceof Error || typeof e === 'string') {
 					this.logger.error(e);
@@ -297,7 +306,7 @@ export class ImportNotesProcessorService {
 
 			try {
 				await fsp.writeFile(path, '', 'utf-8');
-				await this.downloadService.downloadUrl(file.url, path);
+				await this.downloadUrl(file.url, path);
 			} catch (e) { // TODO: 何度か再試行
 				if (e instanceof Error || typeof e === 'string') {
 					this.logger.error(e);
@@ -349,7 +358,7 @@ export class ImportNotesProcessorService {
 
 				if (!exists) {
 					try {
-						await this.downloadService.downloadUrl(file.url, filePath);
+						await this.downloadUrl(file.url, filePath);
 					} catch (e) { // TODO: 何度か再試行
 						this.logger.error(e instanceof Error ? e : new Error(e as string));
 					}
@@ -488,7 +497,7 @@ export class ImportNotesProcessorService {
 
 				if (!exists) {
 					try {
-						await this.downloadService.downloadUrl(file.url, filePath);
+						await this.downloadUrl(file.url, filePath);
 					} catch (e) { // TODO: 何度か再試行
 						this.logger.error(e instanceof Error ? e : new Error(e as string));
 					}

From cb43994841935c6512c530c96ee409ffbd9f00f8 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Fri, 7 Jun 2024 11:40:44 +0100
Subject: [PATCH 29/31] only allow a single boost via hotkey - fixes #467 #468
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

As usual, have to write the same thing four times ☹

The parentheses around `q` tell the hotkey/keymap code to ignore
auto-repeat events (which works fine in Chrome but not in Firefox, I
reported the bug https://bugzilla.mozilla.org/show_bug.cgi?id=1900397
)

In addition, I've added a guard variable that is true while calling
the backend to boost/renote, and false otherwise. This way, even in
Firefox we don't spam-boost. Unboosting is still *only with the
mouse*, I have not added that functionality.
---
 packages/frontend/src/components/MkNote.vue         | 8 +++++---
 packages/frontend/src/components/MkNoteDetailed.vue | 8 +++++---
 packages/frontend/src/components/SkNote.vue         | 8 +++++---
 packages/frontend/src/components/SkNoteDetailed.vue | 8 +++++---
 4 files changed, 20 insertions(+), 12 deletions(-)

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 9a667c3118..a1d241690f 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -328,10 +328,12 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
 	return false;
 }
 
+let renoting = false;
+
 const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
-	'q': () => renote(appearNote.value.visibility),
+	'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
 	'up|k|shift+tab': focusBefore,
 	'down|j|tab': focusAfter,
 	'esc': blur,
@@ -436,7 +438,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			});
+			}).then(() => { renoting = false });
 		}
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
@@ -455,7 +457,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			});
+			}).then(() => renoting = false);
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 3d15f69f73..ec3c3ea3dc 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -336,10 +336,12 @@ if ($i) {
 	});
 }
 
+let renoting = false;
+
 const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
-	'q': () => renote(appearNote.value.visibility),
+	'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
 	'esc': blur,
 	'm|o': () => showMenu(true),
 	's': () => showContent.value !== showContent.value,
@@ -457,7 +459,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		});
+		}).then(() => { renoting = false });
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
 		if (el) {
@@ -474,7 +476,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		});
+		}).then(() => { renoting = false });
 	}
 }
 
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index 09decad1a2..fe14b1eafd 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -329,10 +329,12 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
 	return false;
 }
 
+let renoting = false;
+
 const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
-	'q': () => renote(appearNote.value.visibility),
+	'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
 	'up|k|shift+tab': focusBefore,
 	'down|j|tab': focusAfter,
 	'esc': blur,
@@ -437,7 +439,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			});
+			}).then(() => { renoting = false });
 		}
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
@@ -456,7 +458,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			});
+			}).then(() => { renoting = false });
 		}
 	}
 }
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index ced7e7a176..084c46ddcd 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -345,10 +345,12 @@ if ($i) {
 	});
 }
 
+let renoting = false;
+
 const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
-	'q': () => renote(appearNote.value.visibility),
+	'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
 	'esc': blur,
 	'm|o': () => showMenu(true),
 	's': () => showContent.value !== showContent.value,
@@ -466,7 +468,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		});
+		}).then(() => { renoting = false });
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
 		if (el) {
@@ -483,7 +485,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		});
+		}).then(() => { renoting = false });
 	}
 }
 

From 388926775ec14e58bf9daf447c28d197f925ee60 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Fri, 7 Jun 2024 14:47:16 +0100
Subject: [PATCH 30/31] I meant `finally`, not `then`, thanks fEmber

---
 packages/frontend/src/components/MkNote.vue         | 4 ++--
 packages/frontend/src/components/MkNoteDetailed.vue | 4 ++--
 packages/frontend/src/components/SkNote.vue         | 4 ++--
 packages/frontend/src/components/SkNoteDetailed.vue | 4 ++--
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index a1d241690f..556fedbc30 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -438,7 +438,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			}).then(() => { renoting = false });
+			}).finally(() => { renoting = false });
 		}
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
@@ -457,7 +457,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			}).then(() => renoting = false);
+			}).finally(() => renoting = false);
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index ec3c3ea3dc..ca017bf63b 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -459,7 +459,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		}).then(() => { renoting = false });
+		}).finally(() => { renoting = false });
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
 		if (el) {
@@ -476,7 +476,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		}).then(() => { renoting = false });
+		}).finally(() => { renoting = false });
 	}
 }
 
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index fe14b1eafd..3291390682 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -439,7 +439,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			}).then(() => { renoting = false });
+			}).finally(() => { renoting = false });
 		}
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
@@ -458,7 +458,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			}).then(() => { renoting = false });
+			}).finally(() => { renoting = false });
 		}
 	}
 }
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 084c46ddcd..1acd177e87 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -468,7 +468,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		}).then(() => { renoting = false });
+		}).finally(() => { renoting = false });
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
 		if (el) {
@@ -485,7 +485,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		}).then(() => { renoting = false });
+		}).finally(() => { renoting = false });
 	}
 }
 

From 079abfd7135e88562127b06135b0eb6aabee81b0 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Fri, 7 Jun 2024 15:10:16 +0100
Subject: [PATCH 31/31] rate limit note/reply/boost creation more tightly

5/minute is the same as 300/hour on average, and still high enough
that it shouldn't be a problem for most people
---
 packages/backend/src/server/api/endpoints/notes/create.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 95ebda2f21..cc44721133 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -30,8 +30,8 @@ export const meta = {
 	prohibitMoved: true,
 
 	limit: {
-		duration: ms('1hour'),
-		max: 300,
+		duration: ms('1minute'),
+		max: 5,
 	},
 
 	kind: 'write:notes',