diff --git a/.config/example.yml b/.config/example.yml
index 9126bdfd91..ae55b983bb 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -302,5 +302,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/locales/en-US.yml b/locales/en-US.yml
index ffebe5fbec..44f90c8d47 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -694,6 +694,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 dfcf6b6689..d300ed42a3 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2792,6 +2792,10 @@ export interface Locale extends ILocale {
      * 表示する通知の種別を選択してください。
      */
     "notificationSettingDesc": string;
+    /**
+     * ファビコン通知ドットを有効にする
+     */
+    "enableFaviconNotificationDot": string;
     /**
      * グローバル設定を使う
      */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index f12c1ab4fc..4d214b652a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -694,6 +694,7 @@ channel: "チャンネル"
 create: "作成"
 notificationSetting: "通知設定"
 notificationSettingDesc: "表示する通知の種別を選択してください。"
+enableFaviconNotificationDot: "ファビコン通知ドットを有効にする"
 useGlobalSetting: "グローバル設定を使う"
 useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
 other: "その他"
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 346a557195..58c4d028aa 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -100,6 +100,12 @@ type Source = {
 	perChannelMaxNoteCacheCount?: number;
 	perUserNotificationsMaxCount?: number;
 	deactivateAntennaThreshold?: number;
+
+	import?: {
+		downloadTimeout: number;
+		maxFileSize: number;
+	};
+
 	pidFile: string;
 };
 
@@ -182,6 +188,12 @@ export type Config = {
 	perChannelMaxNoteCacheCount: number;
 	perUserNotificationsMaxCount: number;
 	deactivateAntennaThreshold: number;
+
+	import: {
+		downloadTimeout: number;
+		maxFileSize: number;
+	} | undefined;
+
 	pidFile: string;
 };
 
@@ -291,6 +303,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,
 	};
 }
@@ -436,4 +449,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/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 461cb2b6a9..30009c1229 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -464,10 +464,10 @@ export class MfmService {
 		return new XMLSerializer().serializeToString(body);
 	}
 
-	// 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;
 		}
@@ -485,174 +485,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);
 
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 4ff0d0fbef..41efa76f3f 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 e28299316d..0cb58d04a2 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 (this.isRenote(data)) {
+			if (data.renote.id === oldnote.id) {
+				throw new Error("A note can't renote itself");
+			}
+
 			switch (data.renote.visibility) {
 				case 'public':
 					// public noteは無条件にrenote可能
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index cad1af02e5..4827baad84 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -248,7 +248,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) {
@@ -473,7 +473,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) {
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');
 }
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));
 					}
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);
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 296698522d..5d2895cd46 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',
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index ea219b933d..cca9505846 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!))
 			: '';
 
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 89c950e642..2d1f3d8d44 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -334,10 +334,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,
@@ -464,7 +466,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			});
+			}).finally(() => { renoting = false });
 		}
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
@@ -483,7 +485,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			});
+			}).finally(() => renoting = false);
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index f17c2b542d..8de121b8de 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -346,10 +346,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,
@@ -489,7 +491,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		});
+		}).finally(() => { renoting = false });
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
 		if (el) {
@@ -506,7 +508,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		});
+		}).finally(() => { renoting = false });
 	}
 }
 
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),
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index c6631fe1f4..cfaaeecc34 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">
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index 1a8207e6c6..9e744fcac8 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -335,10 +335,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,
@@ -465,7 +467,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			});
+			}).finally(() => { renoting = false });
 		}
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
@@ -484,7 +486,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
 				renoted.value = true;
-			});
+			}).finally(() => { renoting = false });
 		}
 	}
 }
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 0bf096fe34..70d4704b88 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -355,10 +355,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,
@@ -498,7 +500,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		});
+		}).finally(() => { renoting = false });
 	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 		const el = renoteButton.value as HTMLElement | null | undefined;
 		if (el) {
@@ -515,7 +517,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
-		});
+		}).finally(() => { renoting = false });
 	}
 }
 
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index a21cd9477e..a5effb65ba 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -393,67 +393,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)))];
 				}
 			}
 
@@ -497,17 +497,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': {
@@ -530,8 +530,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 		}
 	}).flat(Infinity) as (VNode | string)[];
 
-	return h('span', {
+	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/index.html b/packages/frontend/src/index.html
index 51a9cdce79..733116b75f 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/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,
 	});
 }
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,
 	})),
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 3b4f000e61..28637228d0 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -117,6 +117,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>
@@ -353,6 +355,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 c774ab5367..5390f12239 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -73,6 +73,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'showReactionsCount',
 	'loadRawImages',
 	'warnMissingAltText',
+	'enableFaviconNotificationDot',
 	'imageNewTab',
 	'dataSaver',
 	'disableShowingAnimatedImages',
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(() => {
diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts
new file mode 100644
index 0000000000..e338f55f72
--- /dev/null
+++ b/packages/frontend/src/scripts/favicon-dot.ts
@@ -0,0 +1,114 @@
+import tinycolor from 'tinycolor2';
+
+class FavIconDot {
+	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
+	 */
+	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.faviconImage = document.createElement('img');
+	
+		this.hasLoaded = new Promise((resolve, reject) => {
+			(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).addEventListener('error', () => {
+				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('link[rel=icon]') ?? this.createFaviconElem()) as HTMLLinkElement;
+			favicon.addEventListener('load', () => {
+				resolve(favicon);
+			});
+
+			favicon.onerror = () => {
+				reject('Failed to load favicon');
+			};
+			resolve(favicon);
+		});
+	}
+
+	private createFaviconElem() {
+		const newLink = document.createElement('link');
+		newLink.setAttribute('rel', 'icon');
+		newLink.setAttribute('href', '/favicon.ico');
+		newLink.setAttribute('type', 'image/x-icon');
+
+		document.head.appendChild(newLink);
+		return newLink;
+	}
+
+	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() {
+		if (!this.ctx || !this.faviconImage) return;
+		this.ctx.beginPath();
+		this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI);
+		const computedStyle = getComputedStyle(document.documentElement);
+		this.ctx.fillStyle = tinycolor(computedStyle.getPropertyValue('--navIndicator')).toHexString();
+		this.ctx.strokeStyle = 'white';
+		this.ctx.fill();
+		this.ctx.stroke();
+	}
+
+	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
+		await this.hasLoaded;
+		this.drawIcon();
+		if (isVisible) this.drawDot();
+		this.setFavicon();
+	}
+}
+
+let icon: FavIconDot | undefined = undefined;
+
+export function setFavIconDot(visible: boolean) {
+	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.addEventListener('load', setIconVisibility);
+	}
+}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 574e98b022..f3f6746b62 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -271,6 +271,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/style.scss b/packages/frontend/src/style.scss
index 057a4fb61e..f77739fdbc 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -565,6 +565,8 @@ html[data-color-mode=dark] ._woodenFrame {
 
 // MFM -----------------------------
 
+div > bdi, p > bdi { display: block }
+
 ._mfm_blur_ {
 	filter: blur(6px);
 	transition: filter 0.3s;
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 4fe53ae6a3..b1fe8e54fc 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -47,8 +47,9 @@ 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 { setFavIconDot } from '../../scripts/favicon-dot';
 import { swInject } from './sw-inject.js';
 import XNotification from './notification.vue';
 import { popups } from '@/os.js';
@@ -93,6 +94,12 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
 if ($i) {
 	const connection = useStream().useChannel('main', null, 'UI');
 	connection.on('notification', onNotification);
+
+	// For the favicon notification dot
+	watch(() => $i?.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot, (hasAny) => setFavIconDot(hasAny as boolean));
+
+	if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true);
+	
 	globalEvents.on('clientNotification', notification => onNotification(notification, true));
 
 	//#region Listen message from SW