From 4b8b413c5170d7bcf9b37eb9bef2511f0ceb2b32 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Mon, 6 Feb 2023 15:52:00 -0500
Subject: [PATCH 01/96] =?UTF-8?q?Improving=20keyboard=20support,=20part=20?=
 =?UTF-8?q?1=E2=84=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/client/src/components/MkButton.vue   |  3 +-
 packages/client/src/components/form/radio.vue |  3 ++
 .../client/src/components/form/switch.vue     |  3 ++
 packages/client/src/directives/tooltip.ts     | 33 ++++++++++++-------
 packages/client/src/pages/admin/_header_.vue  |  6 +---
 packages/client/src/style.scss                |  4 ---
 packages/client/src/ui/_common_/navbar.vue    | 13 ++++++--
 7 files changed, 39 insertions(+), 26 deletions(-)

diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue
index 9042500b4..5220e8c95 100644
--- a/packages/client/src/components/MkButton.vue
+++ b/packages/client/src/components/MkButton.vue
@@ -184,8 +184,7 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&:focus-visible {
-		outline: solid 2px var(--focus);
-		outline-offset: 2px;
+		outline: auto;
 	}
 
 	&.inline {
diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue
index b36f7e9fd..406038582 100644
--- a/packages/client/src/components/form/radio.vue
+++ b/packages/client/src/components/form/radio.vue
@@ -68,6 +68,9 @@ function toggle(): void {
 	&:hover {
 		border-color: var(--inputBorderHover) !important;
 	}
+	&:focus-within {
+		outline: auto;
+	}
 
 	&.checked {
 		background-color: var(--accentedBg) !important;
diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue
index 1ed00ae65..ef717ab6f 100644
--- a/packages/client/src/components/form/switch.vue
+++ b/packages/client/src/components/form/switch.vue
@@ -98,6 +98,9 @@ const toggle = () => {
 			border-color: var(--inputBorderHover) !important;
 		}
 	}
+	&:focus-within > .button {
+		outline: auto;
+	}
 
 	> .label {
 		margin-left: 12px;
diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
index 7738d14e8..91024a6e3 100644
--- a/packages/client/src/directives/tooltip.ts
+++ b/packages/client/src/directives/tooltip.ts
@@ -76,23 +76,32 @@ export default {
 			ev.preventDefault();
 		});
 
+		function showTooltip() {
+			window.clearTimeout(self.showTimer);
+			window.clearTimeout(self.hideTimer);
+			self.showTimer = window.setTimeout(self.show, delay);
+		}
+		function hideTooltip() {
+			window.clearTimeout(self.showTimer);
+			window.clearTimeout(self.hideTimer);
+			self.hideTimer = window.setTimeout(self.close, delay);
+		}
+
 		el.addEventListener(
-			start,
-			() => {
-				window.clearTimeout(self.showTimer);
-				window.clearTimeout(self.hideTimer);
-				self.showTimer = window.setTimeout(self.show, delay);
-			},
+			start, showTooltip,
+			{ passive: true },
+		);
+		el.addEventListener(
+			"focusin", showTooltip,
 			{ passive: true },
 		);
 
 		el.addEventListener(
-			end,
-			() => {
-				window.clearTimeout(self.showTimer);
-				window.clearTimeout(self.hideTimer);
-				self.hideTimer = window.setTimeout(self.close, delay);
-			},
+			end, hideTooltip,
+			{ passive: true },
+		);
+		el.addEventListener(
+			"focusout", hideTooltip,
 			{ passive: true },
 		);
 
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index bdb41b2d2..12702790e 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -265,11 +265,7 @@ onUnmounted(() => {
 			font-weight: normal;
 			opacity: 0.7;
 
-			&:hover {
-				opacity: 1;
-			}
-
-			&.active {
+			&:hover, &:focus-visible, &.active {
 				opacity: 1;
 			}
 
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index ca8bd8b43..01bff888a 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -178,10 +178,6 @@ hr {
 		pointer-events: none;
 	}
 
-	&:focus-visible {
-		outline: none;
-	}
-
 	&:disabled {
 		opacity: 0.5;
 		cursor: default;
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index c44c766b3..e99656ed6 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -341,8 +341,6 @@ function more(ev: MouseEvent) {
 					padding-left: 30px;
 					line-height: 2.85rem;
 					margin-bottom: 0.5rem;
-					text-overflow: ellipsis;
-					overflow: hidden;
 					white-space: nowrap;
 					width: 100%;
 					text-align: left;
@@ -368,6 +366,8 @@ function more(ev: MouseEvent) {
 					> .text {
 						position: relative;
 						font-size: 0.9em;
+						overflow: hidden;
+						text-overflow: ellipsis;
 					}
 
 					&:hover {
@@ -380,7 +380,7 @@ function more(ev: MouseEvent) {
 						color: var(--navActive);
 					}
 
-					&:hover, &.active {
+					&:hover, &:focus-within, &.active {
 						color: var(--accent);
 						transition: all 0.4s ease;
 
@@ -562,5 +562,12 @@ function more(ev: MouseEvent) {
 			}
 		}
 	}
+
+	.item {
+		outline: none;
+		&:focus-visible:before {
+			outline: auto;
+		}
+	}
 }
 </style>

From 2c4881dc94406b4696f8e5572cd15c3297bf112b Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Mon, 6 Feb 2023 23:23:56 -0500
Subject: [PATCH 02/96] Fix tabbing orders & allow closing modals w/ esc

---
 packages/client/src/components/MkMenu.vue      | 14 +++++++-------
 packages/client/src/components/MkModal.vue     |  8 ++++++--
 packages/client/src/components/MkPopupMenu.vue |  2 +-
 packages/client/src/components/MkSuperMenu.vue |  6 +++---
 4 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 78c1ff223..c9f8dbfd6 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -12,33 +12,33 @@
 			<span v-else-if="item.type === 'label'" class="label item">
 				<span>{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
+			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</a>
-			<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else-if="item.type === 'user'" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
+			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
 			</button>
-			<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index ed0e7f59d..62f67a380 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -1,8 +1,8 @@
 <template>
-<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
+<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
+		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1">
 			<slot :max-height="maxHeight" :type="type"></slot>
 		</div>
 	</div>
@@ -214,6 +214,7 @@ const align = () => {
 const onOpened = () => {
 	emit('opened');
 
+	content?.focus()
 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 	const el = content!.children[0];
 	el.addEventListener('mousedown', ev => {
@@ -328,6 +329,9 @@ defineExpose({
 }
 
 .qzhlnise {
+	> .content {
+		border-radius: 16px;
+	}
 	> .bg {
 		&.transparent {
 			background: transparent;
diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index f04c7f561..95234d7ce 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')" tabindex="-1">
 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
 </MkModal>
 </template>
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index 6e73b4959..b784d5239 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -5,15 +5,15 @@
 
 		<div class="items">
 			<template v-for="(item, i) in group.items">
-				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
 					<i v-if="item.icon" class="icon ph-fw ph-lg" :class="item.icon"></i>
 					<span class="text">{{ item.text }}</span>
 				</a>
-				<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
+				<button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
 					<i v-if="item.icon" class="icon ph-fw ph-lg" :class="item.icon"></i>
 					<span class="text">{{ item.text }}</span>
 				</button>
-				<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+				<MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
 					<i v-if="item.icon" class="icon ph-fw ph-lg" :class="item.icon"></i>
 					<span class="text">{{ item.text }}</span>
 				</MkA>

From b8517719af16078bc5003718d3d2a832cdb2b935 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 19:53:38 -0500
Subject: [PATCH 03/96] Use v-bind instead for focus, change tabindex's

---
 packages/client/src/components/MkMenu.vue  | 16 ++++++++--------
 packages/client/src/components/MkModal.vue |  6 +++---
 packages/client/src/directives/focus.ts    |  3 +++
 packages/client/src/directives/index.ts    |  2 ++
 4 files changed, 16 insertions(+), 11 deletions(-)
 create mode 100644 packages/client/src/directives/focus.ts

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index c9f8dbfd6..dd9939dcf 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<div>
+<div tabindex="-1" v-focus>
 	<div
 		ref="itemsEl" v-hotkey="keymap"
 		class="rrevdjwt _popup _shadow"
@@ -12,33 +12,33 @@
 			<span v-else-if="item.type === 'label'" class="label item">
 				<span>{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
+			<span v-else-if="item.type === 'pending'" :tabindex="-1" class="pending item">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="-1" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="-1" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</a>
-			<button v-else-if="item.type === 'user'" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else-if="item.type === 'user'" :tabindex="-1" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<span v-else-if="item.type === 'switch'" :tabindex="-1" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
+			<button v-else-if="item.type === 'parent'" :tabindex="-1" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
 			</button>
-			<button v-else :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="-1" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 62f67a380..6a0744487 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -2,7 +2,7 @@
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1">
+		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
 			<slot :max-height="maxHeight" :type="type"></slot>
 		</div>
 	</div>
@@ -214,7 +214,7 @@ const align = () => {
 const onOpened = () => {
 	emit('opened');
 
-	content?.focus()
+	// content?.focus()
 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 	const el = content!.children[0];
 	el.addEventListener('mousedown', ev => {
@@ -330,7 +330,7 @@ defineExpose({
 
 .qzhlnise {
 	> .content {
-		border-radius: 16px;
+		border-radius: var(--radius);
 	}
 	> .bg {
 		&.transparent {
diff --git a/packages/client/src/directives/focus.ts b/packages/client/src/directives/focus.ts
new file mode 100644
index 000000000..4d34fbf1f
--- /dev/null
+++ b/packages/client/src/directives/focus.ts
@@ -0,0 +1,3 @@
+export default {
+	mounted: (el) => el.focus()
+}
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
index 0a5c32326..77639e2f3 100644
--- a/packages/client/src/directives/index.ts
+++ b/packages/client/src/directives/index.ts
@@ -11,6 +11,7 @@ import anim from "./anim";
 import clickAnime from "./click-anime";
 import panel from "./panel";
 import adaptiveBorder from "./adaptive-border";
+import focus from "./focus";
 
 export default function (app: App) {
 	app.directive("userPreview", userPreview);
@@ -25,4 +26,5 @@ export default function (app: App) {
 	app.directive("click-anime", clickAnime);
 	app.directive("panel", panel);
 	app.directive("adaptive-border", adaptiveBorder);
+	app.directive("focus", focus);
 }

From 1a7a0fdf31d87ad3af17abb9ced9f14a7287c3a0 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 19:53:38 -0500
Subject: [PATCH 04/96] Use v-bind instead for focus, allow focusing submenus,
 change tabindex's

---
 packages/client/src/components/MkMenu.vue  | 16 ++++++++--------
 packages/client/src/components/MkModal.vue |  6 +++---
 packages/client/src/directives/focus.ts    |  3 +++
 packages/client/src/directives/index.ts    |  2 ++
 4 files changed, 16 insertions(+), 11 deletions(-)
 create mode 100644 packages/client/src/directives/focus.ts

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index c9f8dbfd6..dd9939dcf 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<div>
+<div tabindex="-1" v-focus>
 	<div
 		ref="itemsEl" v-hotkey="keymap"
 		class="rrevdjwt _popup _shadow"
@@ -12,33 +12,33 @@
 			<span v-else-if="item.type === 'label'" class="label item">
 				<span>{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
+			<span v-else-if="item.type === 'pending'" :tabindex="-1" class="pending item">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="-1" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="-1" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</a>
-			<button v-else-if="item.type === 'user'" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else-if="item.type === 'user'" :tabindex="-1" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<span v-else-if="item.type === 'switch'" :tabindex="-1" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
+			<button v-else-if="item.type === 'parent'" :tabindex="-1" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
 			</button>
-			<button v-else :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="-1" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 62f67a380..6a0744487 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -2,7 +2,7 @@
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1">
+		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
 			<slot :max-height="maxHeight" :type="type"></slot>
 		</div>
 	</div>
@@ -214,7 +214,7 @@ const align = () => {
 const onOpened = () => {
 	emit('opened');
 
-	content?.focus()
+	// content?.focus()
 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 	const el = content!.children[0];
 	el.addEventListener('mousedown', ev => {
@@ -330,7 +330,7 @@ defineExpose({
 
 .qzhlnise {
 	> .content {
-		border-radius: 16px;
+		border-radius: var(--radius);
 	}
 	> .bg {
 		&.transparent {
diff --git a/packages/client/src/directives/focus.ts b/packages/client/src/directives/focus.ts
new file mode 100644
index 000000000..4d34fbf1f
--- /dev/null
+++ b/packages/client/src/directives/focus.ts
@@ -0,0 +1,3 @@
+export default {
+	mounted: (el) => el.focus()
+}
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
index 0a5c32326..77639e2f3 100644
--- a/packages/client/src/directives/index.ts
+++ b/packages/client/src/directives/index.ts
@@ -11,6 +11,7 @@ import anim from "./anim";
 import clickAnime from "./click-anime";
 import panel from "./panel";
 import adaptiveBorder from "./adaptive-border";
+import focus from "./focus";
 
 export default function (app: App) {
 	app.directive("userPreview", userPreview);
@@ -25,4 +26,5 @@ export default function (app: App) {
 	app.directive("click-anime", clickAnime);
 	app.directive("panel", panel);
 	app.directive("adaptive-border", adaptiveBorder);
+	app.directive("focus", focus);
 }

From e30c159226d0654cdecbef6d11fb500bdd2ce7b6 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 19:56:35 -0500
Subject: [PATCH 05/96] forgot to remove comment

---
 packages/client/src/components/MkModal.vue | 1 -
 1 file changed, 1 deletion(-)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 6a0744487..029e03939 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -214,7 +214,6 @@ const align = () => {
 const onOpened = () => {
 	emit('opened');
 
-	// content?.focus()
 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 	const el = content!.children[0];
 	el.addEventListener('mousedown', ev => {

From 8b92fc8bb8834f30c6c3ffebb6ff86c66f7bed46 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 20:25:22 -0500
Subject: [PATCH 06/96] oops

---
 packages/client/src/components/MkModal.vue | 2 --
 1 file changed, 2 deletions(-)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 9bea5dfc0..029e03939 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -2,7 +2,6 @@
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
 		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
 			<slot :max-height="maxHeight" :type="type"></slot>
 		</div>
@@ -331,7 +330,6 @@ defineExpose({
 .qzhlnise {
 	> .content {
 		border-radius: var(--radius);
-		border-radius: var(--radius);
 	}
 	> .bg {
 		&.transparent {

From 9cc5073dd7ab754b2141c008f7289de25729e977 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 21:19:37 -0500
Subject: [PATCH 07/96] fixes

---
 packages/client/src/components/MkMenu.vue     | 22 ++++++++++---------
 .../client/src/components/MkPopupMenu.vue     |  2 +-
 2 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index dd9939dcf..2037f205d 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -12,33 +12,33 @@
 			<span v-else-if="item.type === 'label'" class="label item">
 				<span>{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="-1" class="pending item">
+			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="-1" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="-1" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</a>
-			<button v-else-if="item.type === 'user'" :tabindex="-1" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else-if="item.type === 'user'" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="-1" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="-1" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
+			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
 			</button>
-			<button v-else :tabindex="-1" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
@@ -207,8 +207,7 @@ onBeforeUnmount(() => {
 		font-size: 0.9em;
 		line-height: 20px;
 		text-align: left;
-		overflow: hidden;
-		text-overflow: ellipsis;
+		outline: none;
 
 		&:before {
 			content: "";
@@ -232,7 +231,7 @@ onBeforeUnmount(() => {
 			transform: translateY(0em);
 		}
 
-		&:not(:disabled):hover {
+		&:not(:disabled):hover, &:focus-visible {
 			color: var(--accent);
 			text-decoration: none;
 
@@ -240,6 +239,9 @@ onBeforeUnmount(() => {
 				background: var(--accentedBg);
 			}
 		}
+		&:focus-visible:before {
+			outline: auto;
+		}
 
 		&.danger {
 			color: #eb6f92;
diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index 95234d7ce..386330f15 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')" tabindex="-1">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')" tabindex="-1" v-focus>
 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
 </MkModal>
 </template>

From 841f8bee60e17dbe8ce398074a362ec6f8c01a04 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 21:55:07 -0500
Subject: [PATCH 08/96] focus selected page

---
 packages/client/src/components/global/RouterView.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index e21a57471..31de1e337 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -1,7 +1,7 @@
 <template>
 <KeepAlive :max="defaultStore.state.numberOfPageCache">
 	<Suspense>
-		<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
+		<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)" tabindex="-1" v-focus/>
 
 		<template #fallback>
 			<MkLoading/>

From 6e8221a62a1c50641cd8f4bcd9ef121860de76be Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 10 Feb 2023 11:43:47 -0500
Subject: [PATCH 09/96] Change line-height to w3c recommend

https://www.w3.org/TR/WCAG21/#text-spacing
---
 packages/client/src/style.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index 01bff888a..2e53aadd7 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -32,7 +32,7 @@ html {
 	overflow-wrap: break-word;
 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
 	font-size: 14px;
-	line-height: 1.35;
+	line-height: 1.6;
 	text-size-adjust: 100%;
 	tab-size: 2;
 

From 14e3b3ac965a636ee8d9063f3d3dc3c0063782de Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sat, 11 Feb 2023 14:11:25 -0500
Subject: [PATCH 10/96] focus to more elements

---
 packages/client/src/components/MkLaunchPad.vue | 2 +-
 packages/client/src/components/MkSuperMenu.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index a88700131..ae3f3dcbe 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -96,7 +96,7 @@ function close() {
 			height: 100px;
 			border-radius: 10px;
 
-			&:hover {
+			&:hover, &:focus-visible {
 				color: var(--accent);
 				background: var(--accentedBg);
 				text-decoration: none;
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index b784d5239..b58906929 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -67,7 +67,7 @@ export default defineComponent({
 				font-size: 0.9em;
 				margin-bottom: 0.3rem;
 
-				&:hover {
+				&:hover, &:focus-visible {
 					text-decoration: none;
 					background: var(--panelHighlight);
 				}

From e266cc0b9928e368914b877834563bc0e253c8bb Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 20:49:41 -0500
Subject: [PATCH 11/96] Focus last element when exiting modal

---
 packages/client/src/components/MkModal.vue | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 029e03939..bc4478def 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -77,10 +77,12 @@ const type = $computed(() => {
 
 let contentClicking = false;
 
+const focusedElement = document.activeElement;
 const close = () => {
 	if (props.src) props.src.style.pointerEvents = 'auto';
 	showing = false;
 	emit('close');
+	focusedElement.focus();
 };
 
 const onBgClick = () => {

From ded8ab980a0de90d1d4d887b00970d4d2718cc32 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 20:59:11 -0500
Subject: [PATCH 12/96] Allow focusing of elements inside notes/posts

---
 packages/client/src/components/MkNote.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index ee6c5770f..fc33e649b 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -193,8 +193,8 @@ const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
 	'q': () => renoteButton.value.renote(true),
-	'up|k|shift+tab': focusBefore,
-	'down|j|tab': focusAfter,
+	'up|k': focusBefore,
+	'down|j': focusAfter,
 	'esc': blur,
 	'm|o': () => menu(true),
 	's': () => showContent.value !== showContent.value,

From 5e07df2ed065c92eb1134c7e1b323f884dae391a Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 21:56:01 -0500
Subject: [PATCH 13/96] FOCUS TRAPPING!!!!

---
 package.json                                  |  2 +
 .../client/src/components/MkMenu.child.vue    |  5 ++-
 packages/client/src/components/MkMenu.vue     |  8 +---
 packages/client/src/components/MkModal.vue    | 13 ++++---
 .../client/src/components/MkModalWindow.vue   | 29 +++++++-------
 pnpm-lock.yaml                                | 38 ++++++++++++++++++-
 6 files changed, 68 insertions(+), 27 deletions(-)

diff --git a/package.json b/package.json
index e8da6c5a7..34015128b 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
 		"@bull-board/ui": "^4.10.2",
 		"@tensorflow/tfjs": "^3.21.0",
 		"calckey-js": "^0.0.22",
+		"focus-trap": "^7.2.0",
+		"focus-trap-vue": "^4.0.1",
 		"js-yaml": "4.1.0",
 		"phosphor-icons": "^1.4.2",
 		"seedrandom": "^3.0.5"
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index 3ada4afbd..ee0514aab 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,6 +1,8 @@
 <template>
 <div ref="el" class="sfhdhdhr">
-	<MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
+	<FocusTrap v-bind:active="isActive">
+		<MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
+	</FocusTrap>
 </div>
 </template>
 
@@ -9,6 +11,7 @@ import { on } from 'events';
 import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue';
 import MkMenu from './MkMenu.vue';
 import { MenuItem } from '@/types/menu';
+import { FocusTrap } from 'focus-trap-vue';
 import * as os from '@/os';
 
 const props = defineProps<{
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index be7cd72bd..b7fd75478 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,7 +1,7 @@
 <template>
 <div tabindex="-1" v-focus>
 	<div
-		ref="itemsEl" v-hotkey="keymap"
+		ref="itemsEl"
 		class="rrevdjwt _popup _shadow"
 		:class="{ center: align === 'center', asDrawer }"
 		:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@@ -84,12 +84,6 @@ let items2: InnerMenuItem[] = $ref([]);
 
 let child = $ref<InstanceType<typeof XChild>>();
 
-let keymap = computed(() => ({
-	'up|k|shift+tab': focusUp,
-	'down|j|tab': focusDown,
-	'esc': close,
-}));
-
 let childShowingItem = $ref<MenuItem | null>();
 
 watch(() => props.items, () => {
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index bc4478def..e31818877 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -1,11 +1,13 @@
 <template>
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
-	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
-		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
-			<slot :max-height="maxHeight" :type="type"></slot>
+	<focus-trap v-model:active="isActive">
+		<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+			<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
+			<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
+				<slot :max-height="maxHeight" :type="type"></slot>
+			</div>
 		</div>
-	</div>
+	</focus-trap>
 </transition>
 </template>
 
@@ -15,6 +17,7 @@ import * as os from '@/os';
 import { isTouchUsing } from '@/scripts/touch';
 import { defaultStore } from '@/store';
 import { deviceKind } from '@/scripts/device-kind';
+import { FocusTrap } from 'focus-trap-vue';
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === 'BODY') return null;
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 666382466..77ae1bd8c 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -1,23 +1,26 @@
 <template>
-<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
-	<div ref="rootEl" class="ebkgoccj _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
-		<div ref="headerEl" class="header">
-			<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
-			<span class="title">
-				<slot name="header"></slot>
-			</span>
-			<button v-if="!withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
-			<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check-bold ph-lg"></i></button>
+<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @keyup.esc="$emit('close')" @closed="$emit('closed')">
+	<FocusTrap v-model:active="isActive">
+		<div ref="rootEl" class="ebkgoccj _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
+			<div ref="headerEl" class="header">
+				<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
+				<span class="title">
+					<slot name="header"></slot>
+				</span>
+				<button v-if="!withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
+				<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check-bold ph-lg"></i></button>
+			</div>
+			<div class="body">
+				<slot :width="bodyWidth" :height="bodyHeight"></slot>
+			</div>
 		</div>
-		<div class="body">
-			<slot :width="bodyWidth" :height="bodyHeight"></slot>
-		</div>
-	</div>
+	</FocusTrap>
 </MkModal>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted } from 'vue';
+import { FocusTrap } from 'focus-trap-vue';
 import MkModal from './MkModal.vue';
 
 const props = withDefaults(defineProps<{
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ee8486978..f295e1c1e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,6 +16,8 @@ importers:
       cross-env: 7.0.3
       cypress: 10.11.0
       execa: 5.1.1
+      focus-trap: ^7.2.0
+      focus-trap-vue: ^4.0.1
       gulp: 4.0.2
       gulp-cssnano: 2.1.3
       gulp-rename: 2.0.0
@@ -33,6 +35,8 @@ importers:
       '@bull-board/ui': 4.10.2
       '@tensorflow/tfjs': 3.21.0_seedrandom@3.0.5
       calckey-js: 0.0.22
+      focus-trap: 7.2.0
+      focus-trap-vue: 4.0.1_focus-trap@7.2.0
       js-yaml: 4.1.0
       phosphor-icons: 1.4.2
       seedrandom: 3.0.5
@@ -3387,7 +3391,7 @@ packages:
   /axios/0.25.0_debug@4.3.4:
     resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==}
     dependencies:
-      follow-redirects: 1.15.2
+      follow-redirects: 1.15.2_debug@4.3.4
     transitivePeerDependencies:
       - debug
     dev: true
@@ -6364,6 +6368,21 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
+  /focus-trap-vue/4.0.1_focus-trap@7.2.0:
+    resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
+    peerDependencies:
+      focus-trap: ^7.0.0
+      vue: ^3.0.0
+    dependencies:
+      focus-trap: 7.2.0
+    dev: false
+
+  /focus-trap/7.2.0:
+    resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==}
+    dependencies:
+      tabbable: 6.0.1
+    dev: false
+
   /follow-redirects/1.15.2:
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     engines: {node: '>=4.0'}
@@ -6372,6 +6391,19 @@ packages:
     peerDependenciesMeta:
       debug:
         optional: true
+    dev: false
+
+  /follow-redirects/1.15.2_debug@4.3.4:
+    resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+    dependencies:
+      debug: 4.3.4
+    dev: true
 
   /for-each/0.3.3:
     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -11986,6 +12018,10 @@ packages:
     resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
     dev: true
 
+  /tabbable/6.0.1:
+    resolution: {integrity: sha512-SYJSIgeyXW7EuX1ytdneO5e8jip42oHWg9xl/o3oTYhmXusZVgiA+VlPvjIN+kHii9v90AmzTZEBcsEvuAY+TA==}
+    dev: false
+
   /tapable/2.2.1:
     resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
     engines: {node: '>=6'}

From afbdd3bbcc5f614dc1e29a582316915ac2c95585 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 22:16:15 -0500
Subject: [PATCH 14/96] foxes

---
 packages/client/src/components/MkMenu.vue  | 99 +++++++++++-----------
 packages/client/src/components/MkModal.vue |  4 +-
 2 files changed, 53 insertions(+), 50 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index b7fd75478..acfaf9459 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,54 +1,56 @@
 <template>
 <div tabindex="-1" v-focus>
-	<div
-		ref="itemsEl"
-		class="rrevdjwt _popup _shadow"
-		:class="{ center: align === 'center', asDrawer }"
-		:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-		@contextmenu.self="e => e.preventDefault()"
-	>
-		<template v-for="(item, i) in items2">
-			<div v-if="item === null" class="divider"></div>
-			<span v-else-if="item.type === 'label'" class="label item">
-				<span>{{ item.text }}</span>
+	<FocusTrap v-bind:active="isActive">
+		<div
+			ref="itemsEl"
+			class="rrevdjwt _popup _shadow"
+			:class="{ center: align === 'center', asDrawer }"
+			:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
+			@contextmenu.self="e => e.preventDefault()"
+		>
+			<template v-for="(item, i) in items2">
+				<div v-if="item === null" class="divider"></div>
+				<span v-else-if="item.type === 'label'" class="label item">
+					<span>{{ item.text }}</span>
+				</span>
+				<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
+					<span><MkEllipsis/></span>
+				</span>
+				<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
+					<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+					<span>{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
+				</MkA>
+				<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
+					<span>{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
+				</a>
+				<button v-else-if="item.type === 'user' && !items.hidden" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
+					<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
+				</button>
+				<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
+				</span>
+				<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
+					<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
+					<span>{{ item.text }}</span>
+					<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
+				</button>
+				<button v-else-if="!item.hidden" :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
+					<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+					<span>{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
+				</button>
+			</template>
+			<span v-if="items2.length === 0" class="none item">
+				<span>{{ i18n.ts.none }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
-				<span><MkEllipsis/></span>
-			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
-				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
-			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
-			</a>
-			<button v-else-if="item.type === 'user' && !items.hidden" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
-				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
-			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
-			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
-				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
-				<span>{{ item.text }}</span>
-				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
-			</button>
-			<button v-else-if="!item.hidden" :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
-				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
-			</button>
-		</template>
-		<span v-if="items2.length === 0" class="none item">
-			<span>{{ i18n.ts.none }}</span>
-		</span>
-	</div>
+		</div>
+	</FocusTrap>
 	<div v-if="childMenu" class="child">
 		<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
 	</div>
@@ -62,6 +64,7 @@ import FormSwitch from '@/components/form/switch.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
+import { FocusTrap } from 'focus-trap-vue';
 
 const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
 
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index e31818877..32bffa038 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -1,9 +1,9 @@
 <template>
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<focus-trap v-model:active="isActive">
-		<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+		<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }" tabindex="-1" v-focus>
 			<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-			<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
+			<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
 				<slot :max-height="maxHeight" :type="type"></slot>
 			</div>
 		</div>

From 3d68efff4c6b3991a7cba1f139c315f38514490a Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 22:19:40 -0500
Subject: [PATCH 15/96] focus trap emoji picker (also fixes closing w/ esc)

---
 .../client/src/components/MkEmojiPicker.vue   | 131 +++++++++---------
 1 file changed, 67 insertions(+), 64 deletions(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index b26eb1206..578407d76 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -1,80 +1,82 @@
 <template>
-<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
-	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
-	<div ref="emojis" class="emojis">
-		<section class="result">
-			<div v-if="searchResultCustom.length > 0" class="body">
-				<button
-					v-for="emoji in searchResultCustom"
-					:key="emoji.id"
-					class="_button item"
-					:title="emoji.name"
-					tabindex="0"
-					@click="chosen(emoji, $event)"
-				>
-					<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
-					<img class="emoji" :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
-				</button>
-			</div>
-			<div v-if="searchResultUnicode.length > 0" class="body">
-				<button
-					v-for="emoji in searchResultUnicode"
-					:key="emoji.name"
-					class="_button item"
-					:title="emoji.name"
-					tabindex="0"
-					@click="chosen(emoji, $event)"
-				>
-					<MkEmoji class="emoji" :emoji="emoji.char"/>
-				</button>
-			</div>
-		</section>
-
-		<div v-if="tab === 'index'" class="group index">
-			<section v-if="showPinned">
-				<div class="body">
+<FocusTrap v-bind:active="isActive">
+	<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
+		<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
+		<div ref="emojis" class="emojis">
+			<section class="result">
+				<div v-if="searchResultCustom.length > 0" class="body">
 					<button
-						v-for="emoji in pinned"
-						:key="emoji"
+						v-for="emoji in searchResultCustom"
+						:key="emoji.id"
 						class="_button item"
+						:title="emoji.name"
 						tabindex="0"
 						@click="chosen(emoji, $event)"
 					>
-						<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
+						<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+						<img class="emoji" :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+					</button>
+				</div>
+				<div v-if="searchResultUnicode.length > 0" class="body">
+					<button
+						v-for="emoji in searchResultUnicode"
+						:key="emoji.name"
+						class="_button item"
+						:title="emoji.name"
+						tabindex="0"
+						@click="chosen(emoji, $event)"
+					>
+						<MkEmoji class="emoji" :emoji="emoji.char"/>
 					</button>
 				</div>
 			</section>
 
-			<section>
-				<header><i class="ph-alarm-bold ph-fw ph-lg"></i> {{ i18n.ts.recentUsed }}</header>
-				<div class="body">
-					<button
-						v-for="emoji in recentlyUsedEmojis"
-						:key="emoji"
-						class="_button item"
-						@click="chosen(emoji, $event)"
-					>
-						<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
-					</button>
-				</div>
-			</section>
+			<div v-if="tab === 'index'" class="group index">
+				<section v-if="showPinned">
+					<div class="body">
+						<button
+							v-for="emoji in pinned"
+							:key="emoji"
+							class="_button item"
+							tabindex="0"
+							@click="chosen(emoji, $event)"
+						>
+							<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
+						</button>
+					</div>
+				</section>
+
+				<section>
+					<header><i class="ph-alarm-bold ph-fw ph-lg"></i> {{ i18n.ts.recentUsed }}</header>
+					<div class="body">
+						<button
+							v-for="emoji in recentlyUsedEmojis"
+							:key="emoji"
+							class="_button item"
+							@click="chosen(emoji, $event)"
+						>
+							<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
+						</button>
+					</div>
+				</section>
+			</div>
+			<div v-once class="group">
+				<header>{{ i18n.ts.customEmojis }}</header>
+				<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
+			</div>
+			<div v-once class="group">
+				<header>{{ i18n.ts.emoji }}</header>
+				<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
+			</div>
 		</div>
-		<div v-once class="group">
-			<header>{{ i18n.ts.customEmojis }}</header>
-			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
-		</div>
-		<div v-once class="group">
-			<header>{{ i18n.ts.emoji }}</header>
-			<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
+		<div class="tabs">
+			<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk-bold ph-lg ph-fw ph-lg"></i></button>
+			<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley-bold ph-lg ph-fw ph-lg"></i></button>
+			<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf-bold ph-lg ph-fw ph-lg"></i></button>
+			<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash-bold ph-lg ph-fw ph-lg"></i></button>
 		</div>
 	</div>
-	<div class="tabs">
-		<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk-bold ph-lg ph-fw ph-lg"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley-bold ph-lg ph-fw ph-lg"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf-bold ph-lg ph-fw ph-lg"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash-bold ph-lg ph-fw ph-lg"></i></button>
-	</div>
-</div>
+</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -90,6 +92,7 @@ import { deviceKind } from '@/scripts/device-kind';
 import { emojiCategories, instance } from '@/instance';
 import { i18n } from '@/i18n';
 import { defaultStore } from '@/store';
+import { FocusTrap } from 'focus-trap-vue';
 
 const props = withDefaults(defineProps<{
 	showPinned?: boolean;

From aab0377a1e350376e45c8738615db12043b565ef Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 22:24:12 -0500
Subject: [PATCH 16/96] fix submenu positioning

---
 packages/client/src/components/MkMenu.vue | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index acfaf9459..fe7ebbe1a 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,13 +1,13 @@
 <template>
 <div tabindex="-1" v-focus>
-	<FocusTrap v-bind:active="isActive">
-		<div
-			ref="itemsEl"
-			class="rrevdjwt _popup _shadow"
-			:class="{ center: align === 'center', asDrawer }"
-			:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-			@contextmenu.self="e => e.preventDefault()"
-		>
+	<div
+	ref="itemsEl"
+	class="rrevdjwt _popup _shadow"
+	:class="{ center: align === 'center', asDrawer }"
+	:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
+	@contextmenu.self="e => e.preventDefault()"
+	>
+		<FocusTrap v-bind:active="isActive">
 			<template v-for="(item, i) in items2">
 				<div v-if="item === null" class="divider"></div>
 				<span v-else-if="item.type === 'label'" class="label item">
@@ -49,8 +49,8 @@
 			<span v-if="items2.length === 0" class="none item">
 				<span>{{ i18n.ts.none }}</span>
 			</span>
-		</div>
-	</FocusTrap>
+		</FocusTrap>
+	</div>
 	<div v-if="childMenu" class="child">
 		<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
 	</div>

From a52281d4e018b33d53911b9376cf742556877157 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 22:31:55 -0500
Subject: [PATCH 17/96] actually fix submenu pos

---
 packages/client/src/components/MkMenu.vue | 26 +++++++++++------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index fe7ebbe1a..2e25843cd 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,13 +1,13 @@
 <template>
 <div tabindex="-1" v-focus>
-	<div
-	ref="itemsEl"
-	class="rrevdjwt _popup _shadow"
-	:class="{ center: align === 'center', asDrawer }"
-	:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-	@contextmenu.self="e => e.preventDefault()"
-	>
-		<FocusTrap v-bind:active="isActive">
+	<FocusTrap v-bind:active="isActive">
+		<div
+			ref="itemsEl"
+			class="rrevdjwt _popup _shadow"
+			:class="{ center: align === 'center', asDrawer }"
+			:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
+			@contextmenu.self="e => e.preventDefault()"
+		>
 			<template v-for="(item, i) in items2">
 				<div v-if="item === null" class="divider"></div>
 				<span v-else-if="item.type === 'label'" class="label item">
@@ -49,11 +49,11 @@
 			<span v-if="items2.length === 0" class="none item">
 				<span>{{ i18n.ts.none }}</span>
 			</span>
-		</FocusTrap>
-	</div>
-	<div v-if="childMenu" class="child">
-		<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
-	</div>
+		</div>
+		<div v-if="childMenu" class="child">
+			<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
+		</div>
+	</FocusTrap>
 </div>
 </template>
 

From 05b2f9b89d514261282c0ea39ff6f392d429b8a3 Mon Sep 17 00:00:00 2001
From: fruye <fruye@unix.dog>
Date: Fri, 28 Apr 2023 19:49:34 +0200
Subject: [PATCH 18/96] fix: Declare /api/v1/accounts/relationships before
 /api/v1/accounts/:id

Previously the 'relationships' part was considered to be an account id
and was handled by completely different API endpoint.
---
 .../server/api/mastodon/endpoints/account.ts  | 76 +++++++++----------
 1 file changed, 38 insertions(+), 38 deletions(-)

diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 70bdb74f3..749058193 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -91,6 +91,44 @@ export function apiAccountMastodon(router: Router): void {
 			ctx.body = e.response.data;
 		}
 	});
+	router.get("/v1/accounts/relationships", async (ctx) => {
+		const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
+		const accessTokens = ctx.headers.authorization;
+		const client = getClient(BASE_URL, accessTokens);
+		let users;
+		try {
+			// TODO: this should be body
+			let ids = ctx.request.query ? ctx.request.query["id[]"] : null;
+			if (typeof ids === "string") {
+				ids = [ids];
+			}
+			users = ids;
+			relationshipModel.id = ids?.toString() || "1";
+			if (!ids) {
+				ctx.body = [relationshipModel];
+				return;
+			}
+
+			let reqIds = [];
+			for (let i = 0; i < ids.length; i++) {
+				reqIds.push(convertId(ids[i], IdType.CalckeyId));
+			}
+
+			const data = await client.getRelationships(reqIds);
+			let resp = data.data;
+			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
+				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
+			}
+			ctx.body = resp;
+		} catch (e: any) {
+			console.error(e);
+			let data = e.response.data;
+			data.users = users;
+			console.error(data);
+			ctx.status = 401;
+			ctx.body = data;
+		}
+	});
 	router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => {
 		const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
 		const accessTokens = ctx.headers.authorization;
@@ -340,44 +378,6 @@ export function apiAccountMastodon(router: Router): void {
 			}
 		},
 	);
-	router.get("/v1/accounts/relationships", async (ctx) => {
-		const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
-		const accessTokens = ctx.headers.authorization;
-		const client = getClient(BASE_URL, accessTokens);
-		let users;
-		try {
-			// TODO: this should be body
-			let ids = ctx.request.query ? ctx.request.query["id[]"] : null;
-			if (typeof ids === "string") {
-				ids = [ids];
-			}
-			users = ids;
-			relationshipModel.id = ids?.toString() || "1";
-			if (!ids) {
-				ctx.body = [relationshipModel];
-				return;
-			}
-
-			let reqIds = [];
-			for (let i = 0; i < ids.length; i++) {
-				reqIds.push(convertId(ids[i], IdType.CalckeyId));
-			}
-
-			const data = await client.getRelationships(reqIds);
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
-		} catch (e: any) {
-			console.error(e);
-			let data = e.response.data;
-			data.users = users;
-			console.error(data);
-			ctx.status = 401;
-			ctx.body = data;
-		}
-	});
 	router.get("/v1/bookmarks", async (ctx) => {
 		const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
 		const accessTokens = ctx.headers.authorization;

From 212420820a02b0e93e29fc114434a25afaf2713a Mon Sep 17 00:00:00 2001
From: Essem <smswessem@gmail.com>
Date: Fri, 28 Apr 2023 13:19:57 -0500
Subject: [PATCH 19/96] Fix dot menu in welcome page

---
 packages/client/src/pages/welcome.entrance.a.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue
index 286ecdda1..3866b088b 100644
--- a/packages/client/src/pages/welcome.entrance.a.vue
+++ b/packages/client/src/pages/welcome.entrance.a.vue
@@ -308,6 +308,7 @@ function showMenu(ev) {
 				height: 32px;
 				border-radius: 8px;
 				font-size: 18px;
+				z-index: 2;
 			}
 
 			> .fg {

From 7bafca446c79819df6801a043d157aafb0d39b83 Mon Sep 17 00:00:00 2001
From: Gear <gear@gear.is>
Date: Fri, 28 Apr 2023 16:01:09 -0400
Subject: [PATCH 20/96] Add documentation for various MFM functions

---
 locales/en-US.yml                             |  8 +++
 packages/client/src/pages/mfm-cheat-sheet.vue | 54 ++++++++++++++++++-
 2 files changed, 61 insertions(+), 1 deletion(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index a11a404d6..2feb2cd94 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1237,6 +1237,14 @@ _mfm:
   sparkleDescription: "Gives content a sparkling particle effect."
   rotate: "Rotate"
   rotateDescription: "Turns content by a specified angle."
+  position: "Position"
+  positionDescription: "Move content by a specified amount."
+  scale: "Scale"
+  scaleDescription: "Scale content by a specified amount."
+  foreground: "Foreground color"
+  foregroundDescription: "Change the foreground color of text."
+  background: "Background color"
+  backgroundDescription: "Change the background color of text."
   plain: "Plain"
   plainDescription: "Deactivates the effects of all MFM contained within this MFM\
     \ effect."
diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index 1f3f0bea2..1946d17ce 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -341,6 +341,54 @@
 						</div>
 					</div>
 				</div>
+				<div class="section _block">
+					<div class="title">{{ i18n.ts._mfm.position }}</div>
+					<div class="content">
+						<p>{{ i18n.ts._mfm.positionDescription }}</p>
+						<div class="preview">
+							<Mfm :text="preview_position" />
+							<MkTextarea v-model="preview_position"
+								><span>MFM</span></MkTextarea
+							>
+						</div>
+					</div>
+				</div>
+				<div class="section _block">
+					<div class="title">{{ i18n.ts._mfm.scale }}</div>
+					<div class="content">
+						<p>{{ i18n.ts._mfm.scaleDescription }}</p>
+						<div class="preview">
+							<Mfm :text="preview_scale" />
+							<MkTextarea v-model="preview_scale"
+								><span>MFM</span></MkTextarea
+							>
+						</div>
+					</div>
+				</div>
+				<div class="section _block">
+					<div class="title">{{ i18n.ts._mfm.foreground }}</div>
+					<div class="content">
+						<p>{{ i18n.ts._mfm.foregroundDescription }}</p>
+						<div class="preview">
+							<Mfm :text="preview_fg" />
+							<MkTextarea v-model="preview_fg"
+								><span>MFM</span></MkTextarea
+							>
+						</div>
+					</div>
+				</div>
+				<div class="section _block">
+					<div class="title">{{ i18n.ts._mfm.background }}</div>
+					<div class="content">
+						<p>{{ i18n.ts._mfm.backgroundDescription }}</p>
+						<div class="preview">
+							<Mfm :text="preview_bg" />
+							<MkTextarea v-model="preview_bg"
+								><span>MFM</span></MkTextarea
+							>
+						</div>
+					</div>
+				</div>
 				<div class="section _block">
 					<div class="title">{{ i18n.ts._mfm.plain }}</div>
 					<div class="content">
@@ -402,7 +450,11 @@ let preview_x4 = $ref("$[x4 🍮]");
 let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
 let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
 let preview_sparkle = $ref("$[sparkle 🍮]");
-let preview_rotate = $ref("$[rotate 🍮]");
+let preview_rotate = $ref("$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]");
+let preview_position = $ref("$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]");
+let preview_scale = $ref("$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]");
+let preview_fg = $ref("$[fg.color=ff0000 Text color]");
+let preview_bg = $ref("$[bg.color=ff0000 Background color]");
 let preview_plain = $ref(
 	"<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>"
 );

From a8f71f76181403f9a1718c6596595eaf3bde3a6d Mon Sep 17 00:00:00 2001
From: jolupa <jolupameister@gmail.com>
Date: Fri, 28 Apr 2023 18:34:42 +0000
Subject: [PATCH 21/96] chore: Translated using Weblate (Catalan)

Currently translated at 35.0% (606 of 1727 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/ca/
---
 locales/ca-ES.yml | 290 +++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 248 insertions(+), 42 deletions(-)

diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 3de877dbe..24e4b6d7d 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -1,7 +1,7 @@
 _lang_: "Català"
 headlineMisskey: "Una xarxa social de codi obert, descentralitzada i gratuita per\
   \ sempre \U0001F680"
-introMisskey: "Benvinguts! Calckey es una plataforma social de codi obert,  descentralitzada\
+introMisskey: "Benvinguts! Calckey es una plataforma social de codi obert, descentralitzada\
   \ i gratuita per sempre! \U0001F680"
 monthAndDay: "{day}/{month}"
 search: "Cercar"
@@ -15,43 +15,43 @@ gotIt: "Ho he entès!"
 cancel: "Cancel·lar"
 enterUsername: "Introdueix el teu nom d'usuari"
 renotedBy: "Resignat per {user}"
-noNotes: "Cap nota"
+noNotes: "Cap publicació"
 noNotifications: "Cap notificació"
-instance: "Instàncies"
+instance: "Instància"
 settings: "Preferències"
 basicSettings: "Configuració bàsica"
-otherSettings: "Configuració avançada"
-openInWindow: "Obrir en una nova finestra"
+otherSettings: "Altres opcions"
+openInWindow: "Obrir en una finestra nova"
 profile: "Perfil"
 timeline: "Línia de temps"
 noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia."
 login: "Iniciar sessió"
-loggingIn: "Identificant-se"
-logout: "Tancar la sessió"
+loggingIn: "Iniciant sessió"
+logout: "Tancar sessió"
 signup: "Registrar-se"
 uploading: "Pujant..."
 save: "Desar"
 users: "Usuaris"
 addUser: "Afegir un usuari"
-favorite: "Afegir a preferits"
+favorite: "Afegir a favorits"
 favorites: "Favorits"
-unfavorite: "Eliminar dels preferits"
-favorited: "Afegit als preferits."
-alreadyFavorited: "Ja s'ha afegit als preferits."
-cantFavorite: "No s'ha pogut afegir als preferits."
+unfavorite: "Eliminar de favorits"
+favorited: "Afegit a favorits."
+alreadyFavorited: "Ja s'ha afegit a favorits."
+cantFavorite: "No s'ha pogut afegir a favorits."
 pin: "Fixar al perfil"
-unpin: "Para de fixar del perfil"
-copyContent: "Copiar el contingut"
-copyLink: "Copiar l'enllaç"
-delete: "Eliminar"
-deleteAndEdit: "Esborrar i editar"
-deleteAndEditConfirm: "Estàs segur que vols suprimir aquesta nota i editar-la? Perdràs\
-  \ totes les reaccions, notes i respostes."
-addToList: "Afegir a una llista"
+unpin: "Deixar de fixar al perfil"
+copyContent: "Còpia el contingut"
+copyLink: "Còpia l'enllaç"
+delete: "Esborra"
+deleteAndEdit: "Esborrar i edita"
+deleteAndEditConfirm: "Estàs segur que vols esborrar aquesta nota i editar-la? Perdràs\
+  \ totes les reaccions, resignats i respostes."
+addToList: "Afegir a la llista"
 sendMessage: "Enviar un missatge"
-copyUsername: "Copiar nom d'usuari"
-searchUser: "Cercar usuaris"
-reply: "Respondre"
+copyUsername: "Còpia nom d'usuari"
+searchUser: "Cercar un usuari"
+reply: "Respon"
 loadMore: "Carregar més"
 showMore: "Veure més"
 youGotNewFollower: "t'ha seguit"
@@ -60,21 +60,21 @@ followRequestAccepted: "Sol·licitud de seguiment acceptada"
 mention: "Menció"
 mentions: "Mencions"
 directNotes: "Missatges directes"
-importAndExport: "Importar / Exportar"
+importAndExport: "Importar / Exportar Dades"
 import: "Importar"
 export: "Exportar"
 files: "Fitxers"
-download: "Baixar"
-driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes\
-  \ associades a aquest fitxer adjunt també se suprimiran."
+download: "Descarregar"
+driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les publicacions\
+  \ associades a aquest fitxer adjunt també es suprimiran."
 unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?"
 exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà\
-  \ a la teva unitat un cop completat."
+  \ al teu Disc un cop completada."
 importRequested: "Has sol·licitat una importació. Això pot trigar una estona."
 lists: "Llistes"
 noLists: "No tens cap llista"
-note: "Post"
-notes: "Posts"
+note: "Publicació"
+notes: "Publicacions"
 following: "Seguint"
 followers: "Seguidors"
 followsYou: "Et segueix"
@@ -83,7 +83,7 @@ manageLists: "Gestionar les llistes"
 error: "Error"
 somethingHappened: "S'ha produït un error"
 retry: "Torna-ho a intentar"
-pageLoadError: "S'ha produït un error en carregar la pàgina"
+pageLoadError: "Alguna cosa a sortit malament al carregar la pàgina."
 pageLoadErrorDescription: "Això normalment es deu a errors de xarxa o a la memòria\
   \ cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després\
   \ d'esperar una estona."
@@ -100,13 +100,13 @@ followRequests: "Sol·licituds de seguiment"
 unfollow: "Deixar de seguir"
 followRequestPending: "Sol·licituds de seguiment pendents"
 enterEmoji: "Introduir un emoji"
-renote: "Renotar"
-unrenote: "Anul·lar renota"
-renoted: "Renotat."
-cantRenote: "Aquesta publicació no pot ser renotada."
-cantReRenote: "Impossible renotar una renota."
+renote: "Impulsà"
+unrenote: "Anul·lar impuls"
+renoted: "Impulsat."
+cantRenote: "Aquesta publicació no pot ser impulsada."
+cantReRenote: "No es pot impulsar un impuls."
 quote: "Citar"
-pinnedNote: "Nota fixada"
+pinnedNote: "Publicació fixada"
 pinned: "Fixar al perfil"
 you: "Tu"
 clickToShow: "Fes clic per mostrar"
@@ -116,7 +116,7 @@ reaction: "Reaccions"
 reactionSetting: "Reaccions a mostrar al selector de reaccions"
 reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem\
   \ \"+\" per afegir."
-rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
+rememberNoteVisibility: "Recorda la configuració de visibilitat de les publicacions"
 attachCancel: "Eliminar el fitxer adjunt"
 markAsSensitive: "Marcar com a NSFW"
 unmarkAsSensitive: "Deixar de marcar com a sensible"
@@ -130,7 +130,7 @@ unsuspend: "Deixa de suspendre"
 instances: "Instàncies"
 remove: "Eliminar"
 nsfw: "NSFW"
-pinnedNotes: "Nota fixada"
+pinnedNotes: "Publicació fixada"
 userList: "Llistes"
 smtpUser: "Nom d'usuari"
 smtpPass: "Contrasenya"
@@ -147,7 +147,7 @@ _mfm:
 _theme:
   keys:
     mention: "Menció"
-    renote: "Renotar"
+    renote: "Impulsar"
 _sfx:
   note: "Posts"
   notification: "Notificacions"
@@ -191,12 +191,12 @@ _notification:
   _types:
     follow: "Seguint"
     mention: "Menció"
-    renote: "Renotar"
+    renote: "Impulsos"
     quote: "Citar"
     reaction: "Reaccions"
   _actions:
     reply: "Respondre"
-    renote: "Renotar"
+    renote: "Impulsos"
 _deck:
   _columns:
     notifications: "Notificacions"
@@ -469,3 +469,209 @@ enableLocalTimeline: Activa la línea de temps local
 enableRecommendedTimeline: Activa la línea de temps de recomanats
 pinnedClipId: ID del clip que vols fixar
 hcaptcha: hCaptcha
+manageAntennas: Gestiona les Antenes
+name: Nom
+notesAndReplies: Articles i respostes
+silence: Posa en silenci
+withFiles: Amb fitxers
+popularUsers: Usuaris populars
+exploreUsersCount: Hi han {count} usuaris
+exploreFediverse: Explora el Fesiverse
+popularTags: Etiquetes populars
+about: Sobre
+recentlyUpdatedUsers: Usuaris actius fa poc
+recentlyRegisteredUsers: Usuaris registrats fa poc
+recentlyDiscoveredUsers: Nous suaris descoberts
+administrator: Administrador
+token: Token
+registerSecurityKey: Registra una clau de seguretat
+securityKeyName: Nom clau
+lastUsed: Feta servir per última vegada
+unregister: Anul·lar el registre
+passwordLessLogin: Identificació sense contrasenya
+share: Comparteix
+notFound: No s'ha trobat
+newPasswordIs: La nova contrasenya és "{password}"
+notFoundDescription: No es pot trobar cap pàgina que correspongui a aquesta adreça
+  URL.
+uploadFolder: Carpeta per defecte per pujar arxius
+cacheClear: Netejar la memòria cau
+markAsReadAllNotifications: Marca totes les notificacions com llegides
+markAsReadAllUnreadNotes: Marca totes les publicacions com a llegides
+markAsReadAllTalkMessages: Marca tots els missatges com llegits
+help: Ajuda
+inputMessageHere: Escriu aquí el missatge
+close: Tancar
+group: Grup
+groups: Grups
+createGroup: Crea un grup
+ownedGroups: Grups que et pertanyen
+joinedGroups: Grups als que t'has unit
+groupName: Nom del grup
+members: Membres
+transfer: Transferir
+messagingWithUser: Conversa privada
+title: Títol
+text: Text
+enable: Activar
+next: Següent
+retype: Torna a entrar
+noteOf: Publicat per {user}
+inviteToGroup: Invitar a un grup
+quoteAttached: Cita
+quoteQuestion: Adjuntar com a cita?
+noMessagesYet: Encara no hi han missatges
+signinRequired: Si us plau registrat o inicia sessió per continuar
+invitations: Invitacions
+invitationCode: Codi d'invitació
+checking: Comprovant...
+usernameInvalidFormat: Pots fer servir lletres en majúscules o minúscules, nombres
+  i guions baixos.
+tooShort: Massa curt
+tooLong: Massa llarg
+weakPassword: Contrasenya amb seguretat feble
+strongPassword: Contrasenya amb seguretat forta
+passwordMatched: Coincidències
+signinWith: Inicieu sessió com {x}
+signinFailed: No es pot iniciar sessió. El nom d'usuari o la contrasenya són incorrectes.
+or: O
+language: Idioma
+uiLanguage: Idioma de la interfície d'usuari
+groupInvited: T'han invitat a un grup
+aboutX: Sobre {x}
+youHaveNoGroups: No tens grups
+disableDrawer: No facis servir els menús amb estil de calaix
+noHistory: No ha historial disponible
+signinHistory: Historial d'inicis de sessió
+disableAnimatedMfm: Desactiva les animacions amb MFM
+doing: Processant...
+category: Categoría
+existingAccount: El compte ja existeix
+regenerate: Regenerar
+docSource: Font d'aquest document
+createAccount: Crear compte
+fontSize: Mida del text
+noFollowRequests: No tens cap sol·licitud de seguiment per aprovar
+openImageInNewTab: Obre les imatges en una pestanya nova
+dashboard: Panell
+local: Local
+remote: Remot
+total: Total
+weekOverWeekChanges: Canvis d'ençà la passada setmana
+dayOverDayChanges: Canvis d'ençà ahir
+appearance: Aparença
+clientSettings: Configuració del client
+accountSettings: Configuració del compte
+promotion: Promogut
+promote: Promoure
+numberOfDays: Nombre de dies
+objectStorageBaseUrl: Adreça URL base
+hideThisNote: Amaga aquest article
+showFeaturedNotesInTimeline: Mostra els articles destacats a la línea de temps
+objectStorage: Emmagatzematge d'objectes
+useObjectStorage: Fes servir l'emmagatzema d'objectes
+expandTweet: Amplia el tuit
+themeEditor: Editor de temes
+description: Descripció
+leaveConfirm: Hi han canvis que no s'han desat. Els vols descartar?
+manage: Administració
+plugins: Afegits
+preferencesBackups: Preferències de còpies de seguretat
+undeck: Treure el Deck
+useBlurEffectForModal: Fes servir efectes de difuminació en les finestres modals
+useFullReactionPicker: Fes servir el selector de reaccions a tamany complert
+deck: Deck
+width: Amplada
+generateAccessToken: Genera un token d'accés
+medium: Mitja
+small: Petit
+permission: Permisos
+enableAll: Activa tots
+tokenRequested: Garantir accés al compte
+pluginTokenRequestedDescription: Aquest afegit podrà fer servir els permisos configurats
+  aquí.
+emailServer: Servidor de correu electrònic
+notificationType: Tipus de notificació
+edit: Editar
+emailAddress: Adreça de Correu electrònic
+smtpConfig: Configuració del servidor SMTP
+smtpHost: Host
+enableEmail: Activa la distribució de correu electrònic
+smtpPort: Port
+emailConfigInfo: Fet servir per confirmar les adreçats de correu electrònic al registrar-se
+  o si s'oblida la contrasenya
+email: Correu electrònic
+smtpSecure: Fes servir SSL/TLS implícit per connectar-se per SMTP
+emptyToDisableSmtpAuth: Deixa el nom d'usuari i la contrasenya sense emplenar per
+  desactivar la verificació SMTP
+smtpSecureInfo: Desactiva això quant facis servir STARTTLS
+testEmail: Envia un correu electrònic de verificació
+wordMute: Silenciar paraules
+regexpError: Error a la Expressió Regular
+regexpErrorDescription: 'Hi ha un error a la expressió regular a la línea {line} de
+  la teva {tab} de paraules silenciades:'
+userSaysSomething: '{name} va dir alguna cosa'
+instanceMute: Silenciar instàncies
+logs: Registres
+copy: Copiar
+delayed: Retardat
+metrics: Mètriques
+overview: Vista general
+database: Base de dades
+regenerateLoginToken: Regenera el token d'inici de sessió
+reduceUiAnimation: Redueix les animacions de la UI
+messagingWithGroup: Conversa en grup
+invites: Invitacions
+unavailable: No disponible
+newMessageExists: Tens nous missatges
+onlyOneFileCanBeAttached: Només pots adjuntar un fitxer per missatge
+normalPassword: Contrasenya amb seguretat mitjana
+passwordNotMatched: No hi han coincidències
+useOsNativeEmojis: Fes servir els emojis per defecte del Sistema Operatiu
+joinOrCreateGroup: Fes que et convidin a un grup o crea el teu propi.
+objectStorageBaseUrlDesc: "Es l'adreça URL que serveix com a referència. Específica\
+  \ la adreça URL del CDN o Proxy si fas servir.\nPer fer servir S3 'https://<bucket>.s3.amazonaws.com'\
+  \ i per GCS o serveis semblants 'https://storage.googleapis.com/<bucket>', etc."
+height: Alçada
+large: Gran
+notificationSetting: Preferències de notificacions
+makeActive: Activar
+notificationSettingDesc: Tria el tipus de notificació que es veure.
+notifyAntenna: Notificar noves articles
+withFileAntenna: Només articles amb fitxers
+enableServiceworker: Activa les notificacions push per al teu navegador
+antennaUsersDescription: Escriu un nom d'usuari per línea
+antennaInstancesDescription: Escriu la adreça d'una instància per línea
+tags: Etiquetes
+antennaSource: Font de la antena
+antennaKeywords: Paraules claus a escolta
+antennaExcludeKeywords: Paraules clau a excluir
+antennaKeywordsDescription: Separades amb espais per fer una condició AND i amb una
+  línea nova per fer una condició OR.
+caseSensitive: Sensible a majúscules i minúscules
+withReplies: Inclou respostes
+connectedTo: Aquest(s) compte(s) estan connectats
+silenceConfirm: Segur que vols posa en silenci aquest usuari?
+unsilence: Desfés posar en silenci
+unsilenceConfirm: Segur que vols treure el silenci a aquest usuari?
+aboutMisskey: Sobre Calckey
+twoStepAuthentication: Autentificació de dos factors
+moderator: Moderador
+moderation: Moderació
+available: Disponible
+tapSecurityKey: Escriu la teva clau de seguretat
+nUsersMentioned: Esmentat per {n} usuari(s)
+securityKey: Clau de seguretat
+resetPassword: Restablir contrasenya
+describeFile: Afegeix un subtítol
+enterFileDescription: Entra un subtítol
+author: Autor
+disableAll: Desactiva tots
+userSaysSomethingReason: '{name} va dir {reason}'
+display: Visualització
+channel: Canals
+create: Crear
+useGlobalSetting: Fes servir els ajustos globals
+useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del
+  teu compte. Si es desactiva , es poden fer configuracions individuals.
+other: Altres

From 575d4c284388f424049af8c872d92c392c5f6c3c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=9A=D0=B8=D0=B1=D0=B5=D1=80=20=D0=BE=D0=B3=D1=83=D1=80?=
 =?UTF-8?q?=D1=87=D0=B8=D0=BA?= <abrew1330@gmail.com>
Date: Thu, 27 Apr 2023 04:11:37 +0000
Subject: [PATCH 22/96] chore: Translated using Weblate (Russian)

Currently translated at 100.0% (1727 of 1727 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/ru/
---
 locales/ru-RU.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 24878686b..91d96fcc5 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -986,7 +986,7 @@ _registry:
   createKey: "Новый ключ"
 _aboutMisskey:
   about: "Calckey это форк Misskey, сделанный ThatOneCalculator, разработка которого\
-    \ начал с 2022."
+    \ началась с 2022."
   contributors: "Основные соавторы"
   allContributors: "Все соавторы"
   source: "Исходный код"

From c8b11536feec82bc33874d6a0b5ba47b29be4c96 Mon Sep 17 00:00:00 2001
From: Kenny Hui <kenny.mh.hui@outlook.com>
Date: Wed, 26 Apr 2023 09:44:37 +0000
Subject: [PATCH 23/96] chore: Translated using Weblate (Chinese (Traditional))

Currently translated at 97.4% (1683 of 1727 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/zh_Hant/
---
 locales/zh-TW.yml | 31 +++++++++++++++++--------------
 1 file changed, 17 insertions(+), 14 deletions(-)

diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 78bf4f34c..eb640b7dd 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -64,7 +64,7 @@ import: "匯入"
 export: "匯出"
 files: "檔案"
 download: "下載"
-driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n"
+driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。"
 unfollowConfirm: "確定要取消追隨{name}嗎?"
 exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。"
 importRequested: "已請求匯入。這可能會花一點時間"
@@ -291,7 +291,7 @@ emptyDrive: "雲端硬碟為空"
 emptyFolder: "資料夾為空"
 unableToDelete: "無法刪除"
 inputNewFileName: "輸入檔案名稱"
-inputNewDescription: "請輸入新標題 "
+inputNewDescription: "請輸入新標題"
 inputNewFolderName: "輸入新資料夾的名稱"
 circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。"
 hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
@@ -324,7 +324,7 @@ yearX: "{year}年"
 pages: "頁面"
 integration: "整合"
 connectService: "己連結"
-disconnectService: "己斷開 "
+disconnectService: "己斷開"
 enableLocalTimeline: "開啟本地時間軸"
 enableGlobalTimeline: "啟用公開時間軸"
 disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
@@ -336,7 +336,7 @@ driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
 inMb: "以Mbps為單位"
 iconUrl: "圖像URL"
 bannerUrl: "橫幅圖像URL"
-backgroundImageUrl: "背景圖片的來源網址 "
+backgroundImageUrl: "背景圖片的來源網址"
 basicInfo: "基本資訊"
 pinnedUsers: "置頂用戶"
 pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。"
@@ -490,7 +490,7 @@ useObjectStorage: "使用Object Storage"
 objectStorageBaseUrl: "Base URL"
 objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理,请指定其URL,例如S3:“https://<bucket>.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/<bucket>”"
 objectStorageBucket: "儲存空間(Bucket)"
-objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 "
+objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。"
 objectStoragePrefix: "前綴"
 objectStoragePrefixDesc: "它存儲在此前綴目錄下。"
 objectStorageEndpoint: "端點(Endpoint)"
@@ -560,8 +560,8 @@ disablePlayer: "關閉播放器"
 expandTweet: "展開推文"
 themeEditor: "主題編輯器"
 description: "描述"
-describeFile: "添加標題 "
-enterFileDescription: "輸入標題 "
+describeFile: "添加標題"
+enterFileDescription: "輸入標題"
 author: "作者"
 leaveConfirm: "有未保存的更改。要放棄嗎?"
 manage: "管理"
@@ -865,7 +865,7 @@ driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
 driveCapOverrideCaption: "如果指定0以下的值,就會被取消。"
 requireAdminForView: "必須以管理者帳號登入才可以檢視。"
 isSystemAccount: "由系統自動建立與管理的帳號。"
-typeToConfirm: "要執行這項操作,請輸入 {x} "
+typeToConfirm: "要執行這項操作,請輸入 {x}"
 deleteAccount: "刪除帳號"
 document: "文件"
 numberOfPageCache: "快取頁面數"
@@ -876,7 +876,7 @@ statusbar: "狀態列"
 pleaseSelect: "請選擇"
 reverse: "翻轉"
 colored: "彩色"
-refreshInterval: "更新間隔"
+refreshInterval: "更新間隔 "
 label: "標籤"
 type: "類型"
 speed: "速度"
@@ -895,7 +895,7 @@ activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,
 navbar: "導覽列"
 shuffle: "隨機"
 account: "帳戶"
-move: "移動 "
+move: "移動"
 customKaTeXMacro: "自定義 KaTeX 宏"
 customKaTeXMacroDescription: "使用宏來輕鬆的輸入數學表達式吧!宏的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\\
   name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉個例子,\\\
@@ -933,11 +933,11 @@ _accountDelete:
   inProgress: "正在刪除"
 _ad:
   back: "返回"
-  reduceFrequencyOfThisAd: "降低此廣告的頻率 "
+  reduceFrequencyOfThisAd: "降低此廣告的頻率"
 _forgotPassword:
   enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
-  ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 "
-  contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。 "
+  ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。"
+  contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。"
 _gallery:
   my: "我的貼文"
   liked: "喜歡的貼文"
@@ -1000,7 +1000,7 @@ _mfm:
   url: "URL"
   urlDescription: "可以展示URL位址。"
   link: "鏈接"
-  linkDescription: "您可以將特定範圍的文章與 URL 相關聯。 "
+  linkDescription: "您可以將特定範圍的文章與 URL 相關聯。"
   bold: "粗體"
   boldDescription: "可以將文字顯示为粗體来強調。"
   small: "縮小"
@@ -1805,3 +1805,6 @@ migration: 遷移
 homeTimeline: 主頁時間軸
 swipeOnDesktop: 允許在桌面上進行手機式滑動
 logoImageUrl: 圖標網址
+addInstance: 增加一個實例
+noInstances: 沒有實例
+flagSpeakAsCat: 像貓一樣地說話

From d61a314f40d493201e6aebd8b4480acfce2c5bfe Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 19:06:29 -0400
Subject: [PATCH 24/96] forgot to add this

---
 pnpm-lock.yaml | 41 ++++++++++++++++++++++++++---------------
 1 file changed, 26 insertions(+), 15 deletions(-)

diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cf4c29c06..9c5079f98 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,6 +19,12 @@ importers:
       '@tensorflow/tfjs':
         specifier: ^3.21.0
         version: 3.21.0(seedrandom@3.0.5)
+      focus-trap:
+        specifier: ^7.2.0
+        version: 7.2.0
+      focus-trap-vue:
+        specifier: ^4.0.1
+        version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
@@ -3803,14 +3809,12 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       source-map: 0.6.1
-    dev: true
 
   /@vue/compiler-dom@3.2.45:
     resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
     dependencies:
       '@vue/compiler-core': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/compiler-sfc@2.7.14:
     resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@@ -3833,14 +3837,12 @@ packages:
       magic-string: 0.25.9
       postcss: 8.4.21
       source-map: 0.6.1
-    dev: true
 
   /@vue/compiler-ssr@3.2.45:
     resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
     dependencies:
       '@vue/compiler-dom': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/reactivity-transform@3.2.45:
     resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@@ -3850,20 +3852,17 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       magic-string: 0.25.9
-    dev: true
 
   /@vue/reactivity@3.2.45:
     resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
     dependencies:
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/runtime-core@3.2.45:
     resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
     dependencies:
       '@vue/reactivity': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/runtime-dom@3.2.45:
     resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@@ -3871,7 +3870,6 @@ packages:
       '@vue/runtime-core': 3.2.45
       '@vue/shared': 3.2.45
       csstype: 2.6.21
-    dev: true
 
   /@vue/server-renderer@3.2.45(vue@3.2.45):
     resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@@ -3881,11 +3879,9 @@ packages:
       '@vue/compiler-ssr': 3.2.45
       '@vue/shared': 3.2.45
       vue: 3.2.45
-    dev: true
 
   /@vue/shared@3.2.45:
     resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
-    dev: true
 
   /@webassemblyjs/ast@1.11.1:
     resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@@ -6074,7 +6070,6 @@ packages:
 
   /csstype@2.6.21:
     resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
-    dev: true
 
   /csstype@3.1.1:
     resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
@@ -6979,7 +6974,6 @@ packages:
 
   /estree-walker@2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
-    dev: true
 
   /esutils@2.0.3:
     resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@@ -7445,6 +7439,22 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
+  /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
+    resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
+    peerDependencies:
+      focus-trap: ^7.0.0
+      vue: ^3.0.0
+    dependencies:
+      focus-trap: 7.2.0
+      vue: 3.2.45
+    dev: false
+
+  /focus-trap@7.2.0:
+    resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==}
+    dependencies:
+      tabbable: 6.1.1
+    dev: false
+
   /follow-redirects@1.15.2(debug@4.3.4):
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     engines: {node: '>=4.0'}
@@ -10350,7 +10360,6 @@ packages:
     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
     dependencies:
       sourcemap-codec: 1.4.8
-    dev: true
 
   /mailcheck@1.1.1:
     resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
@@ -13267,7 +13276,6 @@ packages:
   /sourcemap-codec@1.4.8:
     resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
     deprecated: Please use @jridgewell/sourcemap-codec instead
-    dev: true
 
   /sparkles@1.0.1:
     resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==}
@@ -13686,6 +13694,10 @@ packages:
     resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
     dev: true
 
+  /tabbable@6.1.1:
+    resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==}
+    dev: false
+
   /tapable@2.2.1:
     resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
     engines: {node: '>=6'}
@@ -14776,7 +14788,6 @@ packages:
       '@vue/runtime-dom': 3.2.45
       '@vue/server-renderer': 3.2.45(vue@3.2.45)
       '@vue/shared': 3.2.45
-    dev: true
 
   /vuedraggable@4.1.0(vue@3.2.45):
     resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}

From 10b3079658c7a9fdb9ed45b9d9b46bc83a0b6a76 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 19:28:25 -0400
Subject: [PATCH 25/96] Fix focusing inside of CW's

---
 packages/client/src/components/MkCwButton.vue | 15 ++++++++--
 packages/client/src/components/MkNote.vue     |  4 ++-
 .../src/components/MkSubNoteContent.vue       | 30 ++++++++++++++++---
 3 files changed, 42 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 35af48874..01ab11351 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -1,5 +1,6 @@
 <template>
 	<button
+		ref="el"
 		class="_button"
 		:class="{ showLess: modelValue, fade: !modelValue }"
 		@click.stop="toggle"
@@ -12,7 +13,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from "vue";
+import { computed, ref } from "vue";
 import { length } from "stringz";
 import * as misskey from "calckey-js";
 import { concat } from "@/scripts/array";
@@ -27,6 +28,8 @@ const emit = defineEmits<{
 	(ev: "update:modelValue", v: boolean): void;
 }>();
 
+const el = ref<HTMLElement>(); 
+
 const label = computed(() => {
 	return concat([
 		props.note.text
@@ -43,6 +46,14 @@ const label = computed(() => {
 const toggle = () => {
 	emit("update:modelValue", !props.modelValue);
 };
+
+function focus() {
+	el.value.focus();
+}
+
+defineExpose({
+	focus
+});
 </script>
 
 <style lang="scss" scoped>
@@ -62,7 +73,7 @@ const toggle = () => {
 			}
 		}
 	}
-	&:hover > span {
+	&:hover > span, &:focus > span {
 		background: var(--cwFg) !important;
 		color: var(--cwBg) !important;
 	}
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index f49105e81..5d9c40d38 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -84,6 +84,7 @@
 						:detailedView="detailedView"
 						:parentId="appearNote.parentId"
 						@push="(e) => router.push(notePage(e))"
+						@focusfooter="footerEl.focus()"
 					></MkSubNoteContent>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -117,7 +118,7 @@
 						<MkTime :time="appearNote.createdAt" mode="absolute" />
 					</MkA>
 				</div>
-				<footer ref="el" class="footer" @click.stop>
+				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -278,6 +279,7 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
+const footerEl = ref<HTMLElement>(); 
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index a345b23f5..a20afc39b 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -35,7 +35,11 @@
 			class="content"
 			:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
 		>
-			<div class="body">
+			<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" />
+			<div 
+				class="body"
+				v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
+			>
 				<span v-if="note.deletedAt" style="opacity: 0.5"
 					>({{ i18n.ts.deleted }})</span
 				>
@@ -96,6 +100,11 @@
 						<XNoteSimple :note="note.renote" />
 					</div>
 				</template>
+				<div
+					v-if="!showContent"
+					tabindex="0"
+					v-on:focus="cwButton?.focus()"
+				></div>
 			</div>
 			<button
 				v-if="isLong && collapsed"
@@ -111,13 +120,13 @@
 			>
 				<span>{{ i18n.ts.showLess }}</span>
 			</button>
-			<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
+			<XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" />
 		</div>
 	</div>
 </template>
 
 <script lang="ts" setup>
-import {} from "vue";
+import { ref } from "vue"; 
 import * as misskey from "calckey-js";
 import * as mfm from "mfm-js";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
@@ -138,8 +147,10 @@ const props = defineProps<{
 
 const emit = defineEmits<{
 	(ev: "push", v): void;
+	(ev: "focusfooter"): void;
 }>();
 
+const cwButton = ref<HTMLElement>(); 
 const isLong =
 	!props.detailedView &&
 	props.note.cw == null &&
@@ -151,6 +162,13 @@ const urls = props.note.text
 	: null;
 
 let showContent = $ref(false);
+
+
+function focusFooter(ev) {
+	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
+		emit("focusfooter");
+	}
+}
 </script>
 
 <style lang="scss" scoped>
@@ -242,6 +260,9 @@ let showContent = $ref(false);
 				margin-top: -50px;
 				padding-top: 50px;
 				overflow: hidden;
+				user-select: none;
+				-webkit-user-select: none;
+				-moz-user-select: none;
 			}
 			&.collapsed > .body {
 				box-sizing: border-box;
@@ -271,6 +292,7 @@ let showContent = $ref(false);
 				bottom: 0;
 				left: 0;
 				width: 100%;
+				z-index: 2;
 				> span {
 					display: inline-block;
 					background: var(--panel);
@@ -279,7 +301,7 @@ let showContent = $ref(false);
 					border-radius: 999px;
 					box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 				}
-				&:hover {
+				&:hover, &:focus {
 					> span {
 						background: var(--panelHighlight);
 					}

From 14ec973c7088c5a7ff58fcb51270c00d4d4dbd84 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 19:57:19 -0400
Subject: [PATCH 26/96] add the focus trap thingies again

---
 .../client/src/components/MkEmojiPicker.vue   | 277 ++++++-------
 packages/client/src/components/MkMenu.vue     | 366 +++++++++---------
 packages/client/src/components/MkModal.vue    |  26 +-
 .../client/src/components/MkModalWindow.vue   |  93 ++---
 4 files changed, 380 insertions(+), 382 deletions(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index a22006951..6c18e92f0 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -1,157 +1,159 @@
 <template>
-	<div
-		class="omfetrab"
-		:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
-		:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
-	>
-		<input
-			ref="search"
-			v-model.trim="q"
-			class="search"
-			data-prevent-emoji-insert
-			:class="{ filled: q != null && q != '' }"
-			:placeholder="i18n.ts.search"
-			type="search"
-			@paste.stop="paste"
-			@keyup.enter="done()"
-		/>
-		<div ref="emojis" class="emojis">
-			<section class="result">
-				<div v-if="searchResultCustom.length > 0" class="body">
-					<button
-						v-for="emoji in searchResultCustom"
-						:key="emoji.id"
-						class="_button item"
-						:title="emoji.name"
-						tabindex="0"
-						@click="chosen(emoji, $event)"
-					>
-						<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
-						<img
-							class="emoji"
-							:src="
-								disableShowingAnimatedImages
-									? getStaticImageUrl(emoji.url)
-									: emoji.url
-							"
-						/>
-					</button>
-				</div>
-				<div v-if="searchResultUnicode.length > 0" class="body">
-					<button
-						v-for="emoji in searchResultUnicode"
-						:key="emoji.name"
-						class="_button item"
-						:title="emoji.name"
-						tabindex="0"
-						@click="chosen(emoji, $event)"
-					>
-						<MkEmoji class="emoji" :emoji="emoji.char" />
-					</button>
-				</div>
-			</section>
-
-			<div v-if="tab === 'index'" class="group index">
-				<section v-if="showPinned">
-					<div class="body">
+	<FocusTrap v-bind:active="isActive">
+		<div
+			class="omfetrab"
+			:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
+			:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
+		>
+			<input
+				ref="search"
+				v-model.trim="q"
+				class="search"
+				data-prevent-emoji-insert
+				:class="{ filled: q != null && q != '' }"
+				:placeholder="i18n.ts.search"
+				type="search"
+				@paste.stop="paste"
+				@keyup.enter="done()"
+			/>
+			<div ref="emojis" class="emojis">
+				<section class="result">
+					<div v-if="searchResultCustom.length > 0" class="body">
 						<button
-							v-for="emoji in pinned"
-							:key="emoji"
+							v-for="emoji in searchResultCustom"
+							:key="emoji.id"
 							class="_button item"
+							:title="emoji.name"
 							tabindex="0"
 							@click="chosen(emoji, $event)"
 						>
-							<MkEmoji
+							<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+							<img
 								class="emoji"
-								:emoji="emoji"
-								:normal="true"
+								:src="
+									disableShowingAnimatedImages
+										? getStaticImageUrl(emoji.url)
+										: emoji.url
+								"
 							/>
 						</button>
 					</div>
+					<div v-if="searchResultUnicode.length > 0" class="body">
+						<button
+							v-for="emoji in searchResultUnicode"
+							:key="emoji.name"
+							class="_button item"
+							:title="emoji.name"
+							tabindex="0"
+							@click="chosen(emoji, $event)"
+						>
+							<MkEmoji class="emoji" :emoji="emoji.char" />
+						</button>
+					</div>
 				</section>
 
-				<section>
-					<header class="_acrylic">
-						<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
-						{{ i18n.ts.recentUsed }}
-					</header>
-					<div class="body">
-						<button
-							v-for="emoji in recentlyUsedEmojis"
-							:key="emoji"
-							class="_button item"
-							@click="chosen(emoji, $event)"
-						>
-							<MkEmoji
-								class="emoji"
-								:emoji="emoji"
-								:normal="true"
-							/>
-						</button>
-					</div>
-				</section>
+				<div v-if="tab === 'index'" class="group index">
+					<section v-if="showPinned">
+						<div class="body">
+							<button
+								v-for="emoji in pinned"
+								:key="emoji"
+								class="_button item"
+								tabindex="0"
+								@click="chosen(emoji, $event)"
+							>
+								<MkEmoji
+									class="emoji"
+									:emoji="emoji"
+									:normal="true"
+								/>
+							</button>
+						</div>
+					</section>
+
+					<section>
+						<header class="_acrylic">
+							<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
+							{{ i18n.ts.recentUsed }}
+						</header>
+						<div class="body">
+							<button
+								v-for="emoji in recentlyUsedEmojis"
+								:key="emoji"
+								class="_button item"
+								@click="chosen(emoji, $event)"
+							>
+								<MkEmoji
+									class="emoji"
+									:emoji="emoji"
+									:normal="true"
+								/>
+							</button>
+						</div>
+					</section>
+				</div>
+				<div v-once class="group">
+					<header>{{ i18n.ts.customEmojis }}</header>
+					<XSection
+						v-for="category in customEmojiCategories"
+						:key="'custom:' + category"
+						:initial-shown="false"
+						:emojis="
+							customEmojis
+								.filter((e) => e.category === category)
+								.map((e) => ':' + e.name + ':')
+						"
+						@chosen="chosen"
+						>{{ category || i18n.ts.other }}</XSection
+					>
+				</div>
+				<div v-once class="group">
+					<header>{{ i18n.ts.emoji }}</header>
+					<XSection
+						v-for="category in categories"
+						:key="category"
+						:emojis="
+							emojilist
+								.filter((e) => e.category === category)
+								.map((e) => e.char)
+						"
+						@chosen="chosen"
+						>{{ category }}</XSection
+					>
+				</div>
 			</div>
-			<div v-once class="group">
-				<header>{{ i18n.ts.customEmojis }}</header>
-				<XSection
-					v-for="category in customEmojiCategories"
-					:key="'custom:' + category"
-					:initial-shown="false"
-					:emojis="
-						customEmojis
-							.filter((e) => e.category === category)
-							.map((e) => ':' + e.name + ':')
-					"
-					@chosen="chosen"
-					>{{ category || i18n.ts.other }}</XSection
+			<div class="tabs">
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'index' }"
+					@click="tab = 'index'"
 				>
-			</div>
-			<div v-once class="group">
-				<header>{{ i18n.ts.emoji }}</header>
-				<XSection
-					v-for="category in categories"
-					:key="category"
-					:emojis="
-						emojilist
-							.filter((e) => e.category === category)
-							.map((e) => e.char)
-					"
-					@chosen="chosen"
-					>{{ category }}</XSection
+					<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'custom' }"
+					@click="tab = 'custom'"
 				>
+					<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'unicode' }"
+					@click="tab = 'unicode'"
+				>
+					<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'tags' }"
+					@click="tab = 'tags'"
+				>
+					<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
 			</div>
 		</div>
-		<div class="tabs">
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'index' }"
-				@click="tab = 'index'"
-			>
-				<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'custom' }"
-				@click="tab = 'custom'"
-			>
-				<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'unicode' }"
-				@click="tab = 'unicode'"
-			>
-				<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'tags' }"
-				@click="tab = 'tags'"
-			>
-				<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-		</div>
-	</div>
+	</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -171,6 +173,7 @@ import { deviceKind } from "@/scripts/device-kind";
 import { emojiCategories, instance } from "@/instance";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
+import { FocusTrap } from 'focus-trap-vue';
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 45b69a23f..af2613b8c 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,191 +1,185 @@
 <template>
-	<div>
-		<div
-			ref="itemsEl"
-			v-hotkey="keymap"
-			class="rrevdjwt _popup _shadow"
-			:class="{ center: align === 'center', asDrawer }"
-			:style="{
-				width: width && !asDrawer ? width + 'px' : '',
-				maxHeight: maxHeight ? maxHeight + 'px' : '',
-			}"
-			@contextmenu.self="(e) => e.preventDefault()"
-		>
-			<template v-for="(item, i) in items2">
-				<div v-if="item === null" class="divider"></div>
-				<span v-else-if="item.type === 'label'" class="label item">
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-				</span>
-				<span
-					v-else-if="item.type === 'pending'"
-					:tabindex="i"
-					class="pending item"
-				>
-					<span><MkEllipsis /></span>
-				</span>
-				<MkA
-					v-else-if="item.type === 'link'"
-					:to="item.to"
-					:tabindex="i"
-					class="_button item"
-					@click.passive="close(true)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
+	<FocusTrap v-bind:active="isActive">
+	<div tabindex="-1" v-focus>
+			<div
+				ref="itemsEl"
+				class="rrevdjwt _popup _shadow"
+				:class="{ center: align === 'center', asDrawer }"
+				:style="{
+					width: width && !asDrawer ? width + 'px' : '',
+					maxHeight: maxHeight ? maxHeight + 'px' : '',
+				}"
+				@contextmenu.self="(e) => e.preventDefault()"
+			>
+				<template v-for="(item, i) in items2">
+					<div v-if="item === null" class="divider"></div>
+					<span v-else-if="item.type === 'label'" class="label item">
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
 					</span>
-					<MkAvatar
-						v-if="item.avatar"
-						:user="item.avatar"
-						class="avatar"
-					/>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</MkA>
-				<a
-					v-else-if="item.type === 'a'"
-					:href="item.href"
-					:target="item.target"
-					:download="item.download"
-					:tabindex="i"
-					class="_button item"
-					@click="close(true)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</a>
-				<button
-					v-else-if="item.type === 'user' && !items.hidden"
-					:tabindex="i"
-					class="_button item"
-					:class="{ active: item.active }"
-					:disabled="item.active"
-					@click="clicked(item.action, $event)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<MkAvatar :user="item.user" class="avatar" /><MkUserName
-						:user="item.user"
-					/>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</button>
-				<span
-					v-else-if="item.type === 'switch'"
-					:tabindex="i"
-					class="item"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<FormSwitch
-						v-model="item.ref"
-						:disabled="item.disabled"
-						class="form-switch"
-						:style="item.textStyle || ''"
-						>{{ item.text }}</FormSwitch
+					<span
+						v-else-if="item.type === 'pending'"
+						class="pending item"
 					>
+						<span><MkEllipsis /></span>
+					</span>
+					<MkA
+						v-else-if="item.type === 'link'"
+						:to="item.to"
+						class="_button item"
+						@click.passive="close(true)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<MkAvatar
+							v-if="item.avatar"
+							:user="item.avatar"
+							class="avatar"
+						/>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</MkA>
+					<a
+						v-else-if="item.type === 'a'"
+						:href="item.href"
+						:target="item.target"
+						:download="item.download"
+						class="_button item"
+						@click="close(true)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</a>
+					<button
+						v-else-if="item.type === 'user' && !items.hidden"
+						class="_button item"
+						:class="{ active: item.active }"
+						:disabled="item.active"
+						@click="clicked(item.action, $event)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<MkAvatar :user="item.user" class="avatar" /><MkUserName
+							:user="item.user"
+						/>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</button>
+					<span
+						v-else-if="item.type === 'switch'"
+						class="item"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<FormSwitch
+							v-model="item.ref"
+							:disabled="item.disabled"
+							class="form-switch"
+							:style="item.textStyle || ''"
+							>{{ item.text }}</FormSwitch
+						>
+					</span>
+					<button
+						v-else-if="item.type === 'parent'"
+						class="_button item parent"
+						:class="{ childShowing: childShowingItem === item }"
+						@mouseenter="showChildren(item, $event)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span class="caret"
+							><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
+						></span>
+					</button>
+					<button
+						v-else-if="!item.hidden"
+						class="_button item"
+						:class="{ danger: item.danger, active: item.active }"
+						:disabled="item.active"
+						@click="clicked(item.action, $event)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<MkAvatar
+							v-if="item.avatar"
+							:user="item.avatar"
+							class="avatar"
+						/>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</button>
+				</template>
+				<span v-if="items2.length === 0" class="none item">
+					<span>{{ i18n.ts.none }}</span>
 				</span>
-				<button
-					v-else-if="item.type === 'parent'"
-					:tabindex="i"
-					class="_button item parent"
-					:class="{ childShowing: childShowingItem === item }"
-					@mouseenter="showChildren(item, $event)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span class="caret"
-						><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
-					></span>
-				</button>
-				<button
-					v-else-if="!item.hidden"
-					:tabindex="i"
-					class="_button item"
-					:class="{ danger: item.danger, active: item.active }"
-					:disabled="item.active"
-					@click="clicked(item.action, $event)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<MkAvatar
-						v-if="item.avatar"
-						:user="item.avatar"
-						class="avatar"
-					/>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</button>
-			</template>
-			<span v-if="items2.length === 0" class="none item">
-				<span>{{ i18n.ts.none }}</span>
-			</span>
+			</div>
+			<div v-if="childMenu" class="child">
+				<XChild
+					ref="child"
+					:items="childMenu"
+					:target-element="childTarget"
+					:root-element="itemsEl"
+					showing
+					@actioned="childActioned"
+				/>
+			</div>
 		</div>
-		<div v-if="childMenu" class="child">
-			<XChild
-				ref="child"
-				:items="childMenu"
-				:target-element="childTarget"
-				:root-element="itemsEl"
-				showing
-				@actioned="childActioned"
-			/>
-		</div>
-	</div>
+	</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -229,12 +223,6 @@ let items2: InnerMenuItem[] = $ref([]);
 
 let child = $ref<InstanceType<typeof XChild>>();
 
-let keymap = computed(() => ({
-	"up|k|shift+tab": focusUp,
-	"down|j|tab": focusDown,
-	esc: close,
-}));
-
 let childShowingItem = $ref<MenuItem | null>();
 
 watch(
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 2c5498583..ff001f869 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -35,6 +35,8 @@
 					: 'none',
 				'--transformOrigin': transformOrigin,
 			}"
+			tabindex="-1"
+			v-focus
 		>
 			<div
 				class="_modalBg data-cy-bg"
@@ -50,17 +52,19 @@
 				@mousedown="onBgClick"
 				@contextmenu.prevent.stop="() => {}"
 			></div>
-			<div
-				ref="content"
-				:class="[
-					$style.content,
-					{ [$style.fixed]: fixed, top: type === 'dialog:top' },
-				]"
-				:style="{ zIndex }"
-				@click.self="onBgClick"
-			>
-				<slot :max-height="maxHeight" :type="type"></slot>
-			</div>
+			<focus-trap v-model:active="isActive">
+				<div
+					ref="content"
+					:class="[
+						$style.content,
+						{ [$style.fixed]: fixed, top: type === 'dialog:top' },
+					]"
+					:style="{ zIndex }"
+					@click.self="onBgClick"
+				>
+					<slot :max-height="maxHeight" :type="type"></slot>
+				</div>
+			</focus-trap>
 		</div>
 	</Transition>
 </template>
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index b425c4625..48028422e 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -3,54 +3,57 @@
 		ref="modal"
 		:prefer-type="'dialog'"
 		@click="onBgClick"
+		@keyup.esc="$emit('close')"
 		@closed="$emit('closed')"
 	>
-		<div
-			ref="rootEl"
-			class="ebkgoccj"
-			:style="{
-				width: `${width}px`,
-				height: scroll
-					? height
-						? `${height}px`
-						: null
-					: height
-					? `min(${height}px, 100%)`
-					: '100%',
-			}"
-			@keydown="onKeydown"
-		>
-			<div ref="headerEl" class="header">
-				<button
-					v-if="withOkButton"
-					class="_button"
-					@click="$emit('close')"
-				>
-					<i class="ph-x ph-bold ph-lg"></i>
-				</button>
-				<span class="title">
-					<slot name="header"></slot>
-				</span>
-				<button
-					v-if="!withOkButton"
-					class="_button"
-					@click="$emit('close')"
-				>
-					<i class="ph-x ph-bold ph-lg"></i>
-				</button>
-				<button
-					v-if="withOkButton"
-					class="_button"
-					:disabled="okButtonDisabled"
-					@click="$emit('ok')"
-				>
-					<i class="ph-check ph-bold ph-lg"></i>
-				</button>
+		<focus-trap v-model:active="isActive">
+			<div
+				ref="rootEl"
+				class="ebkgoccj"
+				:style="{
+					width: `${width}px`,
+					height: scroll
+						? height
+							? `${height}px`
+							: null
+						: height
+						? `min(${height}px, 100%)`
+						: '100%',
+				}"
+				@keydown="onKeydown"
+			>
+				<div ref="headerEl" class="header">
+					<button
+						v-if="withOkButton"
+						class="_button"
+						@click="$emit('close')"
+					>
+						<i class="ph-x ph-bold ph-lg"></i>
+					</button>
+					<span class="title">
+						<slot name="header"></slot>
+					</span>
+					<button
+						v-if="!withOkButton"
+						class="_button"
+						@click="$emit('close')"
+					>
+						<i class="ph-x ph-bold ph-lg"></i>
+					</button>
+					<button
+						v-if="withOkButton"
+						class="_button"
+						:disabled="okButtonDisabled"
+						@click="$emit('ok')"
+					>
+						<i class="ph-check ph-bold ph-lg"></i>
+					</button>
+				</div>
+				<div class="body">
+					<slot :width="bodyWidth" :height="bodyHeight"></slot>
+				</div>
 			</div>
-			<div class="body">
-				<slot :width="bodyWidth" :height="bodyHeight"></slot>
-			</div>
-		</div>
+		</focus-trap>
 	</MkModal>
 </template>
 

From ec372782119f82f13785bcd53a8310a41c6b7199 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 20:26:10 -0400
Subject: [PATCH 27/96] fixes

---
 packages/client/src/components/MkMenu.vue  |  2 +-
 packages/client/src/components/MkModal.vue | 67 +++++++++++-----------
 2 files changed, 35 insertions(+), 34 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index af2613b8c..f22f0f9ca 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,6 +1,6 @@
 <template>
 	<FocusTrap v-bind:active="isActive">
-	<div tabindex="-1" v-focus>
+		<div tabindex="-1" v-focus>
 			<div
 				ref="itemsEl"
 				class="rrevdjwt _popup _shadow"
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index ff001f869..c47cd47b3 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -14,45 +14,46 @@
 		:duration="transitionDuration"
 		appear
 		@after-leave="emit('closed')"
+		@keyup.esc="emit('click')"
 		@enter="emit('opening')"
 		@after-enter="onOpened"
 	>
-		<div
-			v-show="manualShowing != null ? manualShowing : showing"
-			v-hotkey.global="keymap"
-			:class="[
-				$style.root,
-				{
-					[$style.drawer]: type === 'drawer',
-					[$style.dialog]: type === 'dialog' || type === 'dialog:top',
-					[$style.popup]: type === 'popup',
-				},
-			]"
-			:style="{
-				zIndex,
-				pointerEvents: (manualShowing != null ? manualShowing : showing)
-					? 'auto'
-					: 'none',
-				'--transformOrigin': transformOrigin,
-			}"
-			tabindex="-1"
-			v-focus
-		>
+		<focus-trap v-model:active="isActive">
 			<div
-				class="_modalBg data-cy-bg"
+				v-show="manualShowing != null ? manualShowing : showing"
+				v-hotkey.global="keymap"
 				:class="[
-					$style.bg,
+					$style.root,
 					{
-						[$style.bgTransparent]: isEnableBgTransparent,
-						'data-cy-transparent': isEnableBgTransparent,
+						[$style.drawer]: type === 'drawer',
+						[$style.dialog]: type === 'dialog' || type === 'dialog:top',
+						[$style.popup]: type === 'popup',
 					},
 				]"
-				:style="{ zIndex }"
-				@click="onBgClick"
-				@mousedown="onBgClick"
-				@contextmenu.prevent.stop="() => {}"
-			></div>
-			<focus-trap v-model:active="isActive">
+				:style="{
+					zIndex,
+					pointerEvents: (manualShowing != null ? manualShowing : showing)
+						? 'auto'
+						: 'none',
+					'--transformOrigin': transformOrigin,
+				}"
+				tabindex="-1"
+				v-focus
+			>
+				<div
+					class="_modalBg data-cy-bg"
+					:class="[
+						$style.bg,
+						{
+							[$style.bgTransparent]: isEnableBgTransparent,
+							'data-cy-transparent': isEnableBgTransparent,
+						},
+					]"
+					:style="{ zIndex }"
+					@click="onBgClick"
+					@mousedown="onBgClick"
+					@contextmenu.prevent.stop="() => {}"
+				></div>
 				<div
 					ref="content"
 					:class="[
@@ -64,8 +65,8 @@
 				>
 					<slot :max-height="maxHeight" :type="type"></slot>
 				</div>
-			</focus-trap>
-		</div>
+			</div>
+		</focus-trap>
 	</Transition>
 </template>
 

From b8ca00c593ce2efdaf0d4b94407d93a1d5df7f15 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 20:47:57 -0400
Subject: [PATCH 28/96] fixes?

---
 packages/client/src/components/MkEmojiPicker.vue | 1 +
 packages/client/src/components/MkMenu.child.vue  | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index 6c18e92f0..88d207bab 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -4,6 +4,7 @@
 			class="omfetrab"
 			:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
 			:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
+			tabindex="-1"
 		>
 			<input
 				ref="search"
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index 52131c5ff..d5e9d774f 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,6 +1,6 @@
 <template>
 	<FocusTrap v-bind:active="isActive">
-		<div ref="el" class="sfhdhdhr">
+		<div ref="el" class="sfhdhdhr" tabindex="-1">
 			<MkMenu
 				ref="menu"
 				:items="items"

From efcd0dd71bbddbf79943fedfd81b54d116336130 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 20:52:09 -0400
Subject: [PATCH 29/96] focus last element on bg click

---
 packages/client/src/components/MkModal.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index c47cd47b3..d1a96b4b7 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -188,6 +188,7 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
 function onBgClick() {
 	if (contentClicking) return;
 	emit("click");
+	focusedElement.focus();
 }
 
 if (type === "drawer") {

From 7c698d1697c9edf542773af1b40076184c8c448b Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 21:18:00 -0400
Subject: [PATCH 30/96] consistency + fix

---
 packages/client/src/components/MkModal.vue          | 6 +++---
 packages/client/src/components/MkModalWindow.vue    | 5 +++--
 packages/client/src/components/MkSubNoteContent.vue | 2 +-
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index d1a96b4b7..c41fb50c0 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -18,7 +18,7 @@
 		@enter="emit('opening')"
 		@after-enter="onOpened"
 	>
-		<focus-trap v-model:active="isActive">
+		<FocusTrap v-model:active="isActive">
 			<div
 				v-show="manualShowing != null ? manualShowing : showing"
 				v-hotkey.global="keymap"
@@ -66,7 +66,7 @@
 					<slot :max-height="maxHeight" :type="type"></slot>
 				</div>
 			</div>
-		</focus-trap>
+		</FocusTrap>
 	</Transition>
 </template>
 
@@ -187,8 +187,8 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
 
 function onBgClick() {
 	if (contentClicking) return;
-	emit("click");
 	focusedElement.focus();
+	emit("click");
 }
 
 if (type === "drawer") {
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 48028422e..017bfae8c 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -6,7 +6,7 @@
 		@keyup.esc="$emit('close')"
 		@closed="$emit('closed')"
 	>
-		<focus-trap v-model:active="isActive">
+		<FocusTrap v-model:active="isActive">
 			<div
 				ref="rootEl"
 				class="ebkgoccj"
@@ -21,6 +21,7 @@
 						: '100%',
 				}"
 				@keydown="onKeydown"
+				tabindex="-1"
 			>
 				<div ref="headerEl" class="header">
 					<button
@@ -53,7 +54,7 @@
 					<slot :width="bodyWidth" :height="bodyHeight"></slot>
 				</div>
 			</div>
-		</focus-trap>
+		</FocusTrap>
 	</MkModal>
 </template>
 
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index a20afc39b..761334e1d 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -101,7 +101,7 @@
 					</div>
 				</template>
 				<div
-					v-if="!showContent"
+					v-if="note.cw && !showContent"
 					tabindex="0"
 					v-on:focus="cwButton?.focus()"
 				></div>

From 9e99df6e82bbd7ce5c9e22966d2b32eb245e04f1 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 21:22:29 -0400
Subject: [PATCH 31/96] fix subnote

---
 packages/client/src/components/MkNoteSub.vue | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index f5e70891f..a0b70ff1f 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -26,6 +26,7 @@
 						:note="note"
 						:parentId="appearNote.parentId"
 						:conversation="conversation"
+						@focusfooter="footerEl.focus()"
 					/>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -46,7 +47,7 @@
 						</div>
 					</div>
 				</div>
-				<footer class="footer" @click.stop>
+				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -212,6 +213,7 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
+const footerEl = ref<HTMLElement>();
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();

From d8a6ce64a36426961481293a56cb2e1daf339478 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 21:39:48 -0400
Subject: [PATCH 32/96] Fix focusing avatars in weird places

---
 packages/client/src/components/MkMenu.vue               | 4 +++-
 packages/client/src/components/MkUserSelectDialog.vue   | 2 ++
 packages/client/src/components/MkUsersTooltip.vue       | 2 +-
 packages/client/src/components/global/MkPageHeader.vue  | 2 ++
 packages/client/src/pages/admin/overview.moderators.vue | 2 +-
 packages/client/src/pages/follow-requests.vue           | 1 +
 packages/client/src/ui/_common_/navbar-for-mobile.vue   | 1 +
 packages/client/src/ui/_common_/navbar.vue              | 1 +
 packages/client/src/ui/classic.header.vue               | 1 +
 packages/client/src/ui/classic.sidebar.vue              | 1 +
 10 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index f22f0f9ca..0b5fb7455 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -46,6 +46,7 @@
 							v-if="item.avatar"
 							:user="item.avatar"
 							class="avatar"
+							disableLink
 						/>
 						<span :style="item.textStyle || ''">{{ item.text }}</span>
 						<span v-if="item.indicate" class="indicator"
@@ -88,7 +89,7 @@
 						@mouseenter.passive="onItemMouseEnter(item)"
 						@mouseleave.passive="onItemMouseLeave(item)"
 					>
-						<MkAvatar :user="item.user" class="avatar" /><MkUserName
+						<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
 							:user="item.user"
 						/>
 						<span v-if="item.indicate" class="indicator"
@@ -157,6 +158,7 @@
 							v-if="item.avatar"
 							:user="item.avatar"
 							class="avatar"
+							disableLink
 						/>
 						<span :style="item.textStyle || ''">{{ item.text }}</span>
 						<span v-if="item.indicate" class="indicator"
diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue
index 506f48bd4..14553ca46 100644
--- a/packages/client/src/components/MkUserSelectDialog.vue
+++ b/packages/client/src/components/MkUserSelectDialog.vue
@@ -46,6 +46,7 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
+							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
@@ -73,6 +74,7 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
+							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue
index 972864d1f..78a4f90f2 100644
--- a/packages/client/src/components/MkUsersTooltip.vue
+++ b/packages/client/src/components/MkUsersTooltip.vue
@@ -7,7 +7,7 @@
 	>
 		<div class="beaffaef">
 			<div v-for="u in users" :key="u.id" class="user">
-				<MkAvatar class="avatar" :user="u" />
+				<MkAvatar class="avatar" :user="u" disableLink />
 				<MkUserName class="name" :user="u" :nowrap="true" />
 			</div>
 			<div v-if="users.length < count" class="omitted">
diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue
index ad1d80ca6..c78ef0c10 100644
--- a/packages/client/src/components/global/MkPageHeader.vue
+++ b/packages/client/src/components/global/MkPageHeader.vue
@@ -19,6 +19,7 @@
 				class="avatar"
 				:user="$i"
 				:disable-preview="true"
+				disableLink
 			/>
 		</div>
 		<template v-if="metadata">
@@ -33,6 +34,7 @@
 					:user="metadata.avatar"
 					:disable-preview="true"
 					:show-indicator="true"
+					disableLink
 				/>
 				<i
 					v-else-if="metadata.icon && !narrow"
diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue
index 6184cfb10..db953b890 100644
--- a/packages/client/src/pages/admin/overview.moderators.vue
+++ b/packages/client/src/pages/admin/overview.moderators.vue
@@ -12,7 +12,7 @@
 					class="user"
 					:to="`/user-info/${user.id}`"
 				>
-					<MkAvatar :user="user" class="avatar" indicator />
+					<MkAvatar :user="user" class="avatar" indicator disableLink />
 				</MkA>
 			</div>
 		</Transition>
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index 2aac52163..35279495b 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -23,6 +23,7 @@
 								class="avatar"
 								:user="req.follower"
 								:show-indicator="true"
+								disableLink
 							/>
 							<div class="body">
 								<div class="name">
diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue
index 43c91d147..39abb7c26 100644
--- a/packages/client/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue
@@ -18,6 +18,7 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
+						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 1c69067e1..4fb27a071 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -18,6 +18,7 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
+						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
index 5c3e6b702..99a0ab098 100644
--- a/packages/client/src/ui/classic.header.vue
+++ b/packages/client/src/ui/classic.header.vue
@@ -83,6 +83,7 @@
 					<MkAvatar :user="$i" class="avatar" /><MkAcct
 						class="acct"
 						:user="$i"
+						disableLink
 					/>
 				</button>
 				<div class="post" @click="post">
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index b70a3c984..33aa62ed7 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -8,6 +8,7 @@
 			<MkAvatar :user="$i" class="avatar" /><MkAcct
 				class="text"
 				:user="$i"
+				disableLink
 			/>
 		</button>
 		<div class="post" data-cy-open-post-form @click="post">

From e52b3cef77fcbe21ed531b8aa343a5e150cd4894 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 21:49:41 -0400
Subject: [PATCH 33/96] Make accounts in account management page focusable

---
 packages/client/src/pages/settings/accounts.vue | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index ec2cd2477..3010354b6 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -6,14 +6,14 @@
 				{{ i18n.ts.addAccount }}</FormButton
 			>
 
-			<div
+			<button
 				v-for="account in accounts"
 				:key="account.id"
 				class="_panel _button lcjjdxlm"
 				@click="menu(account, $event)"
 			>
 				<div class="avatar">
-					<MkAvatar :user="account" class="avatar" />
+					<MkAvatar :user="account" class="avatar" disableLink />
 				</div>
 				<div class="body">
 					<div class="name">
@@ -23,7 +23,7 @@
 						<MkAcct :user="account" />
 					</div>
 				</div>
-			</div>
+			</button>
 		</FormSuspense>
 	</div>
 </template>
@@ -158,6 +158,8 @@ definePageMetadata({
 .lcjjdxlm {
 	display: flex;
 	padding: 16px;
+	width: 100%;
+	text-align: unset;
 
 	> .avatar {
 		display: block;

From 739e0331a84a2f14e881d3c881567e2c009144fd Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:04:35 -0400
Subject: [PATCH 34/96] make folder dropdown focusable

---
 packages/client/src/components/form/folder.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue
index 8868f5784..a2fde5341 100644
--- a/packages/client/src/components/form/folder.vue
+++ b/packages/client/src/components/form/folder.vue
@@ -1,6 +1,6 @@
 <template>
 	<div class="dwzlatin" :class="{ opened }">
-		<div class="header _button" @click="toggle">
+		<button class="header _button" @click="toggle">
 			<span class="icon"><slot name="icon"></slot></span>
 			<span class="text"><slot name="label"></slot></span>
 			<span class="right">
@@ -8,7 +8,7 @@
 				<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
 				<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
 			</span>
-		</div>
+		</button>
 		<KeepAlive>
 			<div v-if="openedAtLeastOnce" v-show="opened" class="body">
 				<MkSpacer :margin-min="14" :margin-max="22">

From d096d7ea9a48d434e1ffc249d759d14db8a4b01c Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:17:39 -0400
Subject: [PATCH 35/96] focus to media

---
 packages/client/src/components/MkDriveFileThumbnail.vue | 7 +++++--
 packages/client/src/components/MkMediaImage.vue         | 4 ++++
 packages/client/src/components/MkPostFormAttaches.vue   | 1 -
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/MkDriveFileThumbnail.vue b/packages/client/src/components/MkDriveFileThumbnail.vue
index 39150c10c..48b542817 100644
--- a/packages/client/src/components/MkDriveFileThumbnail.vue
+++ b/packages/client/src/components/MkDriveFileThumbnail.vue
@@ -1,5 +1,5 @@
 <template>
-	<div ref="thumbnail" class="zdjebgpv">
+	<button ref="thumbnail" class="zdjebgpv">
 		<ImgWithBlurhash
 			v-if="isThumbnailAvailable"
 			:hash="file.blurhash"
@@ -36,7 +36,7 @@
 			v-if="isThumbnailAvailable && is === 'video'"
 			class="ph-file-video ph-bold ph-lg icon-sub"
 		></i>
-	</div>
+	</button>
 </template>
 
 <script lang="ts" setup>
@@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => {
 	background: var(--panel);
 	border-radius: 8px;
 	overflow: clip;
+	border: 0;
+	padding: 0;
+	cursor: pointer;
 
 	> .icon-sub {
 		position: absolute;
diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue
index 882908040..055a9e9b1 100644
--- a/packages/client/src/components/MkMediaImage.vue
+++ b/packages/client/src/components/MkMediaImage.vue
@@ -138,6 +138,10 @@ watch(
 		background-position: center;
 		background-size: contain;
 		background-repeat: no-repeat;
+		&:focus {
+			border: 2px solid var(--accent);
+			box-sizing: border-box;
+		}
 
 		> .gif {
 			background-color: var(--fg);
diff --git a/packages/client/src/components/MkPostFormAttaches.vue b/packages/client/src/components/MkPostFormAttaches.vue
index ad2155155..29d5dc79c 100644
--- a/packages/client/src/components/MkPostFormAttaches.vue
+++ b/packages/client/src/components/MkPostFormAttaches.vue
@@ -198,7 +198,6 @@ export default defineComponent({
 			height: 64px;
 			margin-right: 4px;
 			border-radius: 4px;
-			overflow: hidden;
 			cursor: move;
 
 			&:hover > .remove {

From d21de526ed51347e86aa9e5cae967d0219c4c87a Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:19:02 -0400
Subject: [PATCH 36/96] a

---
 packages/client/src/components/MkMediaImage.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue
index 055a9e9b1..3cfb0f465 100644
--- a/packages/client/src/components/MkMediaImage.vue
+++ b/packages/client/src/components/MkMediaImage.vue
@@ -138,9 +138,9 @@ watch(
 		background-position: center;
 		background-size: contain;
 		background-repeat: no-repeat;
-		&:focus {
+		box-sizing: border-box;
+		&:focus-visible {
 			border: 2px solid var(--accent);
-			box-sizing: border-box;
 		}
 
 		> .gif {

From bd31d5d0af88a9d973e3edb103a4f3de51aca12a Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:24:40 -0400
Subject: [PATCH 37/96] fix

---
 packages/client/src/components/MkMenu.child.vue | 6 +-----
 packages/client/src/components/MkMenu.vue       | 1 +
 2 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index d5e9d774f..e5ca9e4ee 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,6 +1,5 @@
 <template>
-	<FocusTrap v-bind:active="isActive">
-		<div ref="el" class="sfhdhdhr" tabindex="-1">
+	<div ref="el" class="sfhdhdhr" tabindex="-1">
 			<MkMenu
 				ref="menu"
 				:items="items"
@@ -10,7 +9,6 @@
 				@close="onChildClosed"
 			/>
 		</div>
-	</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -25,8 +23,6 @@ import {
 } from "vue";
 import MkMenu from "./MkMenu.vue";
 import { MenuItem } from "@/types/menu";
-import { FocusTrap } from 'focus-trap-vue';
-import * as os from "@/os";
 
 const props = defineProps<{
 	items: MenuItem[];
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 0b5fb7455..c71e3ac58 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -115,6 +115,7 @@
 						class="_button item parent"
 						:class="{ childShowing: childShowingItem === item }"
 						@mouseenter="showChildren(item, $event)"
+						@click="showChildren(item, $event)"
 					>
 						<i
 							v-if="item.icon"

From 3647f78747c94fc56e9a8c81c5fe5449edcd6d82 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:40:23 -0400
Subject: [PATCH 38/96] sidebar focus

---
 packages/client/src/ui/_common_/navbar.vue | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 4fb27a071..20c177f37 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -335,6 +335,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -428,7 +429,8 @@ function more(ev: MouseEvent) {
 						text-overflow: ellipsis;
 					}
 
-					&:hover {
+					&:hover,
+					&:focus-within {
 						text-decoration: none;
 						color: var(--navHoverFg);
 						transition: all 0.4s ease;
@@ -530,6 +532,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -615,6 +618,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						text-decoration: none;
 						color: var(--accent);

From a66b334c3f055b64f5801f7ebdd5960c7444c6d6 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:50:32 -0400
Subject: [PATCH 39/96] widgets

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

diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue
index 07e845032..d48fc5383 100644
--- a/packages/client/src/components/MkWidgets.vue
+++ b/packages/client/src/components/MkWidgets.vue
@@ -1,7 +1,7 @@
 <template>
 	<div class="vjoppmmu">
 		<template v-if="edit">
-			<header>
+			<header tabindex="-1" v-focus>
 				<MkSelect
 					v-model="widgetAdderSelected"
 					style="margin-bottom: var(--margin)"

From 205c634d41f23deb2f57a18090d5f9ebe10706b9 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 23:18:01 -0400
Subject: [PATCH 40/96] classic view fixes

---
 packages/client/src/components/MkModalPageWindow.vue | 1 +
 packages/client/src/ui/classic.sidebar.vue           | 5 +++--
 packages/client/src/ui/classic.vue                   | 2 ++
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue
index 361128464..bf4d8d0bc 100644
--- a/packages/client/src/components/MkModalPageWindow.vue
+++ b/packages/client/src/components/MkModalPageWindow.vue
@@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) {
 	flex-direction: column;
 	contain: content;
 	border-radius: var(--radius);
+	margin: auto;
 
 	--root-margin: 24px;
 
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index 33aa62ed7..fa72c5765 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -5,10 +5,9 @@
 			class="item _button account"
 			@click="openAccountMenu"
 		>
-			<MkAvatar :user="$i" class="avatar" /><MkAcct
+			<MkAvatar :user="$i" class="avatar" disableLink /><MkAcct
 				class="text"
 				:user="$i"
-				disableLink
 			/>
 		</button>
 		<div class="post" data-cy-open-post-form @click="post">
@@ -300,6 +299,7 @@ function openInstanceMenu(ev: MouseEvent) {
 				width: 46px;
 				height: 46px;
 				padding: 0;
+				margin-inline: 0 !important;
 			}
 		}
 
@@ -373,6 +373,7 @@ function openInstanceMenu(ev: MouseEvent) {
 
 		> i {
 			width: 32px;
+			justify-content: center;
 		}
 
 		> i,
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index a721ffd0b..266effd9a 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -227,6 +227,8 @@ onMounted(() => {
 }
 
 .gbhvwtnk {
+	display: flex;
+	justify-content: center;
 	$ui-font-size: 1em;
 	$widgets-hide-threshold: 1200px;
 

From ba21f5446956279367aa22daabb37e5e5c2da00c Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 23:26:19 -0400
Subject: [PATCH 41/96] outline fixes

---
 packages/client/src/components/MkModal.vue           | 1 +
 packages/client/src/components/global/RouterView.vue | 1 +
 2 files changed, 2 insertions(+)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index c41fb50c0..12e79f428 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -490,6 +490,7 @@ defineExpose({
 }
 
 .root {
+	outline: none;
 	&.dialog {
 		> .content {
 			position: fixed;
diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index edb3fedaa..437b7c53e 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -7,6 +7,7 @@
 				v-bind="Object.fromEntries(currentPageProps)"
 				tabindex="-1"
 				v-focus
+				style="outline: none;"
 			/>
 
 			<template #fallback>

From cd4130e22b39cbdf927c27093166eba11d85fc7b Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 23:47:13 -0400
Subject: [PATCH 42/96] a

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

diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue
index 9d388e71b..6fdd79dc6 100644
--- a/packages/client/src/components/MkNotePreview.vue
+++ b/packages/client/src/components/MkNotePreview.vue
@@ -1,6 +1,6 @@
 <template>
 	<div v-size="{ min: [350, 500] }" class="fefdfafb">
-		<MkAvatar class="avatar" :user="$i" />
+		<MkAvatar class="avatar" :user="$i" disableLink />
 		<div class="main">
 			<div class="header">
 				<MkUserName :user="$i" />

From 3e952b34727a873170e99013a4eca5cd6ffa4321 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 29 Apr 2023 14:08:11 +0900
Subject: [PATCH 43/96] chore: update icons on post form

---
 packages/client/src/components/MkPostForm.vue         | 2 +-
 packages/client/src/components/MkPostFormAttaches.vue | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index ff63b77d8..f16d1775a 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -55,7 +55,7 @@
 					:class="{ active: showPreview }"
 					@click="showPreview = !showPreview"
 				>
-					<i class="ph-file-code ph-bold ph-lg"></i>
+					<i class="ph-binoculars ph-bold ph-lg"></i>
 				</button>
 				<button
 					class="submit _buttonGradate"
diff --git a/packages/client/src/components/MkPostFormAttaches.vue b/packages/client/src/components/MkPostFormAttaches.vue
index ad2155155..7c7f240e8 100644
--- a/packages/client/src/components/MkPostFormAttaches.vue
+++ b/packages/client/src/components/MkPostFormAttaches.vue
@@ -154,22 +154,22 @@ export default defineComponent({
 								? i18n.ts.unmarkAsSensitive
 								: i18n.ts.markAsSensitive,
 							icon: file.isSensitive
-								? "ph-eye-slash ph-bold ph-lg"
-								: "ph-eye ph-bold ph-lg",
+								? "ph-eye ph-bold ph-lg"
+								: "ph-eye-slash ph-bold ph-lg",
 							action: () => {
 								this.toggleSensitive(file);
 							},
 						},
 						{
 							text: i18n.ts.describeFile,
-							icon: "ph-cursor-text ph-bold ph-lg",
+							icon: "ph-subtitles ph-bold ph-lg",
 							action: () => {
 								this.describe(file);
 							},
 						},
 						{
 							text: i18n.ts.attachCancel,
-							icon: "ph-circle-wavy-warning ph-bold ph-lg",
+							icon: "ph-x ph-bold ph-lg",
 							action: () => {
 								this.detachMedia(file.id);
 							},

From d1dd8e28ab69f689bee1e05978575ddfcdc4f972 Mon Sep 17 00:00:00 2001
From: Kainoa Kanter <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 09:35:03 +0200
Subject: [PATCH 44/96] chore: Added translation using Weblate (Finnish)

---
 locales/fi.yml | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 locales/fi.yml

diff --git a/locales/fi.yml b/locales/fi.yml
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/locales/fi.yml
@@ -0,0 +1 @@
+{}

From d961239bd8abd0653be0598a860faac728fa189d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ville=20Lepist=C3=B6?= <ville.lepisto@protonmail.com>
Date: Sat, 29 Apr 2023 07:46:39 +0000
Subject: [PATCH 45/96] chore: Translated using Weblate (Finnish)

Currently translated at 2.4% (43 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/fi/
---
 locales/fi.yml | 44 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 43 insertions(+), 1 deletion(-)

diff --git a/locales/fi.yml b/locales/fi.yml
index 0967ef424..c9e5e06e0 100644
--- a/locales/fi.yml
+++ b/locales/fi.yml
@@ -1 +1,43 @@
-{}
+username: Käyttäjänimi
+fetchingAsApObject: Hae Fedeversestä
+gotIt: Selvä!
+cancel: Peruuta
+enterUsername: Anna käyttäjänimi
+renotedBy: Buustannut {käyttäjä}
+noNotes: Ei lähetyksiä
+noNotifications: Ei ilmoituksia
+instance: Instanssi
+settings: Asetukset
+basicSettings: Perusasetukset
+otherSettings: Muut asetukset
+openInWindow: Avaa ikkunaan
+profile: Profiili
+timeline: Aikajana
+noAccountDescription: Käyttäjä ei ole vielä kirjoittanut kuvaustaan vielä.
+login: Kirjaudu sisään
+loggingIn: Kirjautuu sisään
+logout: Kirjaudu ulos
+uploading: Tallentaa ylös...
+save: Tallenna
+favorites: Kirjanmerkit
+unfavorite: Poista kirjanmerkeistä
+favorited: Lisätty kirjanmerkkeihin.
+alreadyFavorited: Lisätty jo kirjanmerkkeihin.
+cantFavorite: Ei voitu lisätä kirjanmerkkeihin.
+pin: Kiinnitä profiiliin
+unpin: Irroita profiilista
+delete: Poista
+forgotPassword: Unohtunut salasana
+search: Etsi
+notifications: Ilmoitukset
+password: Salasana
+ok: OK
+noThankYou: Ei kiitos
+signup: Rekisteröidy
+users: Käyttäjät
+addUser: Lisää käyttäjä
+addInstance: Lisää instanssi
+favorite: Lisää kirjanmerkkeihin
+copyContent: Kopioi sisältö
+deleteAndEdit: Poista ja muokkaa
+copyLink: Kopioi linkki

From d1cdd58bc1ee5e1a31103f73f8597a61e48828fa Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sat, 29 Apr 2023 09:56:28 -0400
Subject: [PATCH 46/96] a

---
 packages/client/src/components/MkCwButton.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 3038f97f7..1f6340510 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -84,6 +84,7 @@ defineExpose({
 		bottom: 0;
 		left: 0;
 		width: 100%;
+		z-index: 2;
 		> span {
 			display: inline-block;
 			background: var(--panel);
@@ -92,7 +93,7 @@ defineExpose({
 			border-radius: 999px;
 			box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 		}
-		&:hover {
+		&:hover, &:focus {
 			> span {
 				background: var(--panelHighlight);
 			}

From dbc608a6c8e2d9d5f19c7865eeaf92400750ea98 Mon Sep 17 00:00:00 2001
From: naskya <naskya@noreply.codeberg.org>
Date: Sat, 29 Apr 2023 14:01:24 +0000
Subject: [PATCH 47/96] fix: centering block math (#9946)

Similar to `inlineCode` and `blockCode`, MFM provides two types of formula syntax, `mathInline` and `mathBlock` (I'm curious why these aren't called `inlineMath`/`blockMath`, but oh well)

Other platforms, like GitHub, **Math**todon, my blog, etc., also support these two types of formula representation, and math blocks are centered on (maybe) all such platforms.

![](https://cdn.discordapp.com/attachments/823878222897741868/1101837026304720997/2023-04-29_201943.png)

But Calckey (Misskey v12) don't center math blocks. I'd say this is a bug, and this makes `blockMath` useless (it's just `inlineMath` in a new line).

![](https://cdn.discordapp.com/attachments/823878222897741868/1101837026027917342/2023-04-29_202008.png)

So I fixed this.

![](https://cdn.discordapp.com/attachments/823878222897741868/1101837183574355978/2023-04-29_202854.png)

Co-authored-by: naskya <m@naskya.net>
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9946
Co-authored-by: naskya <naskya@noreply.codeberg.org>
Co-committed-by: naskya <naskya@noreply.codeberg.org>
---
 packages/client/src/components/MkFormulaCore.vue | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkFormulaCore.vue b/packages/client/src/components/MkFormulaCore.vue
index 5675dbfa0..2db4c7d00 100644
--- a/packages/client/src/components/MkFormulaCore.vue
+++ b/packages/client/src/components/MkFormulaCore.vue
@@ -20,9 +20,12 @@ export default defineComponent({
 	},
 	computed: {
 		compiledFormula(): any {
-			return katex.renderToString(this.formula, {
+			const katexString = katex.renderToString(this.formula, {
 				throwOnError: false,
 			} as any);
+			return this.block
+				? `<div style="text-align:center">${katexString}</div>`
+				: katexString;
 		},
 	},
 });

From 1dbcbe9dbb00b2280d806c0276f6b4565b3e49c0 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 13:28:24 -0700
Subject: [PATCH 48/96] chore: upgrade megalodon

---
 package.json                  |  2 +-
 packages/backend/package.json |  2 +-
 pnpm-lock.yaml                | 14 +++++++-------
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/package.json b/package.json
index 1f3b0e1a4..63b5f1dcd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "calckey",
-	"version": "13.2.0-dev38",
+	"version": "13.2.0-dev39",
 	"codename": "aqua",
 	"repository": {
 		"type": "git",
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 7b92b8311..ce0b2c2e1 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -26,7 +26,7 @@
 		"@bull-board/api": "^4.6.4",
 		"@bull-board/koa": "^4.6.4",
 		"@bull-board/ui": "^4.6.4",
-		"@calckey/megalodon": "5.1.24",
+		"@calckey/megalodon": "5.2.0",
 		"@discordapp/twemoji": "14.0.2",
 		"@elastic/elasticsearch": "7.17.0",
 		"@koa/cors": "3.4.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cf4c29c06..9f0b4195d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -81,8 +81,8 @@ importers:
         specifier: ^4.6.4
         version: 4.10.2
       '@calckey/megalodon':
-        specifier: 5.1.24
-        version: 5.1.24
+        specifier: 5.2.0
+        version: 5.2.0
       '@discordapp/twemoji':
         specifier: 14.0.2
         version: 14.0.2
@@ -1378,8 +1378,8 @@ packages:
       '@bull-board/api': 4.10.2
     dev: false
 
-  /@calckey/megalodon@5.1.24:
-    resolution: {integrity: sha512-VRd6x8MFQ2pMF0rnGF67/GVxgp/92CV7lg2XT1wnPAfQZ1NTsjwlDQX3HewEW3fSG/r7Nzh5WbIBXC8WMWKs9g==}
+  /@calckey/megalodon@5.2.0:
+    resolution: {integrity: sha512-9MEjzKJPyd7o5bHGGlNq4oE1tMt22GUJ8o8tZXcXSpXlrSDb2rSwumirM1KXUWTW8G6NGi1leCM59gOBGLko3w==}
     engines: {node: '>=15.0.0'}
     dependencies:
       '@types/oauth': 0.9.1
@@ -5914,8 +5914,8 @@ packages:
     requiresBuild: true
     dev: false
 
-  /core-js@3.30.0:
-    resolution: {integrity: sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==}
+  /core-js@3.30.1:
+    resolution: {integrity: sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==}
     requiresBuild: true
     dev: true
 
@@ -15402,7 +15402,7 @@ packages:
     name: plyr
     version: 3.7.0
     dependencies:
-      core-js: 3.30.0
+      core-js: 3.30.1
       custom-event-polyfill: 1.0.7
       loadjs: 4.2.0
       rangetouch: 2.0.1

From 4dae793c5eaad657729a028a0a31e2c301cd51df Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 13:38:52 -0700
Subject: [PATCH 49/96] docs

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index af117ac4a..d05cb55db 100644
--- a/README.md
+++ b/README.md
@@ -27,9 +27,9 @@
 - Notable differences:
   - Improved UI/UX (especially on mobile)
   - Improved notifications
-  - Fediverse account migration
   - Improved instance security
   - Improved accessibility
+  - Improved threads
   - Recommended Instances timeline
   - OCR image captioning
   - New and improved Groups

From 08686da50e8167c1309685a65cd27bc3f8af27c9 Mon Sep 17 00:00:00 2001
From: Free <freeplay@duck.com>
Date: Sat, 29 Apr 2023 22:30:14 +0000
Subject: [PATCH 50/96] keyboard accessibility (#9725)

Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9725
Co-authored-by: Free <freeplay@duck.com>
Co-committed-by: Free <freeplay@duck.com>
---
 package.json                                  |   2 +
 packages/client/src/components/MkButton.vue   |   3 +-
 packages/client/src/components/MkCwButton.vue |  18 +-
 .../src/components/MkDriveFileThumbnail.vue   |   7 +-
 .../client/src/components/MkEmojiPicker.vue   | 278 ++++++-------
 .../client/src/components/MkLaunchPad.vue     |   2 +-
 .../client/src/components/MkMediaImage.vue    |   4 +
 .../client/src/components/MkMenu.child.vue    |  21 +-
 packages/client/src/components/MkMenu.vue     | 378 +++++++++---------
 packages/client/src/components/MkModal.vue    |  86 ++--
 .../src/components/MkModalPageWindow.vue      |   1 +
 .../client/src/components/MkModalWindow.vue   |  95 ++---
 packages/client/src/components/MkNote.vue     |   8 +-
 .../client/src/components/MkNotePreview.vue   |   2 +-
 packages/client/src/components/MkNoteSub.vue  |   4 +-
 .../client/src/components/MkPopupMenu.vue     |   2 +
 .../src/components/MkPostFormAttaches.vue     |   1 -
 .../src/components/MkSubNoteContent.vue       |  27 +-
 .../client/src/components/MkSuperMenu.vue     |   5 +-
 .../src/components/MkUserSelectDialog.vue     |   2 +
 .../client/src/components/MkUsersTooltip.vue  |   2 +-
 packages/client/src/components/MkWidgets.vue  |   2 +-
 .../client/src/components/form/folder.vue     |   4 +-
 packages/client/src/components/form/radio.vue |   3 +
 .../client/src/components/form/switch.vue     |   3 +
 .../src/components/global/MkPageHeader.vue    |   2 +
 .../src/components/global/RouterView.vue      |   3 +
 packages/client/src/directives/focus.ts       |   3 +
 packages/client/src/directives/index.ts       |   2 +
 packages/client/src/directives/tooltip.ts     |  33 +-
 packages/client/src/pages/admin/_header_.vue  |   6 +-
 .../src/pages/admin/overview.moderators.vue   |   2 +-
 packages/client/src/pages/follow-requests.vue |   1 +
 .../client/src/pages/settings/accounts.vue    |   8 +-
 packages/client/src/style.scss                |   4 -
 .../src/ui/_common_/navbar-for-mobile.vue     |   1 +
 packages/client/src/ui/_common_/navbar.vue    |  21 +-
 packages/client/src/ui/classic.header.vue     |   1 +
 packages/client/src/ui/classic.sidebar.vue    |   4 +-
 packages/client/src/ui/classic.vue            |   2 +
 pnpm-lock.yaml                                |  41 +-
 41 files changed, 601 insertions(+), 493 deletions(-)
 create mode 100644 packages/client/src/directives/focus.ts

diff --git a/package.json b/package.json
index 63b5f1dcd..0b681f6d5 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
 		"@bull-board/ui": "^4.10.2",
 		"@napi-rs/cli": "^2.15.0",
 		"@tensorflow/tfjs": "^3.21.0",
+		"focus-trap": "^7.2.0",
+		"focus-trap-vue": "^4.0.1",
 		"js-yaml": "4.1.0",
 		"seedrandom": "^3.0.5"
 	},
diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue
index 5f1a5bdb7..feac281d9 100644
--- a/packages/client/src/components/MkButton.vue
+++ b/packages/client/src/components/MkButton.vue
@@ -195,8 +195,7 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&:focus-visible {
-		outline: solid 2px var(--focus);
-		outline-offset: 2px;
+		outline: auto;
 	}
 
 	&.inline {
diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 659cb1fbb..1f6340510 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -1,5 +1,6 @@
 <template>
 	<button
+		ref="el"
 		class="_button"
 		:class="{ showLess: modelValue, fade: !modelValue }"
 		@click.stop="toggle"
@@ -12,7 +13,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from "vue";
+import { computed, ref } from "vue";
 import { length } from "stringz";
 import * as misskey from "calckey-js";
 import { concat } from "@/scripts/array";
@@ -27,6 +28,8 @@ const emit = defineEmits<{
 	(ev: "update:modelValue", v: boolean): void;
 }>();
 
+const el = ref<HTMLElement>(); 
+
 const label = computed(() => {
 	return concat([
 		props.note.text
@@ -43,6 +46,14 @@ const label = computed(() => {
 const toggle = () => {
 	emit("update:modelValue", !props.modelValue);
 };
+
+function focus() {
+	el.value.focus();
+}
+
+defineExpose({
+	focus
+});
 </script>
 
 <style lang="scss" scoped>
@@ -62,7 +73,7 @@ const toggle = () => {
 			}
 		}
 	}
-	&:hover > span {
+	&:hover > span, &:focus > span {
 		background: var(--cwFg) !important;
 		color: var(--cwBg) !important;
 	}
@@ -73,6 +84,7 @@ const toggle = () => {
 		bottom: 0;
 		left: 0;
 		width: 100%;
+		z-index: 2;
 		> span {
 			display: inline-block;
 			background: var(--panel);
@@ -81,7 +93,7 @@ const toggle = () => {
 			border-radius: 999px;
 			box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 		}
-		&:hover {
+		&:hover, &:focus {
 			> span {
 				background: var(--panelHighlight);
 			}
diff --git a/packages/client/src/components/MkDriveFileThumbnail.vue b/packages/client/src/components/MkDriveFileThumbnail.vue
index 39150c10c..48b542817 100644
--- a/packages/client/src/components/MkDriveFileThumbnail.vue
+++ b/packages/client/src/components/MkDriveFileThumbnail.vue
@@ -1,5 +1,5 @@
 <template>
-	<div ref="thumbnail" class="zdjebgpv">
+	<button ref="thumbnail" class="zdjebgpv">
 		<ImgWithBlurhash
 			v-if="isThumbnailAvailable"
 			:hash="file.blurhash"
@@ -36,7 +36,7 @@
 			v-if="isThumbnailAvailable && is === 'video'"
 			class="ph-file-video ph-bold ph-lg icon-sub"
 		></i>
-	</div>
+	</button>
 </template>
 
 <script lang="ts" setup>
@@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => {
 	background: var(--panel);
 	border-radius: 8px;
 	overflow: clip;
+	border: 0;
+	padding: 0;
+	cursor: pointer;
 
 	> .icon-sub {
 		position: absolute;
diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index a22006951..88d207bab 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -1,157 +1,160 @@
 <template>
-	<div
-		class="omfetrab"
-		:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
-		:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
-	>
-		<input
-			ref="search"
-			v-model.trim="q"
-			class="search"
-			data-prevent-emoji-insert
-			:class="{ filled: q != null && q != '' }"
-			:placeholder="i18n.ts.search"
-			type="search"
-			@paste.stop="paste"
-			@keyup.enter="done()"
-		/>
-		<div ref="emojis" class="emojis">
-			<section class="result">
-				<div v-if="searchResultCustom.length > 0" class="body">
-					<button
-						v-for="emoji in searchResultCustom"
-						:key="emoji.id"
-						class="_button item"
-						:title="emoji.name"
-						tabindex="0"
-						@click="chosen(emoji, $event)"
-					>
-						<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
-						<img
-							class="emoji"
-							:src="
-								disableShowingAnimatedImages
-									? getStaticImageUrl(emoji.url)
-									: emoji.url
-							"
-						/>
-					</button>
-				</div>
-				<div v-if="searchResultUnicode.length > 0" class="body">
-					<button
-						v-for="emoji in searchResultUnicode"
-						:key="emoji.name"
-						class="_button item"
-						:title="emoji.name"
-						tabindex="0"
-						@click="chosen(emoji, $event)"
-					>
-						<MkEmoji class="emoji" :emoji="emoji.char" />
-					</button>
-				</div>
-			</section>
-
-			<div v-if="tab === 'index'" class="group index">
-				<section v-if="showPinned">
-					<div class="body">
+	<FocusTrap v-bind:active="isActive">
+		<div
+			class="omfetrab"
+			:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
+			:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
+			tabindex="-1"
+		>
+			<input
+				ref="search"
+				v-model.trim="q"
+				class="search"
+				data-prevent-emoji-insert
+				:class="{ filled: q != null && q != '' }"
+				:placeholder="i18n.ts.search"
+				type="search"
+				@paste.stop="paste"
+				@keyup.enter="done()"
+			/>
+			<div ref="emojis" class="emojis">
+				<section class="result">
+					<div v-if="searchResultCustom.length > 0" class="body">
 						<button
-							v-for="emoji in pinned"
-							:key="emoji"
+							v-for="emoji in searchResultCustom"
+							:key="emoji.id"
 							class="_button item"
+							:title="emoji.name"
 							tabindex="0"
 							@click="chosen(emoji, $event)"
 						>
-							<MkEmoji
+							<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+							<img
 								class="emoji"
-								:emoji="emoji"
-								:normal="true"
+								:src="
+									disableShowingAnimatedImages
+										? getStaticImageUrl(emoji.url)
+										: emoji.url
+								"
 							/>
 						</button>
 					</div>
+					<div v-if="searchResultUnicode.length > 0" class="body">
+						<button
+							v-for="emoji in searchResultUnicode"
+							:key="emoji.name"
+							class="_button item"
+							:title="emoji.name"
+							tabindex="0"
+							@click="chosen(emoji, $event)"
+						>
+							<MkEmoji class="emoji" :emoji="emoji.char" />
+						</button>
+					</div>
 				</section>
 
-				<section>
-					<header class="_acrylic">
-						<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
-						{{ i18n.ts.recentUsed }}
-					</header>
-					<div class="body">
-						<button
-							v-for="emoji in recentlyUsedEmojis"
-							:key="emoji"
-							class="_button item"
-							@click="chosen(emoji, $event)"
-						>
-							<MkEmoji
-								class="emoji"
-								:emoji="emoji"
-								:normal="true"
-							/>
-						</button>
-					</div>
-				</section>
+				<div v-if="tab === 'index'" class="group index">
+					<section v-if="showPinned">
+						<div class="body">
+							<button
+								v-for="emoji in pinned"
+								:key="emoji"
+								class="_button item"
+								tabindex="0"
+								@click="chosen(emoji, $event)"
+							>
+								<MkEmoji
+									class="emoji"
+									:emoji="emoji"
+									:normal="true"
+								/>
+							</button>
+						</div>
+					</section>
+
+					<section>
+						<header class="_acrylic">
+							<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
+							{{ i18n.ts.recentUsed }}
+						</header>
+						<div class="body">
+							<button
+								v-for="emoji in recentlyUsedEmojis"
+								:key="emoji"
+								class="_button item"
+								@click="chosen(emoji, $event)"
+							>
+								<MkEmoji
+									class="emoji"
+									:emoji="emoji"
+									:normal="true"
+								/>
+							</button>
+						</div>
+					</section>
+				</div>
+				<div v-once class="group">
+					<header>{{ i18n.ts.customEmojis }}</header>
+					<XSection
+						v-for="category in customEmojiCategories"
+						:key="'custom:' + category"
+						:initial-shown="false"
+						:emojis="
+							customEmojis
+								.filter((e) => e.category === category)
+								.map((e) => ':' + e.name + ':')
+						"
+						@chosen="chosen"
+						>{{ category || i18n.ts.other }}</XSection
+					>
+				</div>
+				<div v-once class="group">
+					<header>{{ i18n.ts.emoji }}</header>
+					<XSection
+						v-for="category in categories"
+						:key="category"
+						:emojis="
+							emojilist
+								.filter((e) => e.category === category)
+								.map((e) => e.char)
+						"
+						@chosen="chosen"
+						>{{ category }}</XSection
+					>
+				</div>
 			</div>
-			<div v-once class="group">
-				<header>{{ i18n.ts.customEmojis }}</header>
-				<XSection
-					v-for="category in customEmojiCategories"
-					:key="'custom:' + category"
-					:initial-shown="false"
-					:emojis="
-						customEmojis
-							.filter((e) => e.category === category)
-							.map((e) => ':' + e.name + ':')
-					"
-					@chosen="chosen"
-					>{{ category || i18n.ts.other }}</XSection
+			<div class="tabs">
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'index' }"
+					@click="tab = 'index'"
 				>
-			</div>
-			<div v-once class="group">
-				<header>{{ i18n.ts.emoji }}</header>
-				<XSection
-					v-for="category in categories"
-					:key="category"
-					:emojis="
-						emojilist
-							.filter((e) => e.category === category)
-							.map((e) => e.char)
-					"
-					@chosen="chosen"
-					>{{ category }}</XSection
+					<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'custom' }"
+					@click="tab = 'custom'"
 				>
+					<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'unicode' }"
+					@click="tab = 'unicode'"
+				>
+					<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'tags' }"
+					@click="tab = 'tags'"
+				>
+					<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
 			</div>
 		</div>
-		<div class="tabs">
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'index' }"
-				@click="tab = 'index'"
-			>
-				<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'custom' }"
-				@click="tab = 'custom'"
-			>
-				<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'unicode' }"
-				@click="tab = 'unicode'"
-			>
-				<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'tags' }"
-				@click="tab = 'tags'"
-			>
-				<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-		</div>
-	</div>
+	</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -171,6 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
 import { emojiCategories, instance } from "@/instance";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
+import { FocusTrap } from 'focus-trap-vue';
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index f713b4c41..759c215f7 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -139,7 +139,7 @@ function close() {
 			height: 100px;
 			border-radius: 10px;
 
-			&:hover {
+			&:hover, &:focus-visible {
 				color: var(--accent);
 				background: var(--accentedBg);
 				text-decoration: none;
diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue
index 882908040..3cfb0f465 100644
--- a/packages/client/src/components/MkMediaImage.vue
+++ b/packages/client/src/components/MkMediaImage.vue
@@ -138,6 +138,10 @@ watch(
 		background-position: center;
 		background-size: contain;
 		background-repeat: no-repeat;
+		box-sizing: border-box;
+		&:focus-visible {
+			border: 2px solid var(--accent);
+		}
 
 		> .gif {
 			background-color: var(--fg);
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index 6b05ab447..e5ca9e4ee 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,14 +1,14 @@
 <template>
-	<div ref="el" class="sfhdhdhr">
-		<MkMenu
-			ref="menu"
-			:items="items"
-			:align="align"
-			:width="width"
-			:as-drawer="false"
-			@close="onChildClosed"
-		/>
-	</div>
+	<div ref="el" class="sfhdhdhr" tabindex="-1">
+			<MkMenu
+				ref="menu"
+				:items="items"
+				:align="align"
+				:width="width"
+				:as-drawer="false"
+				@close="onChildClosed"
+			/>
+		</div>
 </template>
 
 <script lang="ts" setup>
@@ -23,7 +23,6 @@ import {
 } from "vue";
 import MkMenu from "./MkMenu.vue";
 import { MenuItem } from "@/types/menu";
-import * as os from "@/os";
 
 const props = defineProps<{
 	items: MenuItem[];
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 88c8af1c5..c71e3ac58 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,191 +1,188 @@
 <template>
-	<div>
-		<div
-			ref="itemsEl"
-			v-hotkey="keymap"
-			class="rrevdjwt _popup _shadow"
-			:class="{ center: align === 'center', asDrawer }"
-			:style="{
-				width: width && !asDrawer ? width + 'px' : '',
-				maxHeight: maxHeight ? maxHeight + 'px' : '',
-			}"
-			@contextmenu.self="(e) => e.preventDefault()"
-		>
-			<template v-for="(item, i) in items2">
-				<div v-if="item === null" class="divider"></div>
-				<span v-else-if="item.type === 'label'" class="label item">
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-				</span>
-				<span
-					v-else-if="item.type === 'pending'"
-					:tabindex="i"
-					class="pending item"
-				>
-					<span><MkEllipsis /></span>
-				</span>
-				<MkA
-					v-else-if="item.type === 'link'"
-					:to="item.to"
-					:tabindex="i"
-					class="_button item"
-					@click.passive="close(true)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
+	<FocusTrap v-bind:active="isActive">
+		<div tabindex="-1" v-focus>
+			<div
+				ref="itemsEl"
+				class="rrevdjwt _popup _shadow"
+				:class="{ center: align === 'center', asDrawer }"
+				:style="{
+					width: width && !asDrawer ? width + 'px' : '',
+					maxHeight: maxHeight ? maxHeight + 'px' : '',
+				}"
+				@contextmenu.self="(e) => e.preventDefault()"
+			>
+				<template v-for="(item, i) in items2">
+					<div v-if="item === null" class="divider"></div>
+					<span v-else-if="item.type === 'label'" class="label item">
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
 					</span>
-					<MkAvatar
-						v-if="item.avatar"
-						:user="item.avatar"
-						class="avatar"
-					/>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</MkA>
-				<a
-					v-else-if="item.type === 'a'"
-					:href="item.href"
-					:target="item.target"
-					:download="item.download"
-					:tabindex="i"
-					class="_button item"
-					@click="close(true)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</a>
-				<button
-					v-else-if="item.type === 'user' && !items.hidden"
-					:tabindex="i"
-					class="_button item"
-					:class="{ active: item.active }"
-					:disabled="item.active"
-					@click="clicked(item.action, $event)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<MkAvatar :user="item.user" class="avatar" /><MkUserName
-						:user="item.user"
-					/>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</button>
-				<span
-					v-else-if="item.type === 'switch'"
-					:tabindex="i"
-					class="item"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<FormSwitch
-						v-model="item.ref"
-						:disabled="item.disabled"
-						class="form-switch"
-						:style="item.textStyle || ''"
-						>{{ item.text }}</FormSwitch
+					<span
+						v-else-if="item.type === 'pending'"
+						class="pending item"
 					>
+						<span><MkEllipsis /></span>
+					</span>
+					<MkA
+						v-else-if="item.type === 'link'"
+						:to="item.to"
+						class="_button item"
+						@click.passive="close(true)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<MkAvatar
+							v-if="item.avatar"
+							:user="item.avatar"
+							class="avatar"
+							disableLink
+						/>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</MkA>
+					<a
+						v-else-if="item.type === 'a'"
+						:href="item.href"
+						:target="item.target"
+						:download="item.download"
+						class="_button item"
+						@click="close(true)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</a>
+					<button
+						v-else-if="item.type === 'user' && !items.hidden"
+						class="_button item"
+						:class="{ active: item.active }"
+						:disabled="item.active"
+						@click="clicked(item.action, $event)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
+							:user="item.user"
+						/>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</button>
+					<span
+						v-else-if="item.type === 'switch'"
+						class="item"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<FormSwitch
+							v-model="item.ref"
+							:disabled="item.disabled"
+							class="form-switch"
+							:style="item.textStyle || ''"
+							>{{ item.text }}</FormSwitch
+						>
+					</span>
+					<button
+						v-else-if="item.type === 'parent'"
+						class="_button item parent"
+						:class="{ childShowing: childShowingItem === item }"
+						@mouseenter="showChildren(item, $event)"
+						@click="showChildren(item, $event)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span class="caret"
+							><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
+						></span>
+					</button>
+					<button
+						v-else-if="!item.hidden"
+						class="_button item"
+						:class="{ danger: item.danger, active: item.active }"
+						:disabled="item.active"
+						@click="clicked(item.action, $event)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<MkAvatar
+							v-if="item.avatar"
+							:user="item.avatar"
+							class="avatar"
+							disableLink
+						/>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</button>
+				</template>
+				<span v-if="items2.length === 0" class="none item">
+					<span>{{ i18n.ts.none }}</span>
 				</span>
-				<button
-					v-else-if="item.type === 'parent'"
-					:tabindex="i"
-					class="_button item parent"
-					:class="{ childShowing: childShowingItem === item }"
-					@mouseenter="showChildren(item, $event)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span class="caret"
-						><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
-					></span>
-				</button>
-				<button
-					v-else-if="!item.hidden"
-					:tabindex="i"
-					class="_button item"
-					:class="{ danger: item.danger, active: item.active }"
-					:disabled="item.active"
-					@click="clicked(item.action, $event)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<MkAvatar
-						v-if="item.avatar"
-						:user="item.avatar"
-						class="avatar"
-					/>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</button>
-			</template>
-			<span v-if="items2.length === 0" class="none item">
-				<span>{{ i18n.ts.none }}</span>
-			</span>
+			</div>
+			<div v-if="childMenu" class="child">
+				<XChild
+					ref="child"
+					:items="childMenu"
+					:target-element="childTarget"
+					:root-element="itemsEl"
+					showing
+					@actioned="childActioned"
+				/>
+			</div>
 		</div>
-		<div v-if="childMenu" class="child">
-			<XChild
-				ref="child"
-				:items="childMenu"
-				:target-element="childTarget"
-				:root-element="itemsEl"
-				showing
-				@actioned="childActioned"
-			/>
-		</div>
-	</div>
+	</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -206,6 +203,7 @@ import FormSwitch from "@/components/form/switch.vue";
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
+import { FocusTrap } from 'focus-trap-vue';
 
 const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
 
@@ -228,12 +226,6 @@ let items2: InnerMenuItem[] = $ref([]);
 
 let child = $ref<InstanceType<typeof XChild>>();
 
-let keymap = computed(() => ({
-	"up|k|shift+tab": focusUp,
-	"down|j|tab": focusDown,
-	esc: close,
-}));
-
 let childShowingItem = $ref<MenuItem | null>();
 
 watch(
@@ -364,8 +356,7 @@ onBeforeUnmount(() => {
 		font-size: 0.9em;
 		line-height: 20px;
 		text-align: left;
-		overflow: hidden;
-		text-overflow: ellipsis;
+		outline: none;
 
 		&:before {
 			content: "";
@@ -389,7 +380,7 @@ onBeforeUnmount(() => {
 			transform: translateY(0em);
 		}
 
-		&:not(:disabled):hover {
+		&:not(:disabled):hover, &:focus-visible {
 			color: var(--accent);
 			text-decoration: none;
 
@@ -397,6 +388,9 @@ onBeforeUnmount(() => {
 				background: var(--accentedBg);
 			}
 		}
+		&:focus-visible:before {
+			outline: auto;
+		}
 
 		&.danger {
 			color: #eb6f92;
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index d9cd56f95..12e79f428 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -14,54 +14,59 @@
 		:duration="transitionDuration"
 		appear
 		@after-leave="emit('closed')"
+		@keyup.esc="emit('click')"
 		@enter="emit('opening')"
 		@after-enter="onOpened"
 	>
-		<div
-			v-show="manualShowing != null ? manualShowing : showing"
-			v-hotkey.global="keymap"
-			:class="[
-				$style.root,
-				{
-					[$style.drawer]: type === 'drawer',
-					[$style.dialog]: type === 'dialog' || type === 'dialog:top',
-					[$style.popup]: type === 'popup',
-				},
-			]"
-			:style="{
-				zIndex,
-				pointerEvents: (manualShowing != null ? manualShowing : showing)
-					? 'auto'
-					: 'none',
-				'--transformOrigin': transformOrigin,
-			}"
-		>
+		<FocusTrap v-model:active="isActive">
 			<div
-				class="_modalBg data-cy-bg"
+				v-show="manualShowing != null ? manualShowing : showing"
+				v-hotkey.global="keymap"
 				:class="[
-					$style.bg,
+					$style.root,
 					{
-						[$style.bgTransparent]: isEnableBgTransparent,
-						'data-cy-transparent': isEnableBgTransparent,
+						[$style.drawer]: type === 'drawer',
+						[$style.dialog]: type === 'dialog' || type === 'dialog:top',
+						[$style.popup]: type === 'popup',
 					},
 				]"
-				:style="{ zIndex }"
-				@click="onBgClick"
-				@mousedown="onBgClick"
-				@contextmenu.prevent.stop="() => {}"
-			></div>
-			<div
-				ref="content"
-				:class="[
-					$style.content,
-					{ [$style.fixed]: fixed, top: type === 'dialog:top' },
-				]"
-				:style="{ zIndex }"
-				@click.self="onBgClick"
+				:style="{
+					zIndex,
+					pointerEvents: (manualShowing != null ? manualShowing : showing)
+						? 'auto'
+						: 'none',
+					'--transformOrigin': transformOrigin,
+				}"
+				tabindex="-1"
+				v-focus
 			>
-				<slot :max-height="maxHeight" :type="type"></slot>
+				<div
+					class="_modalBg data-cy-bg"
+					:class="[
+						$style.bg,
+						{
+							[$style.bgTransparent]: isEnableBgTransparent,
+							'data-cy-transparent': isEnableBgTransparent,
+						},
+					]"
+					:style="{ zIndex }"
+					@click="onBgClick"
+					@mousedown="onBgClick"
+					@contextmenu.prevent.stop="() => {}"
+				></div>
+				<div
+					ref="content"
+					:class="[
+						$style.content,
+						{ [$style.fixed]: fixed, top: type === 'dialog:top' },
+					]"
+					:style="{ zIndex }"
+					@click.self="onBgClick"
+				>
+					<slot :max-height="maxHeight" :type="type"></slot>
+				</div>
 			</div>
-		</div>
+		</FocusTrap>
 	</Transition>
 </template>
 
@@ -71,6 +76,7 @@ import * as os from "@/os";
 import { isTouchUsing } from "@/scripts/touch";
 import { defaultStore } from "@/store";
 import { deviceKind } from "@/scripts/device-kind";
+import { FocusTrap } from 'focus-trap-vue';
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === "BODY") return null;
@@ -166,6 +172,7 @@ let transitionDuration = $computed(() =>
 
 let contentClicking = false;
 
+const focusedElement = document.activeElement;
 function close(opts: { useSendAnimation?: boolean } = {}) {
 	if (opts.useSendAnimation) {
 		useSendAnime = true;
@@ -175,10 +182,12 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
 	if (props.src) props.src.style.pointerEvents = "auto";
 	showing = false;
 	emit("close");
+	focusedElement.focus();
 }
 
 function onBgClick() {
 	if (contentClicking) return;
+	focusedElement.focus();
 	emit("click");
 }
 
@@ -481,6 +490,7 @@ defineExpose({
 }
 
 .root {
+	outline: none;
 	&.dialog {
 		> .content {
 			position: fixed;
diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue
index 361128464..bf4d8d0bc 100644
--- a/packages/client/src/components/MkModalPageWindow.vue
+++ b/packages/client/src/components/MkModalPageWindow.vue
@@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) {
 	flex-direction: column;
 	contain: content;
 	border-radius: var(--radius);
+	margin: auto;
 
 	--root-margin: 24px;
 
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 3afcff6cb..017bfae8c 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -3,59 +3,64 @@
 		ref="modal"
 		:prefer-type="'dialog'"
 		@click="onBgClick"
+		@keyup.esc="$emit('close')"
 		@closed="$emit('closed')"
 	>
-		<div
-			ref="rootEl"
-			class="ebkgoccj"
-			:style="{
-				width: `${width}px`,
-				height: scroll
-					? height
-						? `${height}px`
-						: null
-					: height
-					? `min(${height}px, 100%)`
-					: '100%',
-			}"
-			@keydown="onKeydown"
-		>
-			<div ref="headerEl" class="header">
-				<button
-					v-if="withOkButton"
-					class="_button"
-					@click="$emit('close')"
-				>
-					<i class="ph-x ph-bold ph-lg"></i>
-				</button>
-				<span class="title">
-					<slot name="header"></slot>
-				</span>
-				<button
-					v-if="!withOkButton"
-					class="_button"
-					@click="$emit('close')"
-				>
-					<i class="ph-x ph-bold ph-lg"></i>
-				</button>
-				<button
-					v-if="withOkButton"
-					class="_button"
-					:disabled="okButtonDisabled"
-					@click="$emit('ok')"
-				>
-					<i class="ph-check ph-bold ph-lg"></i>
-				</button>
+		<FocusTrap v-model:active="isActive">
+			<div
+				ref="rootEl"
+				class="ebkgoccj"
+				:style="{
+					width: `${width}px`,
+					height: scroll
+						? height
+							? `${height}px`
+							: null
+						: height
+						? `min(${height}px, 100%)`
+						: '100%',
+				}"
+				@keydown="onKeydown"
+				tabindex="-1"
+			>
+				<div ref="headerEl" class="header">
+					<button
+						v-if="withOkButton"
+						class="_button"
+						@click="$emit('close')"
+					>
+						<i class="ph-x ph-bold ph-lg"></i>
+					</button>
+					<span class="title">
+						<slot name="header"></slot>
+					</span>
+					<button
+						v-if="!withOkButton"
+						class="_button"
+						@click="$emit('close')"
+					>
+						<i class="ph-x ph-bold ph-lg"></i>
+					</button>
+					<button
+						v-if="withOkButton"
+						class="_button"
+						:disabled="okButtonDisabled"
+						@click="$emit('ok')"
+					>
+						<i class="ph-check ph-bold ph-lg"></i>
+					</button>
+				</div>
+				<div class="body">
+					<slot :width="bodyWidth" :height="bodyHeight"></slot>
+				</div>
 			</div>
-			<div class="body">
-				<slot :width="bodyWidth" :height="bodyHeight"></slot>
-			</div>
-		</div>
+		</FocusTrap>
 	</MkModal>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted } from "vue";
+import { FocusTrap } from 'focus-trap-vue';
 import MkModal from "./MkModal.vue";
 
 const props = withDefaults(
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index 22a7ef93f..5d9c40d38 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -84,6 +84,7 @@
 						:detailedView="detailedView"
 						:parentId="appearNote.parentId"
 						@push="(e) => router.push(notePage(e))"
+						@focusfooter="footerEl.focus()"
 					></MkSubNoteContent>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -117,7 +118,7 @@
 						<MkTime :time="appearNote.createdAt" mode="absolute" />
 					</MkA>
 				</div>
-				<footer ref="el" class="footer" @click.stop>
+				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -278,6 +279,7 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
+const footerEl = ref<HTMLElement>(); 
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
@@ -298,8 +300,8 @@ const keymap = {
 	r: () => reply(true),
 	"e|a|plus": () => react(true),
 	q: () => renoteButton.value.renote(true),
-	"up|k|shift+tab": focusBefore,
-	"down|j|tab": focusAfter,
+	"up|k": focusBefore,
+	"down|j": focusAfter,
 	esc: blur,
 	"m|o": () => menu(true),
 	s: () => showContent.value !== showContent.value,
diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue
index 9d388e71b..6fdd79dc6 100644
--- a/packages/client/src/components/MkNotePreview.vue
+++ b/packages/client/src/components/MkNotePreview.vue
@@ -1,6 +1,6 @@
 <template>
 	<div v-size="{ min: [350, 500] }" class="fefdfafb">
-		<MkAvatar class="avatar" :user="$i" />
+		<MkAvatar class="avatar" :user="$i" disableLink />
 		<div class="main">
 			<div class="header">
 				<MkUserName :user="$i" />
diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index f5e70891f..a0b70ff1f 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -26,6 +26,7 @@
 						:note="note"
 						:parentId="appearNote.parentId"
 						:conversation="conversation"
+						@focusfooter="footerEl.focus()"
 					/>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -46,7 +47,7 @@
 						</div>
 					</div>
 				</div>
-				<footer class="footer" @click.stop>
+				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -212,6 +213,7 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
+const footerEl = ref<HTMLElement>();
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index 4d52616e1..5f1ed037b 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -7,6 +7,8 @@
 		:transparent-bg="true"
 		@click="modal.close()"
 		@closed="emit('closed')"
+		tabindex="-1"
+		v-focus
 	>
 		<MkMenu
 			:items="items"
diff --git a/packages/client/src/components/MkPostFormAttaches.vue b/packages/client/src/components/MkPostFormAttaches.vue
index 7c7f240e8..7cf397e55 100644
--- a/packages/client/src/components/MkPostFormAttaches.vue
+++ b/packages/client/src/components/MkPostFormAttaches.vue
@@ -198,7 +198,6 @@ export default defineComponent({
 			height: 64px;
 			margin-right: 4px;
 			border-radius: 4px;
-			overflow: hidden;
 			cursor: move;
 
 			&:hover > .remove {
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index a1f7cc1b9..68439527a 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -35,7 +35,11 @@
 			class="content"
 			:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
 		>
-			<div class="body">
+			<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" />
+			<div 
+				class="body"
+				v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
+			>
 				<span v-if="note.deletedAt" style="opacity: 0.5"
 					>({{ i18n.ts.deleted }})</span
 				>
@@ -96,15 +100,20 @@
 						<XNoteSimple :note="note.renote" />
 					</div>
 				</template>
+				<div
+					v-if="note.cw && !showContent"
+					tabindex="0"
+					v-on:focus="cwButton?.focus()"
+				></div>
 			</div>
 			<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
-			<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
+			<XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" />
 		</div>
 	</div>
 </template>
 
 <script lang="ts" setup>
-import {} from "vue";
+import { ref } from "vue"; 
 import * as misskey from "calckey-js";
 import * as mfm from "mfm-js";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
@@ -126,8 +135,10 @@ const props = defineProps<{
 
 const emit = defineEmits<{
 	(ev: "push", v): void;
+	(ev: "focusfooter"): void;
 }>();
 
+const cwButton = ref<HTMLElement>(); 
 const isLong =
 	!props.detailedView &&
 	props.note.cw == null &&
@@ -140,6 +151,13 @@ const urls = props.note.text
 	: null;
 
 let showContent = $ref(false);
+
+
+function focusFooter(ev) {
+	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
+		emit("focusfooter");
+	}
+}
 </script>
 
 <style lang="scss" scoped>
@@ -231,6 +249,9 @@ let showContent = $ref(false);
 				margin-top: -50px;
 				padding-top: 50px;
 				overflow: hidden;
+				user-select: none;
+				-webkit-user-select: none;
+				-moz-user-select: none;
 			}
 			&.collapsed > .body {
 				box-sizing: border-box;
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index 55d6fba50..83c667070 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -9,7 +9,6 @@
 						v-if="item.type === 'a'"
 						:href="item.href"
 						:target="item.target"
-						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 					>
@@ -22,7 +21,6 @@
 					</a>
 					<button
 						v-else-if="item.type === 'button'"
-						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 						:disabled="item.active"
@@ -38,7 +36,6 @@
 					<MkA
 						v-else
 						:to="item.to"
-						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 					>
@@ -99,7 +96,7 @@ export default defineComponent({
 				font-size: 0.9em;
 				margin-bottom: 0.3rem;
 
-				&:hover {
+				&:hover, &:focus-visible {
 					text-decoration: none;
 					background: var(--panelHighlight);
 				}
diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue
index 506f48bd4..14553ca46 100644
--- a/packages/client/src/components/MkUserSelectDialog.vue
+++ b/packages/client/src/components/MkUserSelectDialog.vue
@@ -46,6 +46,7 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
+							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
@@ -73,6 +74,7 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
+							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue
index 972864d1f..78a4f90f2 100644
--- a/packages/client/src/components/MkUsersTooltip.vue
+++ b/packages/client/src/components/MkUsersTooltip.vue
@@ -7,7 +7,7 @@
 	>
 		<div class="beaffaef">
 			<div v-for="u in users" :key="u.id" class="user">
-				<MkAvatar class="avatar" :user="u" />
+				<MkAvatar class="avatar" :user="u" disableLink />
 				<MkUserName class="name" :user="u" :nowrap="true" />
 			</div>
 			<div v-if="users.length < count" class="omitted">
diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue
index 07e845032..d48fc5383 100644
--- a/packages/client/src/components/MkWidgets.vue
+++ b/packages/client/src/components/MkWidgets.vue
@@ -1,7 +1,7 @@
 <template>
 	<div class="vjoppmmu">
 		<template v-if="edit">
-			<header>
+			<header tabindex="-1" v-focus>
 				<MkSelect
 					v-model="widgetAdderSelected"
 					style="margin-bottom: var(--margin)"
diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue
index 8868f5784..a2fde5341 100644
--- a/packages/client/src/components/form/folder.vue
+++ b/packages/client/src/components/form/folder.vue
@@ -1,6 +1,6 @@
 <template>
 	<div class="dwzlatin" :class="{ opened }">
-		<div class="header _button" @click="toggle">
+		<button class="header _button" @click="toggle">
 			<span class="icon"><slot name="icon"></slot></span>
 			<span class="text"><slot name="label"></slot></span>
 			<span class="right">
@@ -8,7 +8,7 @@
 				<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
 				<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
 			</span>
-		</div>
+		</button>
 		<KeepAlive>
 			<div v-if="openedAtLeastOnce" v-show="opened" class="body">
 				<MkSpacer :margin-min="14" :margin-max="22">
diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue
index 493b2d010..ef644b327 100644
--- a/packages/client/src/components/form/radio.vue
+++ b/packages/client/src/components/form/radio.vue
@@ -66,6 +66,9 @@ function toggle(): void {
 	&:hover {
 		border-color: var(--inputBorderHover) !important;
 	}
+	&:focus-within {
+		outline: auto;
+	}
 
 	&.checked {
 		background-color: var(--accentedBg) !important;
diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue
index efaf488a9..b1c6df4e9 100644
--- a/packages/client/src/components/form/switch.vue
+++ b/packages/client/src/components/form/switch.vue
@@ -99,6 +99,9 @@ const toggle = () => {
 			border-color: var(--inputBorderHover) !important;
 		}
 	}
+	&:focus-within > .button {
+		outline: auto;
+	}
 
 	> .label {
 		margin-left: 12px;
diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue
index ad1d80ca6..c78ef0c10 100644
--- a/packages/client/src/components/global/MkPageHeader.vue
+++ b/packages/client/src/components/global/MkPageHeader.vue
@@ -19,6 +19,7 @@
 				class="avatar"
 				:user="$i"
 				:disable-preview="true"
+				disableLink
 			/>
 		</div>
 		<template v-if="metadata">
@@ -33,6 +34,7 @@
 					:user="metadata.avatar"
 					:disable-preview="true"
 					:show-indicator="true"
+					disableLink
 				/>
 				<i
 					v-else-if="metadata.icon && !narrow"
diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index 8423ce773..437b7c53e 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -5,6 +5,9 @@
 				:is="currentPageComponent"
 				:key="key"
 				v-bind="Object.fromEntries(currentPageProps)"
+				tabindex="-1"
+				v-focus
+				style="outline: none;"
 			/>
 
 			<template #fallback>
diff --git a/packages/client/src/directives/focus.ts b/packages/client/src/directives/focus.ts
new file mode 100644
index 000000000..4d34fbf1f
--- /dev/null
+++ b/packages/client/src/directives/focus.ts
@@ -0,0 +1,3 @@
+export default {
+	mounted: (el) => el.focus()
+}
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
index 0a5c32326..77639e2f3 100644
--- a/packages/client/src/directives/index.ts
+++ b/packages/client/src/directives/index.ts
@@ -11,6 +11,7 @@ import anim from "./anim";
 import clickAnime from "./click-anime";
 import panel from "./panel";
 import adaptiveBorder from "./adaptive-border";
+import focus from "./focus";
 
 export default function (app: App) {
 	app.directive("userPreview", userPreview);
@@ -25,4 +26,5 @@ export default function (app: App) {
 	app.directive("click-anime", clickAnime);
 	app.directive("panel", panel);
 	app.directive("adaptive-border", adaptiveBorder);
+	app.directive("focus", focus);
 }
diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
index 7738d14e8..91024a6e3 100644
--- a/packages/client/src/directives/tooltip.ts
+++ b/packages/client/src/directives/tooltip.ts
@@ -76,23 +76,32 @@ export default {
 			ev.preventDefault();
 		});
 
+		function showTooltip() {
+			window.clearTimeout(self.showTimer);
+			window.clearTimeout(self.hideTimer);
+			self.showTimer = window.setTimeout(self.show, delay);
+		}
+		function hideTooltip() {
+			window.clearTimeout(self.showTimer);
+			window.clearTimeout(self.hideTimer);
+			self.hideTimer = window.setTimeout(self.close, delay);
+		}
+
 		el.addEventListener(
-			start,
-			() => {
-				window.clearTimeout(self.showTimer);
-				window.clearTimeout(self.hideTimer);
-				self.showTimer = window.setTimeout(self.show, delay);
-			},
+			start, showTooltip,
+			{ passive: true },
+		);
+		el.addEventListener(
+			"focusin", showTooltip,
 			{ passive: true },
 		);
 
 		el.addEventListener(
-			end,
-			() => {
-				window.clearTimeout(self.showTimer);
-				window.clearTimeout(self.hideTimer);
-				self.hideTimer = window.setTimeout(self.close, delay);
-			},
+			end, hideTooltip,
+			{ passive: true },
+		);
+		el.addEventListener(
+			"focusout", hideTooltip,
 			{ passive: true },
 		);
 
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index 69fd1bc58..bf070e269 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -313,11 +313,7 @@ onUnmounted(() => {
 			font-weight: normal;
 			opacity: 0.7;
 
-			&:hover {
-				opacity: 1;
-			}
-
-			&.active {
+			&:hover, &:focus-visible, &.active {
 				opacity: 1;
 			}
 
diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue
index 6184cfb10..db953b890 100644
--- a/packages/client/src/pages/admin/overview.moderators.vue
+++ b/packages/client/src/pages/admin/overview.moderators.vue
@@ -12,7 +12,7 @@
 					class="user"
 					:to="`/user-info/${user.id}`"
 				>
-					<MkAvatar :user="user" class="avatar" indicator />
+					<MkAvatar :user="user" class="avatar" indicator disableLink />
 				</MkA>
 			</div>
 		</Transition>
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index 2aac52163..35279495b 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -23,6 +23,7 @@
 								class="avatar"
 								:user="req.follower"
 								:show-indicator="true"
+								disableLink
 							/>
 							<div class="body">
 								<div class="name">
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index ec2cd2477..3010354b6 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -6,14 +6,14 @@
 				{{ i18n.ts.addAccount }}</FormButton
 			>
 
-			<div
+			<button
 				v-for="account in accounts"
 				:key="account.id"
 				class="_panel _button lcjjdxlm"
 				@click="menu(account, $event)"
 			>
 				<div class="avatar">
-					<MkAvatar :user="account" class="avatar" />
+					<MkAvatar :user="account" class="avatar" disableLink />
 				</div>
 				<div class="body">
 					<div class="name">
@@ -23,7 +23,7 @@
 						<MkAcct :user="account" />
 					</div>
 				</div>
-			</div>
+			</button>
 		</FormSuspense>
 	</div>
 </template>
@@ -158,6 +158,8 @@ definePageMetadata({
 .lcjjdxlm {
 	display: flex;
 	padding: 16px;
+	width: 100%;
+	text-align: unset;
 
 	> .avatar {
 		display: block;
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index 051edf6e0..52c7b62f4 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -204,10 +204,6 @@ hr {
 		pointer-events: none;
 	}
 
-	&:focus-visible {
-		outline: none;
-	}
-
 	&:disabled {
 		opacity: 0.5;
 		cursor: default;
diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue
index 43c91d147..39abb7c26 100644
--- a/packages/client/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue
@@ -18,6 +18,7 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
+						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 380f77c3c..20c177f37 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -18,6 +18,7 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
+						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
@@ -334,6 +335,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -398,8 +400,6 @@ function more(ev: MouseEvent) {
 					padding-left: 30px;
 					line-height: 2.85rem;
 					margin-bottom: 0.5rem;
-					text-overflow: ellipsis;
-					overflow: hidden;
 					white-space: nowrap;
 					width: 100%;
 					text-align: left;
@@ -425,9 +425,12 @@ function more(ev: MouseEvent) {
 					> .text {
 						position: relative;
 						font-size: 0.9em;
+						overflow: hidden;
+						text-overflow: ellipsis;
 					}
 
-					&:hover {
+					&:hover,
+					&:focus-within {
 						text-decoration: none;
 						color: var(--navHoverFg);
 						transition: all 0.4s ease;
@@ -437,7 +440,8 @@ function more(ev: MouseEvent) {
 						color: var(--navActive);
 					}
 
-					&:hover,
+					&:hover, 
+					&:focus-within,
 					&.active {
 						color: var(--accent);
 						transition: all 0.4s ease;
@@ -528,6 +532,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -613,6 +618,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						text-decoration: none;
 						color: var(--accent);
@@ -642,5 +648,12 @@ function more(ev: MouseEvent) {
 			}
 		}
 	}
+
+	.item {
+		outline: none;
+		&:focus-visible:before {
+			outline: auto;
+		}
+	}
 }
 </style>
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
index 5c3e6b702..99a0ab098 100644
--- a/packages/client/src/ui/classic.header.vue
+++ b/packages/client/src/ui/classic.header.vue
@@ -83,6 +83,7 @@
 					<MkAvatar :user="$i" class="avatar" /><MkAcct
 						class="acct"
 						:user="$i"
+						disableLink
 					/>
 				</button>
 				<div class="post" @click="post">
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index b70a3c984..fa72c5765 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -5,7 +5,7 @@
 			class="item _button account"
 			@click="openAccountMenu"
 		>
-			<MkAvatar :user="$i" class="avatar" /><MkAcct
+			<MkAvatar :user="$i" class="avatar" disableLink /><MkAcct
 				class="text"
 				:user="$i"
 			/>
@@ -299,6 +299,7 @@ function openInstanceMenu(ev: MouseEvent) {
 				width: 46px;
 				height: 46px;
 				padding: 0;
+				margin-inline: 0 !important;
 			}
 		}
 
@@ -372,6 +373,7 @@ function openInstanceMenu(ev: MouseEvent) {
 
 		> i {
 			width: 32px;
+			justify-content: center;
 		}
 
 		> i,
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index a721ffd0b..266effd9a 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -227,6 +227,8 @@ onMounted(() => {
 }
 
 .gbhvwtnk {
+	display: flex;
+	justify-content: center;
 	$ui-font-size: 1em;
 	$widgets-hide-threshold: 1200px;
 
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9f0b4195d..60e42e11a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,6 +19,12 @@ importers:
       '@tensorflow/tfjs':
         specifier: ^3.21.0
         version: 3.21.0(seedrandom@3.0.5)
+      focus-trap:
+        specifier: ^7.2.0
+        version: 7.2.0
+      focus-trap-vue:
+        specifier: ^4.0.1
+        version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
@@ -3803,14 +3809,12 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       source-map: 0.6.1
-    dev: true
 
   /@vue/compiler-dom@3.2.45:
     resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
     dependencies:
       '@vue/compiler-core': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/compiler-sfc@2.7.14:
     resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@@ -3833,14 +3837,12 @@ packages:
       magic-string: 0.25.9
       postcss: 8.4.21
       source-map: 0.6.1
-    dev: true
 
   /@vue/compiler-ssr@3.2.45:
     resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
     dependencies:
       '@vue/compiler-dom': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/reactivity-transform@3.2.45:
     resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@@ -3850,20 +3852,17 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       magic-string: 0.25.9
-    dev: true
 
   /@vue/reactivity@3.2.45:
     resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
     dependencies:
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/runtime-core@3.2.45:
     resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
     dependencies:
       '@vue/reactivity': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/runtime-dom@3.2.45:
     resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@@ -3871,7 +3870,6 @@ packages:
       '@vue/runtime-core': 3.2.45
       '@vue/shared': 3.2.45
       csstype: 2.6.21
-    dev: true
 
   /@vue/server-renderer@3.2.45(vue@3.2.45):
     resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@@ -3881,11 +3879,9 @@ packages:
       '@vue/compiler-ssr': 3.2.45
       '@vue/shared': 3.2.45
       vue: 3.2.45
-    dev: true
 
   /@vue/shared@3.2.45:
     resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
-    dev: true
 
   /@webassemblyjs/ast@1.11.1:
     resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@@ -6074,7 +6070,6 @@ packages:
 
   /csstype@2.6.21:
     resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
-    dev: true
 
   /csstype@3.1.1:
     resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
@@ -6979,7 +6974,6 @@ packages:
 
   /estree-walker@2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
-    dev: true
 
   /esutils@2.0.3:
     resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@@ -7445,6 +7439,22 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
+  /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
+    resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
+    peerDependencies:
+      focus-trap: ^7.0.0
+      vue: ^3.0.0
+    dependencies:
+      focus-trap: 7.2.0
+      vue: 3.2.45
+    dev: false
+
+  /focus-trap@7.2.0:
+    resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==}
+    dependencies:
+      tabbable: 6.1.1
+    dev: false
+
   /follow-redirects@1.15.2(debug@4.3.4):
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     engines: {node: '>=4.0'}
@@ -10350,7 +10360,6 @@ packages:
     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
     dependencies:
       sourcemap-codec: 1.4.8
-    dev: true
 
   /mailcheck@1.1.1:
     resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
@@ -13267,7 +13276,6 @@ packages:
   /sourcemap-codec@1.4.8:
     resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
     deprecated: Please use @jridgewell/sourcemap-codec instead
-    dev: true
 
   /sparkles@1.0.1:
     resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==}
@@ -13686,6 +13694,10 @@ packages:
     resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
     dev: true
 
+  /tabbable@6.1.1:
+    resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==}
+    dev: false
+
   /tapable@2.2.1:
     resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
     engines: {node: '>=6'}
@@ -14776,7 +14788,6 @@ packages:
       '@vue/runtime-dom': 3.2.45
       '@vue/server-renderer': 3.2.45(vue@3.2.45)
       '@vue/shared': 3.2.45
-    dev: true
 
   /vuedraggable@4.1.0(vue@3.2.45):
     resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}

From 6eba097e09f87cfb9191ec48aabcb222e6293898 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 15:32:33 -0700
Subject: [PATCH 51/96] new logos

---
 package.json                                 | 2 +-
 packages/backend/assets/favicon.ico          | 4 ++--
 packages/backend/assets/favicon.png          | 4 ++--
 packages/backend/assets/favicon.svg          | 4 ++--
 packages/backend/assets/icons/192.png        | 4 ++--
 packages/backend/assets/icons/512.png        | 4 ++--
 packages/backend/assets/inverse wordmark.png | 3 +++
 packages/backend/assets/inverse wordmark.svg | 4 ++--
 packages/backend/assets/splash.png           | 4 ++--
 9 files changed, 18 insertions(+), 15 deletions(-)
 create mode 100644 packages/backend/assets/inverse wordmark.png

diff --git a/package.json b/package.json
index 63b5f1dcd..80942ff54 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "calckey",
-	"version": "13.2.0-dev39",
+	"version": "13.2.0-dev40",
 	"codename": "aqua",
 	"repository": {
 		"type": "git",
diff --git a/packages/backend/assets/favicon.ico b/packages/backend/assets/favicon.ico
index fd7aadf2e..8d46b0c1d 100644
--- a/packages/backend/assets/favicon.ico
+++ b/packages/backend/assets/favicon.ico
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:ce4ba88c40e8d79d697cead7ccad4325846c03995f429194e19a5aa1383b7a82
-size 3973
+oid sha256:c414a146ef32ee9017444a977d5b6bb40079be9cdfeaab9a8a58f836fb6eea62
+size 4286
diff --git a/packages/backend/assets/favicon.png b/packages/backend/assets/favicon.png
index b90943252..44d777ceb 100644
--- a/packages/backend/assets/favicon.png
+++ b/packages/backend/assets/favicon.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:c259dc343a2824ee408c5e2daafa56abd0b31d29fd1f4c59be7d9d1fe0c5e379
-size 3951
+oid sha256:25f2b237d58b1094ff047ae1d8653fafeb1ae79b5997054f6997c4c252ab1a4e
+size 6122
diff --git a/packages/backend/assets/favicon.svg b/packages/backend/assets/favicon.svg
index 7f55f6312..4e00347fb 100644
--- a/packages/backend/assets/favicon.svg
+++ b/packages/backend/assets/favicon.svg
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:4f17cc091606efe4c5e6fc3dbf04b018bc169705f352d52c43dc771d5a716a1d
-size 4285
+oid sha256:3d6f88a7b660f9960a5ed9ac4473ec447b46a0b79d02064639c05729519af39d
+size 2890
diff --git a/packages/backend/assets/icons/192.png b/packages/backend/assets/icons/192.png
index 51434dd78..48a863e34 100644
--- a/packages/backend/assets/icons/192.png
+++ b/packages/backend/assets/icons/192.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:712381afff5aaf9f5ed479d8862a67ca5581307c5b807d7cfa3edf813cf214ad
-size 7000
+oid sha256:ef1b3ac7867fa31073dd30b8bb90ef942d74446fadec5e663ae662bd3b74f8bd
+size 7483
diff --git a/packages/backend/assets/icons/512.png b/packages/backend/assets/icons/512.png
index 8c45a6311..e5ba760ad 100644
--- a/packages/backend/assets/icons/512.png
+++ b/packages/backend/assets/icons/512.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:27b9944477bdba4219e6f3af9af3393d947fbcc52f88b3d9ecf9df06547cd970
-size 7099
+oid sha256:23616706bb3475f88e355c7489f48306b37d0f6aa3de5cf5559ce357a5b728ba
+size 21631
diff --git a/packages/backend/assets/inverse wordmark.png b/packages/backend/assets/inverse wordmark.png
new file mode 100644
index 000000000..e5ba760ad
--- /dev/null
+++ b/packages/backend/assets/inverse wordmark.png	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:23616706bb3475f88e355c7489f48306b37d0f6aa3de5cf5559ce357a5b728ba
+size 21631
diff --git a/packages/backend/assets/inverse wordmark.svg b/packages/backend/assets/inverse wordmark.svg
index fe9a77be9..33d426136 100644
--- a/packages/backend/assets/inverse wordmark.svg	
+++ b/packages/backend/assets/inverse wordmark.svg	
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:be36c9edc904f05d7f4a96f2092154b14cd7696fc2b9a317e77e56d85f1f06a0
-size 4395
+oid sha256:b034df14985fe2e6a3f2b37d80ed5144a2c75be2cf8393b236a71ef55e6432ba
+size 2163
diff --git a/packages/backend/assets/splash.png b/packages/backend/assets/splash.png
index 0547a8f19..1e396a741 100644
--- a/packages/backend/assets/splash.png
+++ b/packages/backend/assets/splash.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:a751c980d885ce137f65ae4baaacd89f46b8206774db3b90fb32ac7c06a3a8ed
-size 9326
+oid sha256:67df7f31c099111ad56e98d75a71340c4274d27e0f38c979e941c1d5d073e3e8
+size 21007

From f9a72e1ea6cd126a288368cf98f8c90829d47b97 Mon Sep 17 00:00:00 2001
From: Kaity A <kaity@theallans.com.au>
Date: Sun, 30 Apr 2023 11:09:51 +1000
Subject: [PATCH 52/96] Add Libre Translate support

---
 .../migration/1682777547198-LibreTranslate.js | 23 ++++++++++
 packages/backend/src/config/types.ts          |  5 +++
 packages/backend/src/models/entities/meta.ts  | 12 ++++++
 .../api/endpoints/admin/accounts/hosted.ts    | 11 +++++
 .../src/server/api/endpoints/admin/meta.ts    |  5 ++-
 .../server/api/endpoints/admin/update-meta.ts | 18 ++++++++
 .../backend/src/server/api/endpoints/meta.ts  |  3 +-
 .../server/api/endpoints/notes/translate.ts   | 43 ++++++++++++++++++-
 packages/client/src/pages/admin/settings.vue  | 34 +++++++++++++++
 9 files changed, 150 insertions(+), 4 deletions(-)
 create mode 100644 packages/backend/migration/1682777547198-LibreTranslate.js

diff --git a/packages/backend/migration/1682777547198-LibreTranslate.js b/packages/backend/migration/1682777547198-LibreTranslate.js
new file mode 100644
index 000000000..dbaf483e6
--- /dev/null
+++ b/packages/backend/migration/1682777547198-LibreTranslate.js
@@ -0,0 +1,23 @@
+export class LibreTranslate1682777547198 {
+	name = "LibreTranslate1682777547198";
+
+	async up(queryRunner) {
+		await queryRunner.query(`
+				ALTER TABLE "meta"
+				ADD "libreTranslateApiUrl" character varying(512)
+		`);
+		await queryRunner.query(`
+				ALTER TABLE "meta"
+				ADD "libreTranslateApiKey" character varying(128)
+		`);
+	}
+
+	async down(queryRunner) {
+		await queryRunner.query(`
+				ALTER TABLE "meta" DROP COLUMN "libreTranslateApiKey"
+		`);
+		await queryRunner.query(`
+				ALTER TABLE "meta" DROP COLUMN "libreTranslateApiUrl"
+		`);
+	}
+}
diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts
index 4f367debe..0cd8c02ad 100644
--- a/packages/backend/src/config/types.ts
+++ b/packages/backend/src/config/types.ts
@@ -89,6 +89,11 @@ export type Source = {
 		authKey?: string;
 		isPro?: boolean;
 	};
+	libreTranslate: {
+		managed?: boolean;
+		apiUrl?: string;
+		apiKey?: string;
+	};
 	email: {
 		managed?: boolean;
 		address?: string;
diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts
index 26a7c9c19..2f77796c4 100644
--- a/packages/backend/src/models/entities/meta.ts
+++ b/packages/backend/src/models/entities/meta.ts
@@ -386,6 +386,18 @@ export class Meta {
 	})
 	public deeplIsPro: boolean;
 
+	@Column('varchar', {
+		length: 512,
+		nullable: true,
+	})
+	public libreTranslateApiUrl: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true,
+	})
+	public libreTranslateApiKey: string | null;
+
 	@Column('varchar', {
 		length: 512,
 		nullable: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
index 15ad1f9a1..a7b6e95c2 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
@@ -30,6 +30,17 @@ export default define(meta, paramDef, async (ps, me) => {
 				set.deeplIsPro = config.deepl.isPro;
 			}
 		}
+		if (
+			config.libreTranslate.managed != null &&
+			config.libreTranslate.managed === true
+		) {
+			if (typeof config.libreTranslate.apiUrl === "string") {
+				set.libreTranslateApiUrl = config.libreTranslate.apiUrl;
+			}
+			if (typeof config.libreTranslate.apiKey === "string") {
+				set.libreTranslateApiKey = config.libreTranslate.apiKey;
+			}
+		}
 		if (config.email.managed != null && config.email.managed === true) {
 			set.enableEmail = true;
 			if (typeof config.email.address === "string") {
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index c8c639f50..f0ac57892 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -512,7 +512,8 @@ export default define(meta, paramDef, async (ps, me) => {
 		enableGithubIntegration: instance.enableGithubIntegration,
 		enableDiscordIntegration: instance.enableDiscordIntegration,
 		enableServiceWorker: instance.enableServiceWorker,
-		translatorAvailable: instance.deeplAuthKey != null,
+		translatorAvailable:
+			instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
 		pinnedPages: instance.pinnedPages,
 		pinnedClipId: instance.pinnedClipId,
 		cacheRemoteFiles: instance.cacheRemoteFiles,
@@ -564,6 +565,8 @@ export default define(meta, paramDef, async (ps, me) => {
 		objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
 		deeplAuthKey: instance.deeplAuthKey,
 		deeplIsPro: instance.deeplIsPro,
+		libreTranslateApiUrl: instance.libreTranslateApiUrl,
+		libreTranslateApiKey: instance.libreTranslateApiKey,
 		enableIpLogging: instance.enableIpLogging,
 		enableActiveEmailValidation: instance.enableActiveEmailValidation,
 	};
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index f7e79b64b..a23000732 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -124,6 +124,8 @@ export const paramDef = {
 		summalyProxy: { type: "string", nullable: true },
 		deeplAuthKey: { type: "string", nullable: true },
 		deeplIsPro: { type: "boolean" },
+		libreTranslateApiUrl: { type: "string", nullable: true },
+		libreTranslateApiKey: { type: "string", nullable: true },
 		enableTwitterIntegration: { type: "boolean" },
 		twitterConsumerKey: { type: "string", nullable: true },
 		twitterConsumerSecret: { type: "string", nullable: true },
@@ -515,6 +517,22 @@ export default define(meta, paramDef, async (ps, me) => {
 		set.deeplIsPro = ps.deeplIsPro;
 	}
 
+	if (ps.libreTranslateApiUrl !== undefined) {
+		if (ps.libreTranslateApiUrl === "") {
+			set.libreTranslateApiUrl = null;
+		} else {
+			set.libreTranslateApiUrl = ps.libreTranslateApiUrl;
+		}
+	}
+
+	if (ps.libreTranslateApiKey !== undefined) {
+		if (ps.libreTranslateApiKey === "") {
+			set.libreTranslateApiKey = null;
+		} else {
+			set.libreTranslateApiKey = ps.libreTranslateApiKey;
+		}
+	}
+
 	if (ps.enableIpLogging !== undefined) {
 		set.enableIpLogging = ps.enableIpLogging;
 	}
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 4dc1c941e..23989750f 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -482,7 +482,8 @@ export default define(meta, paramDef, async (ps, me) => {
 
 		enableServiceWorker: instance.enableServiceWorker,
 
-		translatorAvailable: instance.deeplAuthKey != null,
+		translatorAvailable:
+			instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
 		defaultReaction: instance.defaultReaction,
 
 		...(ps.detail
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index c6415ceef..d86fc12a2 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -51,15 +51,54 @@ export default define(meta, paramDef, async (ps, user) => {
 
 	const instance = await fetchMeta();
 
-	if (instance.deeplAuthKey == null) {
+	if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
 		return 204; // TODO: 良い感じのエラー返す
 	}
 
 	let targetLang = ps.targetLang;
 	if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
 
+	if (instance.libreTranslateApiUrl != null) {
+		const jsonBody = {
+			q: note.text,
+			source: "auto",
+			target: targetLang,
+			format: "text",
+			api_key: instance.libreTranslateApiKey ?? "",
+		};
+
+		const url = new URL(instance.libreTranslateApiUrl);
+		if (url.pathname.endsWith("/")) {
+			url.pathname = url.pathname.slice(0, -1);
+		}
+		if (!url.pathname.endsWith("/translate")) {
+			url.pathname += "/translate";
+		}
+		const res = await fetch(url.toString(), {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/json",
+			},
+			body: JSON.stringify(jsonBody),
+			agent: getAgentByUrl,
+		});
+
+		const json = (await res.json()) as {
+			detectedLanguage?: {
+				confidence: number;
+				language: string;
+			};
+			translatedText: string;
+		};
+
+		return {
+			sourceLang: json.detectedLanguage?.language,
+			text: json.translatedText,
+		};
+	}
+
 	const params = new URLSearchParams();
-	params.append("auth_key", instance.deeplAuthKey);
+	params.append("auth_key", instance.deeplAuthKey ?? "");
 	params.append("text", note.text);
 	params.append("target_lang", targetLang);
 
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index 5349df805..feedaff6d 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -371,6 +371,34 @@
 								<template #label>Pro account</template>
 							</FormSwitch>
 						</FormSection>
+
+						<FormSection>
+							<template #label>Libre Translate</template>
+
+							<FormInput
+								v-model="libreTranslateApiUrl"
+								class="_formBlock"
+							>
+								<template #prefix
+									><i class="ph-link ph-bold ph-lg"></i
+								></template>
+								<template #label
+									>Libre Translate API URL</template
+								>
+							</FormInput>
+
+							<FormInput
+								v-model="libreTranslateApiKey"
+								class="_formBlock"
+							>
+								<template #prefix
+									><i class="ph-key ph-bold ph-lg"></i
+								></template>
+								<template #label
+									>Libre Translate API Key</template
+								>
+							</FormInput>
+						</FormSection>
 					</div>
 				</FormSuspense>
 			</MkSpacer>
@@ -422,6 +450,8 @@ let swPublicKey: any = $ref(null);
 let swPrivateKey: any = $ref(null);
 let deeplAuthKey: string = $ref("");
 let deeplIsPro: boolean = $ref(false);
+let libreTranslateApiUrl: string = $ref("");
+let libreTranslateApiKey: string = $ref("");
 let defaultReaction: string = $ref("");
 let defaultReactionCustom: string = $ref("");
 
@@ -456,6 +486,8 @@ async function init() {
 	swPrivateKey = meta.swPrivateKey;
 	deeplAuthKey = meta.deeplAuthKey;
 	deeplIsPro = meta.deeplIsPro;
+	libreTranslateApiUrl = meta.libreTranslateApiUrl;
+	libreTranslateApiKey = meta.libreTranslateApiKey;
 	defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
 		? meta.defaultReaction
 		: "custom";
@@ -498,6 +530,8 @@ function save() {
 		swPrivateKey,
 		deeplAuthKey,
 		deeplIsPro,
+		libreTranslateApiUrl,
+		libreTranslateApiKey,
 		defaultReaction,
 	}).then(() => {
 		fetchInstance();

From 0f4e88cf53ed8dd07da0526b772096ae4919f7d4 Mon Sep 17 00:00:00 2001
From: Kaitlyn Allan <kaitlyn.allan@enlabs.cloud>
Date: Sat, 29 Apr 2023 21:05:33 +1000
Subject: [PATCH 53/96] Remove eslint from calckey-js (use rome)

---
 packages/calckey-js/.eslintignore |  7 ----
 packages/calckey-js/.eslintrc.js  | 65 -------------------------------
 packages/calckey-js/package.json  |  3 +-
 3 files changed, 1 insertion(+), 74 deletions(-)
 delete mode 100644 packages/calckey-js/.eslintignore
 delete mode 100644 packages/calckey-js/.eslintrc.js

diff --git a/packages/calckey-js/.eslintignore b/packages/calckey-js/.eslintignore
deleted file mode 100644
index f22128f04..000000000
--- a/packages/calckey-js/.eslintignore
+++ /dev/null
@@ -1,7 +0,0 @@
-node_modules
-/built
-/coverage
-/.eslintrc.js
-/jest.config.ts
-/test
-/test-d
diff --git a/packages/calckey-js/.eslintrc.js b/packages/calckey-js/.eslintrc.js
deleted file mode 100644
index 164cf1fbe..000000000
--- a/packages/calckey-js/.eslintrc.js
+++ /dev/null
@@ -1,65 +0,0 @@
-module.exports = {
-	root: true,
-	parser: "@typescript-eslint/parser",
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ["./tsconfig.json"],
-	},
-	plugins: ["@typescript-eslint"],
-	extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
-	rules: {
-		indent: [
-			"error",
-			"tab",
-			{
-				SwitchCase: 1,
-				MemberExpression: "off",
-				flatTernaryExpressions: true,
-				ArrayExpression: "first",
-				ObjectExpression: "first",
-			},
-		],
-		"eol-last": ["error", "always"],
-		semi: ["error", "always"],
-		quotes: ["error", "single"],
-		"comma-dangle": ["error", "always-multiline"],
-		"keyword-spacing": [
-			"error",
-			{
-				before: true,
-				after: true,
-			},
-		],
-		"key-spacing": [
-			"error",
-			{
-				beforeColon: false,
-				afterColon: true,
-			},
-		],
-		"space-infix-ops": ["error"],
-		"space-before-blocks": ["error", "always"],
-		"object-curly-spacing": ["error", "always"],
-		"nonblock-statement-body-position": ["error", "beside"],
-		eqeqeq: ["error", "always", { null: "ignore" }],
-		"no-multiple-empty-lines": ["error", { max: 1 }],
-		"no-multi-spaces": ["error"],
-		"no-var": ["error"],
-		"prefer-arrow-callback": ["error"],
-		"no-throw-literal": ["error"],
-		"no-param-reassign": ["warn"],
-		"no-constant-condition": ["warn"],
-		"no-empty-pattern": ["warn"],
-		"@typescript-eslint/no-unnecessary-condition": ["error"],
-		"@typescript-eslint/no-inferrable-types": ["warn"],
-		"@typescript-eslint/no-non-null-assertion": ["warn"],
-		"@typescript-eslint/explicit-function-return-type": ["warn"],
-		"@typescript-eslint/no-misused-promises": [
-			"error",
-			{
-				checksVoidReturn: false,
-			},
-		],
-		"@typescript-eslint/consistent-type-imports": "error",
-	},
-};
diff --git a/packages/calckey-js/package.json b/packages/calckey-js/package.json
index d68f24175..598dd1cdb 100644
--- a/packages/calckey-js/package.json
+++ b/packages/calckey-js/package.json
@@ -9,9 +9,8 @@
 		"tsd": "tsd",
 		"api": "pnpm api-extractor run --local --verbose",
 		"api-prod": "pnpm api-extractor run --verbose",
-		"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
 		"typecheck": "tsc --noEmit",
-		"lint": "pnpm typecheck && pnpm eslint",
+		"lint": "pnpm typecheck && pnpm rome check \"src/*.ts\"",
 		"jest": "jest --coverage --detectOpenHandles",
 		"test": "pnpm jest && pnpm tsd"
 	},

From 1051da8fbf84be7abdc9b468d0e762278e1c4d88 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 19:14:36 -0700
Subject: [PATCH 54/96] Revert "keyboard accessibility (#9725)"

This reverts commit c1d5922acbe7060c0ea779ccf314e9f0e6b91bb3.
---
 package.json                                  |   2 -
 packages/client/src/components/MkButton.vue   |   3 +-
 packages/client/src/components/MkCwButton.vue |  18 +-
 .../src/components/MkDriveFileThumbnail.vue   |   7 +-
 .../client/src/components/MkEmojiPicker.vue   | 278 +++++++------
 .../client/src/components/MkLaunchPad.vue     |   2 +-
 .../client/src/components/MkMediaImage.vue    |   4 -
 .../client/src/components/MkMenu.child.vue    |  21 +-
 packages/client/src/components/MkMenu.vue     | 382 +++++++++---------
 packages/client/src/components/MkModal.vue    |  86 ++--
 .../src/components/MkModalPageWindow.vue      |   1 -
 .../client/src/components/MkModalWindow.vue   |  95 +++--
 packages/client/src/components/MkNote.vue     |   8 +-
 .../client/src/components/MkNotePreview.vue   |   2 +-
 packages/client/src/components/MkNoteSub.vue  |   4 +-
 .../client/src/components/MkPopupMenu.vue     |   2 -
 .../src/components/MkPostFormAttaches.vue     |   1 +
 .../src/components/MkSubNoteContent.vue       |  27 +-
 .../client/src/components/MkSuperMenu.vue     |   5 +-
 .../src/components/MkUserSelectDialog.vue     |   2 -
 .../client/src/components/MkUsersTooltip.vue  |   2 +-
 packages/client/src/components/MkWidgets.vue  |   2 +-
 .../client/src/components/form/folder.vue     |   4 +-
 packages/client/src/components/form/radio.vue |   3 -
 .../client/src/components/form/switch.vue     |   3 -
 .../src/components/global/MkPageHeader.vue    |   2 -
 .../src/components/global/RouterView.vue      |   3 -
 packages/client/src/directives/focus.ts       |   3 -
 packages/client/src/directives/index.ts       |   2 -
 packages/client/src/directives/tooltip.ts     |  33 +-
 packages/client/src/pages/admin/_header_.vue  |   6 +-
 .../src/pages/admin/overview.moderators.vue   |   2 +-
 packages/client/src/pages/follow-requests.vue |   1 -
 .../client/src/pages/settings/accounts.vue    |   8 +-
 packages/client/src/style.scss                |   4 +
 .../src/ui/_common_/navbar-for-mobile.vue     |   1 -
 packages/client/src/ui/_common_/navbar.vue    |  21 +-
 packages/client/src/ui/classic.header.vue     |   1 -
 packages/client/src/ui/classic.sidebar.vue    |   4 +-
 packages/client/src/ui/classic.vue            |   2 -
 pnpm-lock.yaml                                |  41 +-
 41 files changed, 495 insertions(+), 603 deletions(-)
 delete mode 100644 packages/client/src/directives/focus.ts

diff --git a/package.json b/package.json
index 51556dea5..80942ff54 100644
--- a/package.json
+++ b/package.json
@@ -40,8 +40,6 @@
 		"@bull-board/ui": "^4.10.2",
 		"@napi-rs/cli": "^2.15.0",
 		"@tensorflow/tfjs": "^3.21.0",
-		"focus-trap": "^7.2.0",
-		"focus-trap-vue": "^4.0.1",
 		"js-yaml": "4.1.0",
 		"seedrandom": "^3.0.5"
 	},
diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue
index feac281d9..5f1a5bdb7 100644
--- a/packages/client/src/components/MkButton.vue
+++ b/packages/client/src/components/MkButton.vue
@@ -195,7 +195,8 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&:focus-visible {
-		outline: auto;
+		outline: solid 2px var(--focus);
+		outline-offset: 2px;
 	}
 
 	&.inline {
diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 1f6340510..659cb1fbb 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -1,6 +1,5 @@
 <template>
 	<button
-		ref="el"
 		class="_button"
 		:class="{ showLess: modelValue, fade: !modelValue }"
 		@click.stop="toggle"
@@ -13,7 +12,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, ref } from "vue";
+import { computed } from "vue";
 import { length } from "stringz";
 import * as misskey from "calckey-js";
 import { concat } from "@/scripts/array";
@@ -28,8 +27,6 @@ const emit = defineEmits<{
 	(ev: "update:modelValue", v: boolean): void;
 }>();
 
-const el = ref<HTMLElement>(); 
-
 const label = computed(() => {
 	return concat([
 		props.note.text
@@ -46,14 +43,6 @@ const label = computed(() => {
 const toggle = () => {
 	emit("update:modelValue", !props.modelValue);
 };
-
-function focus() {
-	el.value.focus();
-}
-
-defineExpose({
-	focus
-});
 </script>
 
 <style lang="scss" scoped>
@@ -73,7 +62,7 @@ defineExpose({
 			}
 		}
 	}
-	&:hover > span, &:focus > span {
+	&:hover > span {
 		background: var(--cwFg) !important;
 		color: var(--cwBg) !important;
 	}
@@ -84,7 +73,6 @@ defineExpose({
 		bottom: 0;
 		left: 0;
 		width: 100%;
-		z-index: 2;
 		> span {
 			display: inline-block;
 			background: var(--panel);
@@ -93,7 +81,7 @@ defineExpose({
 			border-radius: 999px;
 			box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 		}
-		&:hover, &:focus {
+		&:hover {
 			> span {
 				background: var(--panelHighlight);
 			}
diff --git a/packages/client/src/components/MkDriveFileThumbnail.vue b/packages/client/src/components/MkDriveFileThumbnail.vue
index 48b542817..39150c10c 100644
--- a/packages/client/src/components/MkDriveFileThumbnail.vue
+++ b/packages/client/src/components/MkDriveFileThumbnail.vue
@@ -1,5 +1,5 @@
 <template>
-	<button ref="thumbnail" class="zdjebgpv">
+	<div ref="thumbnail" class="zdjebgpv">
 		<ImgWithBlurhash
 			v-if="isThumbnailAvailable"
 			:hash="file.blurhash"
@@ -36,7 +36,7 @@
 			v-if="isThumbnailAvailable && is === 'video'"
 			class="ph-file-video ph-bold ph-lg icon-sub"
 		></i>
-	</button>
+	</div>
 </template>
 
 <script lang="ts" setup>
@@ -88,9 +88,6 @@ const isThumbnailAvailable = computed(() => {
 	background: var(--panel);
 	border-radius: 8px;
 	overflow: clip;
-	border: 0;
-	padding: 0;
-	cursor: pointer;
 
 	> .icon-sub {
 		position: absolute;
diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index 88d207bab..a22006951 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -1,160 +1,157 @@
 <template>
-	<FocusTrap v-bind:active="isActive">
-		<div
-			class="omfetrab"
-			:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
-			:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
-			tabindex="-1"
-		>
-			<input
-				ref="search"
-				v-model.trim="q"
-				class="search"
-				data-prevent-emoji-insert
-				:class="{ filled: q != null && q != '' }"
-				:placeholder="i18n.ts.search"
-				type="search"
-				@paste.stop="paste"
-				@keyup.enter="done()"
-			/>
-			<div ref="emojis" class="emojis">
-				<section class="result">
-					<div v-if="searchResultCustom.length > 0" class="body">
+	<div
+		class="omfetrab"
+		:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
+		:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
+	>
+		<input
+			ref="search"
+			v-model.trim="q"
+			class="search"
+			data-prevent-emoji-insert
+			:class="{ filled: q != null && q != '' }"
+			:placeholder="i18n.ts.search"
+			type="search"
+			@paste.stop="paste"
+			@keyup.enter="done()"
+		/>
+		<div ref="emojis" class="emojis">
+			<section class="result">
+				<div v-if="searchResultCustom.length > 0" class="body">
+					<button
+						v-for="emoji in searchResultCustom"
+						:key="emoji.id"
+						class="_button item"
+						:title="emoji.name"
+						tabindex="0"
+						@click="chosen(emoji, $event)"
+					>
+						<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+						<img
+							class="emoji"
+							:src="
+								disableShowingAnimatedImages
+									? getStaticImageUrl(emoji.url)
+									: emoji.url
+							"
+						/>
+					</button>
+				</div>
+				<div v-if="searchResultUnicode.length > 0" class="body">
+					<button
+						v-for="emoji in searchResultUnicode"
+						:key="emoji.name"
+						class="_button item"
+						:title="emoji.name"
+						tabindex="0"
+						@click="chosen(emoji, $event)"
+					>
+						<MkEmoji class="emoji" :emoji="emoji.char" />
+					</button>
+				</div>
+			</section>
+
+			<div v-if="tab === 'index'" class="group index">
+				<section v-if="showPinned">
+					<div class="body">
 						<button
-							v-for="emoji in searchResultCustom"
-							:key="emoji.id"
+							v-for="emoji in pinned"
+							:key="emoji"
 							class="_button item"
-							:title="emoji.name"
 							tabindex="0"
 							@click="chosen(emoji, $event)"
 						>
-							<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
-							<img
+							<MkEmoji
 								class="emoji"
-								:src="
-									disableShowingAnimatedImages
-										? getStaticImageUrl(emoji.url)
-										: emoji.url
-								"
+								:emoji="emoji"
+								:normal="true"
 							/>
 						</button>
 					</div>
-					<div v-if="searchResultUnicode.length > 0" class="body">
-						<button
-							v-for="emoji in searchResultUnicode"
-							:key="emoji.name"
-							class="_button item"
-							:title="emoji.name"
-							tabindex="0"
-							@click="chosen(emoji, $event)"
-						>
-							<MkEmoji class="emoji" :emoji="emoji.char" />
-						</button>
-					</div>
 				</section>
 
-				<div v-if="tab === 'index'" class="group index">
-					<section v-if="showPinned">
-						<div class="body">
-							<button
-								v-for="emoji in pinned"
-								:key="emoji"
-								class="_button item"
-								tabindex="0"
-								@click="chosen(emoji, $event)"
-							>
-								<MkEmoji
-									class="emoji"
-									:emoji="emoji"
-									:normal="true"
-								/>
-							</button>
-						</div>
-					</section>
-
-					<section>
-						<header class="_acrylic">
-							<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
-							{{ i18n.ts.recentUsed }}
-						</header>
-						<div class="body">
-							<button
-								v-for="emoji in recentlyUsedEmojis"
-								:key="emoji"
-								class="_button item"
-								@click="chosen(emoji, $event)"
-							>
-								<MkEmoji
-									class="emoji"
-									:emoji="emoji"
-									:normal="true"
-								/>
-							</button>
-						</div>
-					</section>
-				</div>
-				<div v-once class="group">
-					<header>{{ i18n.ts.customEmojis }}</header>
-					<XSection
-						v-for="category in customEmojiCategories"
-						:key="'custom:' + category"
-						:initial-shown="false"
-						:emojis="
-							customEmojis
-								.filter((e) => e.category === category)
-								.map((e) => ':' + e.name + ':')
-						"
-						@chosen="chosen"
-						>{{ category || i18n.ts.other }}</XSection
-					>
-				</div>
-				<div v-once class="group">
-					<header>{{ i18n.ts.emoji }}</header>
-					<XSection
-						v-for="category in categories"
-						:key="category"
-						:emojis="
-							emojilist
-								.filter((e) => e.category === category)
-								.map((e) => e.char)
-						"
-						@chosen="chosen"
-						>{{ category }}</XSection
-					>
-				</div>
+				<section>
+					<header class="_acrylic">
+						<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
+						{{ i18n.ts.recentUsed }}
+					</header>
+					<div class="body">
+						<button
+							v-for="emoji in recentlyUsedEmojis"
+							:key="emoji"
+							class="_button item"
+							@click="chosen(emoji, $event)"
+						>
+							<MkEmoji
+								class="emoji"
+								:emoji="emoji"
+								:normal="true"
+							/>
+						</button>
+					</div>
+				</section>
 			</div>
-			<div class="tabs">
-				<button
-					class="_button tab"
-					:class="{ active: tab === 'index' }"
-					@click="tab = 'index'"
+			<div v-once class="group">
+				<header>{{ i18n.ts.customEmojis }}</header>
+				<XSection
+					v-for="category in customEmojiCategories"
+					:key="'custom:' + category"
+					:initial-shown="false"
+					:emojis="
+						customEmojis
+							.filter((e) => e.category === category)
+							.map((e) => ':' + e.name + ':')
+					"
+					@chosen="chosen"
+					>{{ category || i18n.ts.other }}</XSection
 				>
-					<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
-				</button>
-				<button
-					class="_button tab"
-					:class="{ active: tab === 'custom' }"
-					@click="tab = 'custom'"
+			</div>
+			<div v-once class="group">
+				<header>{{ i18n.ts.emoji }}</header>
+				<XSection
+					v-for="category in categories"
+					:key="category"
+					:emojis="
+						emojilist
+							.filter((e) => e.category === category)
+							.map((e) => e.char)
+					"
+					@chosen="chosen"
+					>{{ category }}</XSection
 				>
-					<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
-				</button>
-				<button
-					class="_button tab"
-					:class="{ active: tab === 'unicode' }"
-					@click="tab = 'unicode'"
-				>
-					<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
-				</button>
-				<button
-					class="_button tab"
-					:class="{ active: tab === 'tags' }"
-					@click="tab = 'tags'"
-				>
-					<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
-				</button>
 			</div>
 		</div>
-	</FocusTrap>
+		<div class="tabs">
+			<button
+				class="_button tab"
+				:class="{ active: tab === 'index' }"
+				@click="tab = 'index'"
+			>
+				<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
+			</button>
+			<button
+				class="_button tab"
+				:class="{ active: tab === 'custom' }"
+				@click="tab = 'custom'"
+			>
+				<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
+			</button>
+			<button
+				class="_button tab"
+				:class="{ active: tab === 'unicode' }"
+				@click="tab = 'unicode'"
+			>
+				<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
+			</button>
+			<button
+				class="_button tab"
+				:class="{ active: tab === 'tags' }"
+				@click="tab = 'tags'"
+			>
+				<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
+			</button>
+		</div>
+	</div>
 </template>
 
 <script lang="ts" setup>
@@ -174,7 +171,6 @@ import { deviceKind } from "@/scripts/device-kind";
 import { emojiCategories, instance } from "@/instance";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
-import { FocusTrap } from 'focus-trap-vue';
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index 759c215f7..f713b4c41 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -139,7 +139,7 @@ function close() {
 			height: 100px;
 			border-radius: 10px;
 
-			&:hover, &:focus-visible {
+			&:hover {
 				color: var(--accent);
 				background: var(--accentedBg);
 				text-decoration: none;
diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue
index 3cfb0f465..882908040 100644
--- a/packages/client/src/components/MkMediaImage.vue
+++ b/packages/client/src/components/MkMediaImage.vue
@@ -138,10 +138,6 @@ watch(
 		background-position: center;
 		background-size: contain;
 		background-repeat: no-repeat;
-		box-sizing: border-box;
-		&:focus-visible {
-			border: 2px solid var(--accent);
-		}
 
 		> .gif {
 			background-color: var(--fg);
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index e5ca9e4ee..6b05ab447 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,14 +1,14 @@
 <template>
-	<div ref="el" class="sfhdhdhr" tabindex="-1">
-			<MkMenu
-				ref="menu"
-				:items="items"
-				:align="align"
-				:width="width"
-				:as-drawer="false"
-				@close="onChildClosed"
-			/>
-		</div>
+	<div ref="el" class="sfhdhdhr">
+		<MkMenu
+			ref="menu"
+			:items="items"
+			:align="align"
+			:width="width"
+			:as-drawer="false"
+			@close="onChildClosed"
+		/>
+	</div>
 </template>
 
 <script lang="ts" setup>
@@ -23,6 +23,7 @@ import {
 } from "vue";
 import MkMenu from "./MkMenu.vue";
 import { MenuItem } from "@/types/menu";
+import * as os from "@/os";
 
 const props = defineProps<{
 	items: MenuItem[];
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index c71e3ac58..88c8af1c5 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,188 +1,191 @@
 <template>
-	<FocusTrap v-bind:active="isActive">
-		<div tabindex="-1" v-focus>
-			<div
-				ref="itemsEl"
-				class="rrevdjwt _popup _shadow"
-				:class="{ center: align === 'center', asDrawer }"
-				:style="{
-					width: width && !asDrawer ? width + 'px' : '',
-					maxHeight: maxHeight ? maxHeight + 'px' : '',
-				}"
-				@contextmenu.self="(e) => e.preventDefault()"
-			>
-				<template v-for="(item, i) in items2">
-					<div v-if="item === null" class="divider"></div>
-					<span v-else-if="item.type === 'label'" class="label item">
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
-					</span>
-					<span
-						v-else-if="item.type === 'pending'"
-						class="pending item"
-					>
-						<span><MkEllipsis /></span>
-					</span>
-					<MkA
-						v-else-if="item.type === 'link'"
-						:to="item.to"
-						class="_button item"
-						@click.passive="close(true)"
-						@mouseenter.passive="onItemMouseEnter(item)"
-						@mouseleave.passive="onItemMouseLeave(item)"
-					>
-						<i
-							v-if="item.icon"
-							class="ph-fw ph-lg"
-							:class="item.icon"
-						></i>
-						<span v-else-if="item.icons">
-							<i
-								v-for="icon in item.icons"
-								class="ph-fw ph-lg"
-								:class="icon"
-							></i>
-						</span>
-						<MkAvatar
-							v-if="item.avatar"
-							:user="item.avatar"
-							class="avatar"
-							disableLink
-						/>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
-						<span v-if="item.indicate" class="indicator"
-							><i class="ph-circle ph-fill"></i
-						></span>
-					</MkA>
-					<a
-						v-else-if="item.type === 'a'"
-						:href="item.href"
-						:target="item.target"
-						:download="item.download"
-						class="_button item"
-						@click="close(true)"
-						@mouseenter.passive="onItemMouseEnter(item)"
-						@mouseleave.passive="onItemMouseLeave(item)"
-					>
-						<i
-							v-if="item.icon"
-							class="ph-fw ph-lg"
-							:class="item.icon"
-						></i>
-						<span v-else-if="item.icons">
-							<i
-								v-for="icon in item.icons"
-								class="ph-fw ph-lg"
-								:class="icon"
-							></i>
-						</span>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
-						<span v-if="item.indicate" class="indicator"
-							><i class="ph-circle ph-fill"></i
-						></span>
-					</a>
-					<button
-						v-else-if="item.type === 'user' && !items.hidden"
-						class="_button item"
-						:class="{ active: item.active }"
-						:disabled="item.active"
-						@click="clicked(item.action, $event)"
-						@mouseenter.passive="onItemMouseEnter(item)"
-						@mouseleave.passive="onItemMouseLeave(item)"
-					>
-						<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
-							:user="item.user"
-						/>
-						<span v-if="item.indicate" class="indicator"
-							><i class="ph-circle ph-fill"></i
-						></span>
-					</button>
-					<span
-						v-else-if="item.type === 'switch'"
-						class="item"
-						@mouseenter.passive="onItemMouseEnter(item)"
-						@mouseleave.passive="onItemMouseLeave(item)"
-					>
-						<FormSwitch
-							v-model="item.ref"
-							:disabled="item.disabled"
-							class="form-switch"
-							:style="item.textStyle || ''"
-							>{{ item.text }}</FormSwitch
-						>
-					</span>
-					<button
-						v-else-if="item.type === 'parent'"
-						class="_button item parent"
-						:class="{ childShowing: childShowingItem === item }"
-						@mouseenter="showChildren(item, $event)"
-						@click="showChildren(item, $event)"
-					>
-						<i
-							v-if="item.icon"
-							class="ph-fw ph-lg"
-							:class="item.icon"
-						></i>
-						<span v-else-if="item.icons">
-							<i
-								v-for="icon in item.icons"
-								class="ph-fw ph-lg"
-								:class="icon"
-							></i>
-						</span>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
-						<span class="caret"
-							><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
-						></span>
-					</button>
-					<button
-						v-else-if="!item.hidden"
-						class="_button item"
-						:class="{ danger: item.danger, active: item.active }"
-						:disabled="item.active"
-						@click="clicked(item.action, $event)"
-						@mouseenter.passive="onItemMouseEnter(item)"
-						@mouseleave.passive="onItemMouseLeave(item)"
-					>
-						<i
-							v-if="item.icon"
-							class="ph-fw ph-lg"
-							:class="item.icon"
-						></i>
-						<span v-else-if="item.icons">
-							<i
-								v-for="icon in item.icons"
-								class="ph-fw ph-lg"
-								:class="icon"
-							></i>
-						</span>
-						<MkAvatar
-							v-if="item.avatar"
-							:user="item.avatar"
-							class="avatar"
-							disableLink
-						/>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
-						<span v-if="item.indicate" class="indicator"
-							><i class="ph-circle ph-fill"></i
-						></span>
-					</button>
-				</template>
-				<span v-if="items2.length === 0" class="none item">
-					<span>{{ i18n.ts.none }}</span>
+	<div>
+		<div
+			ref="itemsEl"
+			v-hotkey="keymap"
+			class="rrevdjwt _popup _shadow"
+			:class="{ center: align === 'center', asDrawer }"
+			:style="{
+				width: width && !asDrawer ? width + 'px' : '',
+				maxHeight: maxHeight ? maxHeight + 'px' : '',
+			}"
+			@contextmenu.self="(e) => e.preventDefault()"
+		>
+			<template v-for="(item, i) in items2">
+				<div v-if="item === null" class="divider"></div>
+				<span v-else-if="item.type === 'label'" class="label item">
+					<span :style="item.textStyle || ''">{{ item.text }}</span>
 				</span>
-			</div>
-			<div v-if="childMenu" class="child">
-				<XChild
-					ref="child"
-					:items="childMenu"
-					:target-element="childTarget"
-					:root-element="itemsEl"
-					showing
-					@actioned="childActioned"
-				/>
-			</div>
+				<span
+					v-else-if="item.type === 'pending'"
+					:tabindex="i"
+					class="pending item"
+				>
+					<span><MkEllipsis /></span>
+				</span>
+				<MkA
+					v-else-if="item.type === 'link'"
+					:to="item.to"
+					:tabindex="i"
+					class="_button item"
+					@click.passive="close(true)"
+					@mouseenter.passive="onItemMouseEnter(item)"
+					@mouseleave.passive="onItemMouseLeave(item)"
+				>
+					<i
+						v-if="item.icon"
+						class="ph-fw ph-lg"
+						:class="item.icon"
+					></i>
+					<span v-else-if="item.icons">
+						<i
+							v-for="icon in item.icons"
+							class="ph-fw ph-lg"
+							:class="icon"
+						></i>
+					</span>
+					<MkAvatar
+						v-if="item.avatar"
+						:user="item.avatar"
+						class="avatar"
+					/>
+					<span :style="item.textStyle || ''">{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"
+						><i class="ph-circle ph-fill"></i
+					></span>
+				</MkA>
+				<a
+					v-else-if="item.type === 'a'"
+					:href="item.href"
+					:target="item.target"
+					:download="item.download"
+					:tabindex="i"
+					class="_button item"
+					@click="close(true)"
+					@mouseenter.passive="onItemMouseEnter(item)"
+					@mouseleave.passive="onItemMouseLeave(item)"
+				>
+					<i
+						v-if="item.icon"
+						class="ph-fw ph-lg"
+						:class="item.icon"
+					></i>
+					<span v-else-if="item.icons">
+						<i
+							v-for="icon in item.icons"
+							class="ph-fw ph-lg"
+							:class="icon"
+						></i>
+					</span>
+					<span :style="item.textStyle || ''">{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"
+						><i class="ph-circle ph-fill"></i
+					></span>
+				</a>
+				<button
+					v-else-if="item.type === 'user' && !items.hidden"
+					:tabindex="i"
+					class="_button item"
+					:class="{ active: item.active }"
+					:disabled="item.active"
+					@click="clicked(item.action, $event)"
+					@mouseenter.passive="onItemMouseEnter(item)"
+					@mouseleave.passive="onItemMouseLeave(item)"
+				>
+					<MkAvatar :user="item.user" class="avatar" /><MkUserName
+						:user="item.user"
+					/>
+					<span v-if="item.indicate" class="indicator"
+						><i class="ph-circle ph-fill"></i
+					></span>
+				</button>
+				<span
+					v-else-if="item.type === 'switch'"
+					:tabindex="i"
+					class="item"
+					@mouseenter.passive="onItemMouseEnter(item)"
+					@mouseleave.passive="onItemMouseLeave(item)"
+				>
+					<FormSwitch
+						v-model="item.ref"
+						:disabled="item.disabled"
+						class="form-switch"
+						:style="item.textStyle || ''"
+						>{{ item.text }}</FormSwitch
+					>
+				</span>
+				<button
+					v-else-if="item.type === 'parent'"
+					:tabindex="i"
+					class="_button item parent"
+					:class="{ childShowing: childShowingItem === item }"
+					@mouseenter="showChildren(item, $event)"
+				>
+					<i
+						v-if="item.icon"
+						class="ph-fw ph-lg"
+						:class="item.icon"
+					></i>
+					<span v-else-if="item.icons">
+						<i
+							v-for="icon in item.icons"
+							class="ph-fw ph-lg"
+							:class="icon"
+						></i>
+					</span>
+					<span :style="item.textStyle || ''">{{ item.text }}</span>
+					<span class="caret"
+						><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
+					></span>
+				</button>
+				<button
+					v-else-if="!item.hidden"
+					:tabindex="i"
+					class="_button item"
+					:class="{ danger: item.danger, active: item.active }"
+					:disabled="item.active"
+					@click="clicked(item.action, $event)"
+					@mouseenter.passive="onItemMouseEnter(item)"
+					@mouseleave.passive="onItemMouseLeave(item)"
+				>
+					<i
+						v-if="item.icon"
+						class="ph-fw ph-lg"
+						:class="item.icon"
+					></i>
+					<span v-else-if="item.icons">
+						<i
+							v-for="icon in item.icons"
+							class="ph-fw ph-lg"
+							:class="icon"
+						></i>
+					</span>
+					<MkAvatar
+						v-if="item.avatar"
+						:user="item.avatar"
+						class="avatar"
+					/>
+					<span :style="item.textStyle || ''">{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"
+						><i class="ph-circle ph-fill"></i
+					></span>
+				</button>
+			</template>
+			<span v-if="items2.length === 0" class="none item">
+				<span>{{ i18n.ts.none }}</span>
+			</span>
 		</div>
-	</FocusTrap>
+		<div v-if="childMenu" class="child">
+			<XChild
+				ref="child"
+				:items="childMenu"
+				:target-element="childTarget"
+				:root-element="itemsEl"
+				showing
+				@actioned="childActioned"
+			/>
+		</div>
+	</div>
 </template>
 
 <script lang="ts" setup>
@@ -203,7 +206,6 @@ import FormSwitch from "@/components/form/switch.vue";
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
-import { FocusTrap } from 'focus-trap-vue';
 
 const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
 
@@ -226,6 +228,12 @@ let items2: InnerMenuItem[] = $ref([]);
 
 let child = $ref<InstanceType<typeof XChild>>();
 
+let keymap = computed(() => ({
+	"up|k|shift+tab": focusUp,
+	"down|j|tab": focusDown,
+	esc: close,
+}));
+
 let childShowingItem = $ref<MenuItem | null>();
 
 watch(
@@ -356,7 +364,8 @@ onBeforeUnmount(() => {
 		font-size: 0.9em;
 		line-height: 20px;
 		text-align: left;
-		outline: none;
+		overflow: hidden;
+		text-overflow: ellipsis;
 
 		&:before {
 			content: "";
@@ -380,7 +389,7 @@ onBeforeUnmount(() => {
 			transform: translateY(0em);
 		}
 
-		&:not(:disabled):hover, &:focus-visible {
+		&:not(:disabled):hover {
 			color: var(--accent);
 			text-decoration: none;
 
@@ -388,9 +397,6 @@ onBeforeUnmount(() => {
 				background: var(--accentedBg);
 			}
 		}
-		&:focus-visible:before {
-			outline: auto;
-		}
 
 		&.danger {
 			color: #eb6f92;
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 12e79f428..d9cd56f95 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -14,59 +14,54 @@
 		:duration="transitionDuration"
 		appear
 		@after-leave="emit('closed')"
-		@keyup.esc="emit('click')"
 		@enter="emit('opening')"
 		@after-enter="onOpened"
 	>
-		<FocusTrap v-model:active="isActive">
+		<div
+			v-show="manualShowing != null ? manualShowing : showing"
+			v-hotkey.global="keymap"
+			:class="[
+				$style.root,
+				{
+					[$style.drawer]: type === 'drawer',
+					[$style.dialog]: type === 'dialog' || type === 'dialog:top',
+					[$style.popup]: type === 'popup',
+				},
+			]"
+			:style="{
+				zIndex,
+				pointerEvents: (manualShowing != null ? manualShowing : showing)
+					? 'auto'
+					: 'none',
+				'--transformOrigin': transformOrigin,
+			}"
+		>
 			<div
-				v-show="manualShowing != null ? manualShowing : showing"
-				v-hotkey.global="keymap"
+				class="_modalBg data-cy-bg"
 				:class="[
-					$style.root,
+					$style.bg,
 					{
-						[$style.drawer]: type === 'drawer',
-						[$style.dialog]: type === 'dialog' || type === 'dialog:top',
-						[$style.popup]: type === 'popup',
+						[$style.bgTransparent]: isEnableBgTransparent,
+						'data-cy-transparent': isEnableBgTransparent,
 					},
 				]"
-				:style="{
-					zIndex,
-					pointerEvents: (manualShowing != null ? manualShowing : showing)
-						? 'auto'
-						: 'none',
-					'--transformOrigin': transformOrigin,
-				}"
-				tabindex="-1"
-				v-focus
+				:style="{ zIndex }"
+				@click="onBgClick"
+				@mousedown="onBgClick"
+				@contextmenu.prevent.stop="() => {}"
+			></div>
+			<div
+				ref="content"
+				:class="[
+					$style.content,
+					{ [$style.fixed]: fixed, top: type === 'dialog:top' },
+				]"
+				:style="{ zIndex }"
+				@click.self="onBgClick"
 			>
-				<div
-					class="_modalBg data-cy-bg"
-					:class="[
-						$style.bg,
-						{
-							[$style.bgTransparent]: isEnableBgTransparent,
-							'data-cy-transparent': isEnableBgTransparent,
-						},
-					]"
-					:style="{ zIndex }"
-					@click="onBgClick"
-					@mousedown="onBgClick"
-					@contextmenu.prevent.stop="() => {}"
-				></div>
-				<div
-					ref="content"
-					:class="[
-						$style.content,
-						{ [$style.fixed]: fixed, top: type === 'dialog:top' },
-					]"
-					:style="{ zIndex }"
-					@click.self="onBgClick"
-				>
-					<slot :max-height="maxHeight" :type="type"></slot>
-				</div>
+				<slot :max-height="maxHeight" :type="type"></slot>
 			</div>
-		</FocusTrap>
+		</div>
 	</Transition>
 </template>
 
@@ -76,7 +71,6 @@ import * as os from "@/os";
 import { isTouchUsing } from "@/scripts/touch";
 import { defaultStore } from "@/store";
 import { deviceKind } from "@/scripts/device-kind";
-import { FocusTrap } from 'focus-trap-vue';
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === "BODY") return null;
@@ -172,7 +166,6 @@ let transitionDuration = $computed(() =>
 
 let contentClicking = false;
 
-const focusedElement = document.activeElement;
 function close(opts: { useSendAnimation?: boolean } = {}) {
 	if (opts.useSendAnimation) {
 		useSendAnime = true;
@@ -182,12 +175,10 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
 	if (props.src) props.src.style.pointerEvents = "auto";
 	showing = false;
 	emit("close");
-	focusedElement.focus();
 }
 
 function onBgClick() {
 	if (contentClicking) return;
-	focusedElement.focus();
 	emit("click");
 }
 
@@ -490,7 +481,6 @@ defineExpose({
 }
 
 .root {
-	outline: none;
 	&.dialog {
 		> .content {
 			position: fixed;
diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue
index bf4d8d0bc..361128464 100644
--- a/packages/client/src/components/MkModalPageWindow.vue
+++ b/packages/client/src/components/MkModalPageWindow.vue
@@ -158,7 +158,6 @@ function onContextmenu(ev: MouseEvent) {
 	flex-direction: column;
 	contain: content;
 	border-radius: var(--radius);
-	margin: auto;
 
 	--root-margin: 24px;
 
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 017bfae8c..3afcff6cb 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -3,64 +3,59 @@
 		ref="modal"
 		:prefer-type="'dialog'"
 		@click="onBgClick"
-		@keyup.esc="$emit('close')"
 		@closed="$emit('closed')"
 	>
-		<FocusTrap v-model:active="isActive">
-			<div
-				ref="rootEl"
-				class="ebkgoccj"
-				:style="{
-					width: `${width}px`,
-					height: scroll
-						? height
-							? `${height}px`
-							: null
-						: height
-						? `min(${height}px, 100%)`
-						: '100%',
-				}"
-				@keydown="onKeydown"
-				tabindex="-1"
-			>
-				<div ref="headerEl" class="header">
-					<button
-						v-if="withOkButton"
-						class="_button"
-						@click="$emit('close')"
-					>
-						<i class="ph-x ph-bold ph-lg"></i>
-					</button>
-					<span class="title">
-						<slot name="header"></slot>
-					</span>
-					<button
-						v-if="!withOkButton"
-						class="_button"
-						@click="$emit('close')"
-					>
-						<i class="ph-x ph-bold ph-lg"></i>
-					</button>
-					<button
-						v-if="withOkButton"
-						class="_button"
-						:disabled="okButtonDisabled"
-						@click="$emit('ok')"
-					>
-						<i class="ph-check ph-bold ph-lg"></i>
-					</button>
-				</div>
-				<div class="body">
-					<slot :width="bodyWidth" :height="bodyHeight"></slot>
-				</div>
+		<div
+			ref="rootEl"
+			class="ebkgoccj"
+			:style="{
+				width: `${width}px`,
+				height: scroll
+					? height
+						? `${height}px`
+						: null
+					: height
+					? `min(${height}px, 100%)`
+					: '100%',
+			}"
+			@keydown="onKeydown"
+		>
+			<div ref="headerEl" class="header">
+				<button
+					v-if="withOkButton"
+					class="_button"
+					@click="$emit('close')"
+				>
+					<i class="ph-x ph-bold ph-lg"></i>
+				</button>
+				<span class="title">
+					<slot name="header"></slot>
+				</span>
+				<button
+					v-if="!withOkButton"
+					class="_button"
+					@click="$emit('close')"
+				>
+					<i class="ph-x ph-bold ph-lg"></i>
+				</button>
+				<button
+					v-if="withOkButton"
+					class="_button"
+					:disabled="okButtonDisabled"
+					@click="$emit('ok')"
+				>
+					<i class="ph-check ph-bold ph-lg"></i>
+				</button>
 			</div>
-		</FocusTrap>
+			<div class="body">
+				<slot :width="bodyWidth" :height="bodyHeight"></slot>
+			</div>
+		</div>
 	</MkModal>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted } from "vue";
-import { FocusTrap } from 'focus-trap-vue';
 import MkModal from "./MkModal.vue";
 
 const props = withDefaults(
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index 5d9c40d38..22a7ef93f 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -84,7 +84,6 @@
 						:detailedView="detailedView"
 						:parentId="appearNote.parentId"
 						@push="(e) => router.push(notePage(e))"
-						@focusfooter="footerEl.focus()"
 					></MkSubNoteContent>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -118,7 +117,7 @@
 						<MkTime :time="appearNote.createdAt" mode="absolute" />
 					</MkA>
 				</div>
-				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
+				<footer ref="el" class="footer" @click.stop>
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -279,7 +278,6 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
-const footerEl = ref<HTMLElement>(); 
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
@@ -300,8 +298,8 @@ const keymap = {
 	r: () => reply(true),
 	"e|a|plus": () => react(true),
 	q: () => renoteButton.value.renote(true),
-	"up|k": focusBefore,
-	"down|j": focusAfter,
+	"up|k|shift+tab": focusBefore,
+	"down|j|tab": focusAfter,
 	esc: blur,
 	"m|o": () => menu(true),
 	s: () => showContent.value !== showContent.value,
diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue
index 6fdd79dc6..9d388e71b 100644
--- a/packages/client/src/components/MkNotePreview.vue
+++ b/packages/client/src/components/MkNotePreview.vue
@@ -1,6 +1,6 @@
 <template>
 	<div v-size="{ min: [350, 500] }" class="fefdfafb">
-		<MkAvatar class="avatar" :user="$i" disableLink />
+		<MkAvatar class="avatar" :user="$i" />
 		<div class="main">
 			<div class="header">
 				<MkUserName :user="$i" />
diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index a0b70ff1f..f5e70891f 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -26,7 +26,6 @@
 						:note="note"
 						:parentId="appearNote.parentId"
 						:conversation="conversation"
-						@focusfooter="footerEl.focus()"
 					/>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -47,7 +46,7 @@
 						</div>
 					</div>
 				</div>
-				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
+				<footer class="footer" @click.stop>
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -213,7 +212,6 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
-const footerEl = ref<HTMLElement>();
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index 5f1ed037b..4d52616e1 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -7,8 +7,6 @@
 		:transparent-bg="true"
 		@click="modal.close()"
 		@closed="emit('closed')"
-		tabindex="-1"
-		v-focus
 	>
 		<MkMenu
 			:items="items"
diff --git a/packages/client/src/components/MkPostFormAttaches.vue b/packages/client/src/components/MkPostFormAttaches.vue
index 7cf397e55..7c7f240e8 100644
--- a/packages/client/src/components/MkPostFormAttaches.vue
+++ b/packages/client/src/components/MkPostFormAttaches.vue
@@ -198,6 +198,7 @@ export default defineComponent({
 			height: 64px;
 			margin-right: 4px;
 			border-radius: 4px;
+			overflow: hidden;
 			cursor: move;
 
 			&:hover > .remove {
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 68439527a..a1f7cc1b9 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -35,11 +35,7 @@
 			class="content"
 			:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
 		>
-			<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" />
-			<div 
-				class="body"
-				v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
-			>
+			<div class="body">
 				<span v-if="note.deletedAt" style="opacity: 0.5"
 					>({{ i18n.ts.deleted }})</span
 				>
@@ -100,20 +96,15 @@
 						<XNoteSimple :note="note.renote" />
 					</div>
 				</template>
-				<div
-					v-if="note.cw && !showContent"
-					tabindex="0"
-					v-on:focus="cwButton?.focus()"
-				></div>
 			</div>
 			<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
-			<XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" />
+			<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
 		</div>
 	</div>
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue"; 
+import {} from "vue";
 import * as misskey from "calckey-js";
 import * as mfm from "mfm-js";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
@@ -135,10 +126,8 @@ const props = defineProps<{
 
 const emit = defineEmits<{
 	(ev: "push", v): void;
-	(ev: "focusfooter"): void;
 }>();
 
-const cwButton = ref<HTMLElement>(); 
 const isLong =
 	!props.detailedView &&
 	props.note.cw == null &&
@@ -151,13 +140,6 @@ const urls = props.note.text
 	: null;
 
 let showContent = $ref(false);
-
-
-function focusFooter(ev) {
-	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
-		emit("focusfooter");
-	}
-}
 </script>
 
 <style lang="scss" scoped>
@@ -249,9 +231,6 @@ function focusFooter(ev) {
 				margin-top: -50px;
 				padding-top: 50px;
 				overflow: hidden;
-				user-select: none;
-				-webkit-user-select: none;
-				-moz-user-select: none;
 			}
 			&.collapsed > .body {
 				box-sizing: border-box;
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index 83c667070..55d6fba50 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -9,6 +9,7 @@
 						v-if="item.type === 'a'"
 						:href="item.href"
 						:target="item.target"
+						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 					>
@@ -21,6 +22,7 @@
 					</a>
 					<button
 						v-else-if="item.type === 'button'"
+						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 						:disabled="item.active"
@@ -36,6 +38,7 @@
 					<MkA
 						v-else
 						:to="item.to"
+						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 					>
@@ -96,7 +99,7 @@ export default defineComponent({
 				font-size: 0.9em;
 				margin-bottom: 0.3rem;
 
-				&:hover, &:focus-visible {
+				&:hover {
 					text-decoration: none;
 					background: var(--panelHighlight);
 				}
diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue
index 14553ca46..506f48bd4 100644
--- a/packages/client/src/components/MkUserSelectDialog.vue
+++ b/packages/client/src/components/MkUserSelectDialog.vue
@@ -46,7 +46,6 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
-							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
@@ -74,7 +73,6 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
-							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue
index 78a4f90f2..972864d1f 100644
--- a/packages/client/src/components/MkUsersTooltip.vue
+++ b/packages/client/src/components/MkUsersTooltip.vue
@@ -7,7 +7,7 @@
 	>
 		<div class="beaffaef">
 			<div v-for="u in users" :key="u.id" class="user">
-				<MkAvatar class="avatar" :user="u" disableLink />
+				<MkAvatar class="avatar" :user="u" />
 				<MkUserName class="name" :user="u" :nowrap="true" />
 			</div>
 			<div v-if="users.length < count" class="omitted">
diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue
index d48fc5383..07e845032 100644
--- a/packages/client/src/components/MkWidgets.vue
+++ b/packages/client/src/components/MkWidgets.vue
@@ -1,7 +1,7 @@
 <template>
 	<div class="vjoppmmu">
 		<template v-if="edit">
-			<header tabindex="-1" v-focus>
+			<header>
 				<MkSelect
 					v-model="widgetAdderSelected"
 					style="margin-bottom: var(--margin)"
diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue
index a2fde5341..8868f5784 100644
--- a/packages/client/src/components/form/folder.vue
+++ b/packages/client/src/components/form/folder.vue
@@ -1,6 +1,6 @@
 <template>
 	<div class="dwzlatin" :class="{ opened }">
-		<button class="header _button" @click="toggle">
+		<div class="header _button" @click="toggle">
 			<span class="icon"><slot name="icon"></slot></span>
 			<span class="text"><slot name="label"></slot></span>
 			<span class="right">
@@ -8,7 +8,7 @@
 				<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
 				<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
 			</span>
-		</button>
+		</div>
 		<KeepAlive>
 			<div v-if="openedAtLeastOnce" v-show="opened" class="body">
 				<MkSpacer :margin-min="14" :margin-max="22">
diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue
index ef644b327..493b2d010 100644
--- a/packages/client/src/components/form/radio.vue
+++ b/packages/client/src/components/form/radio.vue
@@ -66,9 +66,6 @@ function toggle(): void {
 	&:hover {
 		border-color: var(--inputBorderHover) !important;
 	}
-	&:focus-within {
-		outline: auto;
-	}
 
 	&.checked {
 		background-color: var(--accentedBg) !important;
diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue
index b1c6df4e9..efaf488a9 100644
--- a/packages/client/src/components/form/switch.vue
+++ b/packages/client/src/components/form/switch.vue
@@ -99,9 +99,6 @@ const toggle = () => {
 			border-color: var(--inputBorderHover) !important;
 		}
 	}
-	&:focus-within > .button {
-		outline: auto;
-	}
 
 	> .label {
 		margin-left: 12px;
diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue
index c78ef0c10..ad1d80ca6 100644
--- a/packages/client/src/components/global/MkPageHeader.vue
+++ b/packages/client/src/components/global/MkPageHeader.vue
@@ -19,7 +19,6 @@
 				class="avatar"
 				:user="$i"
 				:disable-preview="true"
-				disableLink
 			/>
 		</div>
 		<template v-if="metadata">
@@ -34,7 +33,6 @@
 					:user="metadata.avatar"
 					:disable-preview="true"
 					:show-indicator="true"
-					disableLink
 				/>
 				<i
 					v-else-if="metadata.icon && !narrow"
diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index 437b7c53e..8423ce773 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -5,9 +5,6 @@
 				:is="currentPageComponent"
 				:key="key"
 				v-bind="Object.fromEntries(currentPageProps)"
-				tabindex="-1"
-				v-focus
-				style="outline: none;"
 			/>
 
 			<template #fallback>
diff --git a/packages/client/src/directives/focus.ts b/packages/client/src/directives/focus.ts
deleted file mode 100644
index 4d34fbf1f..000000000
--- a/packages/client/src/directives/focus.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
-	mounted: (el) => el.focus()
-}
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
index 77639e2f3..0a5c32326 100644
--- a/packages/client/src/directives/index.ts
+++ b/packages/client/src/directives/index.ts
@@ -11,7 +11,6 @@ import anim from "./anim";
 import clickAnime from "./click-anime";
 import panel from "./panel";
 import adaptiveBorder from "./adaptive-border";
-import focus from "./focus";
 
 export default function (app: App) {
 	app.directive("userPreview", userPreview);
@@ -26,5 +25,4 @@ export default function (app: App) {
 	app.directive("click-anime", clickAnime);
 	app.directive("panel", panel);
 	app.directive("adaptive-border", adaptiveBorder);
-	app.directive("focus", focus);
 }
diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
index 91024a6e3..7738d14e8 100644
--- a/packages/client/src/directives/tooltip.ts
+++ b/packages/client/src/directives/tooltip.ts
@@ -76,32 +76,23 @@ export default {
 			ev.preventDefault();
 		});
 
-		function showTooltip() {
-			window.clearTimeout(self.showTimer);
-			window.clearTimeout(self.hideTimer);
-			self.showTimer = window.setTimeout(self.show, delay);
-		}
-		function hideTooltip() {
-			window.clearTimeout(self.showTimer);
-			window.clearTimeout(self.hideTimer);
-			self.hideTimer = window.setTimeout(self.close, delay);
-		}
-
 		el.addEventListener(
-			start, showTooltip,
-			{ passive: true },
-		);
-		el.addEventListener(
-			"focusin", showTooltip,
+			start,
+			() => {
+				window.clearTimeout(self.showTimer);
+				window.clearTimeout(self.hideTimer);
+				self.showTimer = window.setTimeout(self.show, delay);
+			},
 			{ passive: true },
 		);
 
 		el.addEventListener(
-			end, hideTooltip,
-			{ passive: true },
-		);
-		el.addEventListener(
-			"focusout", hideTooltip,
+			end,
+			() => {
+				window.clearTimeout(self.showTimer);
+				window.clearTimeout(self.hideTimer);
+				self.hideTimer = window.setTimeout(self.close, delay);
+			},
 			{ passive: true },
 		);
 
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index bf070e269..69fd1bc58 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -313,7 +313,11 @@ onUnmounted(() => {
 			font-weight: normal;
 			opacity: 0.7;
 
-			&:hover, &:focus-visible, &.active {
+			&:hover {
+				opacity: 1;
+			}
+
+			&.active {
 				opacity: 1;
 			}
 
diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue
index db953b890..6184cfb10 100644
--- a/packages/client/src/pages/admin/overview.moderators.vue
+++ b/packages/client/src/pages/admin/overview.moderators.vue
@@ -12,7 +12,7 @@
 					class="user"
 					:to="`/user-info/${user.id}`"
 				>
-					<MkAvatar :user="user" class="avatar" indicator disableLink />
+					<MkAvatar :user="user" class="avatar" indicator />
 				</MkA>
 			</div>
 		</Transition>
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index 35279495b..2aac52163 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -23,7 +23,6 @@
 								class="avatar"
 								:user="req.follower"
 								:show-indicator="true"
-								disableLink
 							/>
 							<div class="body">
 								<div class="name">
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index 3010354b6..ec2cd2477 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -6,14 +6,14 @@
 				{{ i18n.ts.addAccount }}</FormButton
 			>
 
-			<button
+			<div
 				v-for="account in accounts"
 				:key="account.id"
 				class="_panel _button lcjjdxlm"
 				@click="menu(account, $event)"
 			>
 				<div class="avatar">
-					<MkAvatar :user="account" class="avatar" disableLink />
+					<MkAvatar :user="account" class="avatar" />
 				</div>
 				<div class="body">
 					<div class="name">
@@ -23,7 +23,7 @@
 						<MkAcct :user="account" />
 					</div>
 				</div>
-			</button>
+			</div>
 		</FormSuspense>
 	</div>
 </template>
@@ -158,8 +158,6 @@ definePageMetadata({
 .lcjjdxlm {
 	display: flex;
 	padding: 16px;
-	width: 100%;
-	text-align: unset;
 
 	> .avatar {
 		display: block;
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index 52c7b62f4..051edf6e0 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -204,6 +204,10 @@ hr {
 		pointer-events: none;
 	}
 
+	&:focus-visible {
+		outline: none;
+	}
+
 	&:disabled {
 		opacity: 0.5;
 		cursor: default;
diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue
index 39abb7c26..43c91d147 100644
--- a/packages/client/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue
@@ -18,7 +18,6 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
-						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 20c177f37..380f77c3c 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -18,7 +18,6 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
-						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
@@ -335,7 +334,6 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
-					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -400,6 +398,8 @@ function more(ev: MouseEvent) {
 					padding-left: 30px;
 					line-height: 2.85rem;
 					margin-bottom: 0.5rem;
+					text-overflow: ellipsis;
+					overflow: hidden;
 					white-space: nowrap;
 					width: 100%;
 					text-align: left;
@@ -425,12 +425,9 @@ function more(ev: MouseEvent) {
 					> .text {
 						position: relative;
 						font-size: 0.9em;
-						overflow: hidden;
-						text-overflow: ellipsis;
 					}
 
-					&:hover,
-					&:focus-within {
+					&:hover {
 						text-decoration: none;
 						color: var(--navHoverFg);
 						transition: all 0.4s ease;
@@ -440,8 +437,7 @@ function more(ev: MouseEvent) {
 						color: var(--navActive);
 					}
 
-					&:hover, 
-					&:focus-within,
+					&:hover,
 					&.active {
 						color: var(--accent);
 						transition: all 0.4s ease;
@@ -532,7 +528,6 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
-					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -618,7 +613,6 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
-					&:focus-within,
 					&.active {
 						text-decoration: none;
 						color: var(--accent);
@@ -648,12 +642,5 @@ function more(ev: MouseEvent) {
 			}
 		}
 	}
-
-	.item {
-		outline: none;
-		&:focus-visible:before {
-			outline: auto;
-		}
-	}
 }
 </style>
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
index 99a0ab098..5c3e6b702 100644
--- a/packages/client/src/ui/classic.header.vue
+++ b/packages/client/src/ui/classic.header.vue
@@ -83,7 +83,6 @@
 					<MkAvatar :user="$i" class="avatar" /><MkAcct
 						class="acct"
 						:user="$i"
-						disableLink
 					/>
 				</button>
 				<div class="post" @click="post">
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index fa72c5765..b70a3c984 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -5,7 +5,7 @@
 			class="item _button account"
 			@click="openAccountMenu"
 		>
-			<MkAvatar :user="$i" class="avatar" disableLink /><MkAcct
+			<MkAvatar :user="$i" class="avatar" /><MkAcct
 				class="text"
 				:user="$i"
 			/>
@@ -299,7 +299,6 @@ function openInstanceMenu(ev: MouseEvent) {
 				width: 46px;
 				height: 46px;
 				padding: 0;
-				margin-inline: 0 !important;
 			}
 		}
 
@@ -373,7 +372,6 @@ function openInstanceMenu(ev: MouseEvent) {
 
 		> i {
 			width: 32px;
-			justify-content: center;
 		}
 
 		> i,
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 266effd9a..a721ffd0b 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -227,8 +227,6 @@ onMounted(() => {
 }
 
 .gbhvwtnk {
-	display: flex;
-	justify-content: center;
 	$ui-font-size: 1em;
 	$widgets-hide-threshold: 1200px;
 
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 60e42e11a..9f0b4195d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,12 +19,6 @@ importers:
       '@tensorflow/tfjs':
         specifier: ^3.21.0
         version: 3.21.0(seedrandom@3.0.5)
-      focus-trap:
-        specifier: ^7.2.0
-        version: 7.2.0
-      focus-trap-vue:
-        specifier: ^4.0.1
-        version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
@@ -3809,12 +3803,14 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       source-map: 0.6.1
+    dev: true
 
   /@vue/compiler-dom@3.2.45:
     resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
     dependencies:
       '@vue/compiler-core': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
 
   /@vue/compiler-sfc@2.7.14:
     resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@@ -3837,12 +3833,14 @@ packages:
       magic-string: 0.25.9
       postcss: 8.4.21
       source-map: 0.6.1
+    dev: true
 
   /@vue/compiler-ssr@3.2.45:
     resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
     dependencies:
       '@vue/compiler-dom': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
 
   /@vue/reactivity-transform@3.2.45:
     resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@@ -3852,17 +3850,20 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       magic-string: 0.25.9
+    dev: true
 
   /@vue/reactivity@3.2.45:
     resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
     dependencies:
       '@vue/shared': 3.2.45
+    dev: true
 
   /@vue/runtime-core@3.2.45:
     resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
     dependencies:
       '@vue/reactivity': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
 
   /@vue/runtime-dom@3.2.45:
     resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@@ -3870,6 +3871,7 @@ packages:
       '@vue/runtime-core': 3.2.45
       '@vue/shared': 3.2.45
       csstype: 2.6.21
+    dev: true
 
   /@vue/server-renderer@3.2.45(vue@3.2.45):
     resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@@ -3879,9 +3881,11 @@ packages:
       '@vue/compiler-ssr': 3.2.45
       '@vue/shared': 3.2.45
       vue: 3.2.45
+    dev: true
 
   /@vue/shared@3.2.45:
     resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
+    dev: true
 
   /@webassemblyjs/ast@1.11.1:
     resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@@ -6070,6 +6074,7 @@ packages:
 
   /csstype@2.6.21:
     resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
+    dev: true
 
   /csstype@3.1.1:
     resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
@@ -6974,6 +6979,7 @@ packages:
 
   /estree-walker@2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+    dev: true
 
   /esutils@2.0.3:
     resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@@ -7439,22 +7445,6 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
-  /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
-    resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
-    peerDependencies:
-      focus-trap: ^7.0.0
-      vue: ^3.0.0
-    dependencies:
-      focus-trap: 7.2.0
-      vue: 3.2.45
-    dev: false
-
-  /focus-trap@7.2.0:
-    resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==}
-    dependencies:
-      tabbable: 6.1.1
-    dev: false
-
   /follow-redirects@1.15.2(debug@4.3.4):
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     engines: {node: '>=4.0'}
@@ -10360,6 +10350,7 @@ packages:
     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
     dependencies:
       sourcemap-codec: 1.4.8
+    dev: true
 
   /mailcheck@1.1.1:
     resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
@@ -13276,6 +13267,7 @@ packages:
   /sourcemap-codec@1.4.8:
     resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
     deprecated: Please use @jridgewell/sourcemap-codec instead
+    dev: true
 
   /sparkles@1.0.1:
     resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==}
@@ -13694,10 +13686,6 @@ packages:
     resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
     dev: true
 
-  /tabbable@6.1.1:
-    resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==}
-    dev: false
-
   /tapable@2.2.1:
     resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
     engines: {node: '>=6'}
@@ -14788,6 +14776,7 @@ packages:
       '@vue/runtime-dom': 3.2.45
       '@vue/server-renderer': 3.2.45(vue@3.2.45)
       '@vue/shared': 3.2.45
+    dev: true
 
   /vuedraggable@4.1.0(vue@3.2.45):
     resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}

From 5831f0a2c5ff69d5e51dac40749e5ca57d273b97 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 19:14:47 -0700
Subject: [PATCH 55/96] hotfix

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 80942ff54..61c247ab2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "calckey",
-	"version": "13.2.0-dev40",
+	"version": "13.2.0-dev41",
 	"codename": "aqua",
 	"repository": {
 		"type": "git",

From 9336b6dab02c597ff0e6b82f1495eb4198111f42 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 19:38:26 -0700
Subject: [PATCH 56/96] Revert "Merge pull request 'Add show more button to
 notifications' (#9942) from Freeplay/calckey:notifications into develop"

This reverts commit 8cb321b110dca7f8020f8a7a9af6a49308a0c036, reversing
changes made to 133391122b77c92bff6e0e94f40cfdea542c6f35.
---
 packages/client/src/components/MkCwButton.vue | 37 ----------
 .../client/src/components/MkNotification.vue  | 47 ++++---------
 .../src/components/MkShowMoreButton.vue       | 68 -------------------
 .../src/components/MkSubNoteContent.vue       | 56 +++++++++++++--
 .../client/src/components/MkUserPreview.vue   | 16 ++++-
 5 files changed, 79 insertions(+), 145 deletions(-)
 delete mode 100644 packages/client/src/components/MkShowMoreButton.vue

diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 1f6340510..01ab11351 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -77,42 +77,5 @@ defineExpose({
 		background: var(--cwFg) !important;
 		color: var(--cwBg) !important;
 	}
-
-	&.fade {
-		display: block;
-		position: absolute;
-		bottom: 0;
-		left: 0;
-		width: 100%;
-		z-index: 2;
-		> span {
-			display: inline-block;
-			background: var(--panel);
-			padding: 0.4em 1em;
-			font-size: 0.8em;
-			border-radius: 999px;
-			box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
-		}
-		&:hover, &:focus {
-			> span {
-				background: var(--panelHighlight);
-			}
-		}
-	}
-	&.showLess {
-		width: 100%;
-		margin-top: 1em;
-		position: sticky;
-		bottom: var(--stickyBottom);
-
-		> span {
-			display: inline-block;
-			background: var(--panel);
-			padding: 6px 10px;
-			font-size: 0.8em;
-			border-radius: 999px;
-			box-shadow: 0 0 7px 7px var(--bg);
-		}
-	}
 }
 </style>
diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue
index 2c7281dbe..c909873a5 100644
--- a/packages/client/src/components/MkNotification.vue
+++ b/packages/client/src/components/MkNotification.vue
@@ -89,7 +89,7 @@
 				/>
 			</div>
 		</div>
-		<div class="tail" :class="{ collapsed }">
+		<div class="tail">
 			<header>
 				<span v-if="notification.type === 'pollEnded'">{{
 					i18n.ts._notification.pollEnded
@@ -112,11 +112,11 @@
 				v-if="notification.type === 'reaction'"
 				class="text"
 				:to="notePage(notification.note)"
-				:title="summary"
+				:title="getNoteSummary(notification.note)"
 			>
 				<i class="ph-quotes ph-fill ph-lg"></i>
 				<Mfm
-					:text="summary"
+					:text="getNoteSummary(notification.note)"
 					:plain="true"
 					:nowrap="!full"
 					:custom-emojis="notification.note.emojis"
@@ -142,10 +142,10 @@
 				v-if="notification.type === 'reply'"
 				class="text"
 				:to="notePage(notification.note)"
-				:title="summary"
+				:title="getNoteSummary(notification.note)"
 			>
 				<Mfm
-					:text="summary"
+					:text="getNoteSummary(notification.note)"
 					:plain="true"
 					:nowrap="!full"
 					:custom-emojis="notification.note.emojis"
@@ -155,10 +155,10 @@
 				v-if="notification.type === 'mention'"
 				class="text"
 				:to="notePage(notification.note)"
-				:title="summary"
+				:title="getNoteSummary(notification.note)"
 			>
 				<Mfm
-					:text="summary"
+					:text="getNoteSummary(notification.note)"
 					:plain="true"
 					:nowrap="!full"
 					:custom-emojis="notification.note.emojis"
@@ -168,10 +168,10 @@
 				v-if="notification.type === 'quote'"
 				class="text"
 				:to="notePage(notification.note)"
-				:title="summary"
+				:title="getNoteSummary(notification.note)"
 			>
 				<Mfm
-					:text="summary"
+					:text="getNoteSummary(notification.note)"
 					:plain="true"
 					:nowrap="!full"
 					:custom-emojis="notification.note.emojis"
@@ -181,11 +181,11 @@
 				v-if="notification.type === 'pollVote'"
 				class="text"
 				:to="notePage(notification.note)"
-				:title="summary"
+				:title="getNoteSummary(notification.note)"
 			>
 				<i class="ph-quotes ph-fill ph-lg"></i>
 				<Mfm
-					:text="summary"
+					:text="getNoteSummary(notification.note)"
 					:plain="true"
 					:nowrap="!full"
 					:custom-emojis="notification.note.emojis"
@@ -196,11 +196,11 @@
 				v-if="notification.type === 'pollEnded'"
 				class="text"
 				:to="notePage(notification.note)"
-				:title="summary"
+				:title="getNoteSummary(notification.note)"
 			>
 				<i class="ph-quotes ph-fill ph-lg"></i>
 				<Mfm
-					:text="summary"
+					:text="getNoteSummary(notification.note)"
 					:plain="true"
 					:nowrap="!full"
 					:custom-emojis="notification.note.emojis"
@@ -264,7 +264,6 @@
 			<span v-if="notification.type === 'app'" class="text">
 				<Mfm :text="notification.body" :nowrap="!full" />
 			</span>
-			<xShowMoreButton v-if="isLong" v-model="collapsed"></xShowMoreButton>
 		</div>
 	</div>
 </template>
@@ -275,7 +274,6 @@ import * as misskey from "calckey-js";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
 import MkFollowButton from "@/components/MkFollowButton.vue";
 import XReactionTooltip from "@/components/MkReactionTooltip.vue";
-import XShowMoreButton from "./MkShowMoreButton.vue";
 import { getNoteSummary } from "@/scripts/get-note-summary";
 import { notePage } from "@/filters/note";
 import { userPage } from "@/filters/user";
@@ -301,19 +299,12 @@ const props = withDefaults(
 const elRef = ref<HTMLElement>(null);
 const reactionRef = ref(null);
 
-const summary = getNoteSummary(props.notification.note);
-
 const showEmojiReactions =
 	defaultStore.state.enableEmojiReactions ||
 	defaultStore.state.showEmojisInReactionNotifications;
 const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction)
 	? instance.defaultReaction
 	: "⭐";
-const isLong = (summary.split("\n").length > 3 || summary.length > 200);
-const collapsed = $ref(isLong);
-
-
-
 
 let readObserver: IntersectionObserver | undefined;
 let connection;
@@ -495,7 +486,6 @@ useTooltip(reactionRef, (showing) => {
 	}
 
 	> .tail {
-		position: relative;
 		flex: 1;
 		min-width: 0;
 
@@ -536,17 +526,6 @@ useTooltip(reactionRef, (showing) => {
 				margin-left: 4px;
 			}
 		}
-		&.collapsed > .text {
-			display: block;
-			position: relative;
-			max-height: calc(4em + 50px);
-			overflow: hidden;
-			mask: linear-gradient(black calc(100% - 64px), transparent);
-			-webkit-mask: linear-gradient(
-				black calc(100% - 64px),
-				transparent
-			);
-		}
 	}
 }
 </style>
diff --git a/packages/client/src/components/MkShowMoreButton.vue b/packages/client/src/components/MkShowMoreButton.vue
deleted file mode 100644
index 3516d6f43..000000000
--- a/packages/client/src/components/MkShowMoreButton.vue
+++ /dev/null
@@ -1,68 +0,0 @@
-<template>
-	<button
-		v-if="modelValue"
-		class="fade _button"
-		@click.stop="toggle"
-	>
-		<span>{{ i18n.ts.showMore }}</span>
-	</button>
-	<button
-		v-if="!modelValue"
-		class="showLess _button"
-		@click.stop="toggle"
-	>
-		<span>{{ i18n.ts.showLess }}</span>
-	</button>
-</template>
-<script lang="ts" setup>
-import { i18n } from "@/i18n";
-
-const props = defineProps<{
-	modelValue: boolean;
-}>();
-
-const emit = defineEmits<{
-	(ev: "update:modelValue", v: boolean): void;
-}>();
-
-const toggle = () => {
-	emit("update:modelValue", !props.modelValue);
-};
-</script>
-<style lang="scss" scoped>
-.fade {
-	display: block;
-	position: absolute;
-	bottom: 0;
-	left: 0;
-	width: 100%;
-	> span {
-		display: inline-block;
-		background: var(--panel);
-		padding: 0.4em 1em;
-		font-size: 0.8em;
-		border-radius: 999px;
-		box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
-	}
-	&:hover {
-		> span {
-			background: var(--panelHighlight);
-		}
-	}
-}
-.showLess {
-	width: 100%;
-	margin-top: 1em;
-	position: sticky;
-	bottom: var(--stickyBottom);
-
-	> span {
-		display: inline-block;
-		background: var(--panel);
-		padding: 6px 10px;
-		font-size: 0.8em;
-		border-radius: 999px;
-		box-shadow: 0 0 7px 7px var(--bg);
-	}
-}
-</style>
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 68439527a..94869402f 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -106,8 +106,21 @@
 					v-on:focus="cwButton?.focus()"
 				></div>
 			</div>
-			<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
-			<XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" />
+			<button
+				v-if="isLong && collapsed"
+				class="fade _button"
+				@click.stop="collapsed = false"
+			>
+				<span>{{ i18n.ts.showMore }}</span>
+			</button>
+			<button
+				v-if="isLong && !collapsed"
+				class="showLess _button"
+				@click.stop="collapsed = true"
+			>
+				<span>{{ i18n.ts.showLess }}</span>
+			</button>
+			<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
 		</div>
 	</div>
 </template>
@@ -120,7 +133,6 @@ import XNoteSimple from "@/components/MkNoteSimple.vue";
 import XMediaList from "@/components/MkMediaList.vue";
 import XPoll from "@/components/MkPoll.vue";
 import MkUrlPreview from "@/components/MkUrlPreview.vue";
-import XShowMoreButton from "./MkShowMoreButton.vue";
 import XCwButton from "@/components/MkCwButton.vue";
 import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
 import { i18n } from "@/i18n";
@@ -145,7 +157,6 @@ const isLong =
 	props.note.text != null &&
 	(props.note.text.split("\n").length > 9 || props.note.text.length > 500);
 const collapsed = $ref(props.note.cw == null && isLong);
-
 const urls = props.note.text
 	? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
 	: null;
@@ -274,6 +285,43 @@ function focusFooter(ev) {
 					top: 40px;
 				}
 			}
+
+			:deep(.fade) {
+				display: block;
+				position: absolute;
+				bottom: 0;
+				left: 0;
+				width: 100%;
+				> span {
+					display: inline-block;
+					background: var(--panel);
+					padding: 0.4em 1em;
+					font-size: 0.8em;
+					border-radius: 999px;
+					box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+				}
+				&:hover {
+					> span {
+						background: var(--panelHighlight);
+					}
+				}
+			}
+		}
+
+		:deep(.showLess) {
+			width: 100%;
+			margin-top: 1em;
+			position: sticky;
+			bottom: var(--stickyBottom);
+
+			> span {
+				display: inline-block;
+				background: var(--panel);
+				padding: 6px 10px;
+				font-size: 0.8em;
+				border-radius: 999px;
+				box-shadow: 0 0 7px 7px var(--bg);
+			}
 		}
 	}
 }
diff --git a/packages/client/src/components/MkUserPreview.vue b/packages/client/src/components/MkUserPreview.vue
index ddfd39f14..1e6db1442 100644
--- a/packages/client/src/components/MkUserPreview.vue
+++ b/packages/client/src/components/MkUserPreview.vue
@@ -55,7 +55,20 @@
 						:custom-emojis="user.emojis"
 					/>
 				</div>
-				<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
+				<button
+					v-if="isLong && collapsed"
+					class="fade _button"
+					@click.stop="collapsed = false"
+				>
+					<span>{{ i18n.ts.showMore }}</span>
+				</button>
+				<button
+					v-if="isLong && !collapsed"
+					class="showLess _button"
+					@click.stop="collapsed = true"
+				>
+					<span>{{ i18n.ts.showLess }}</span>
+				</button>
 				<div v-if="user.fields.length > 0" class="fields">
 					<dl
 						v-for="(field, i) in user.fields"
@@ -115,7 +128,6 @@ import * as Acct from "calckey-js/built/acct";
 import type * as misskey from "calckey-js";
 import MkFollowButton from "@/components/MkFollowButton.vue";
 import { userPage } from "@/filters/user";
-import XShowMoreButton from "./MkShowMoreButton.vue";
 import * as os from "@/os";
 import { $i } from "@/account";
 import { i18n } from "@/i18n";

From 9c40247df21b2a8c4daf25cc7b0735fca32e9062 Mon Sep 17 00:00:00 2001
From: Laker Turner <la@laker.gay>
Date: Sat, 29 Apr 2023 10:28:49 +0000
Subject: [PATCH 57/96] chore: Translated using Weblate (English)

Currently translated at 100.0% (1735 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/en/
---
 locales/en-US.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 2feb2cd94..f9d4d23f0 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1042,7 +1042,7 @@ moveFromLabel: "Account you're moving from:"
 moveFromDescription: "This will set an alias of your old account so that you can move\
   \ from that account to this current one. Do this BEFORE moving from your older account.\
   \ Please enter the tag of the account formatted like @person@instance.com"
-migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}?\
+migrationConfirm: "Are you absolutely sure you want to migrate your account to {account}?\
   \ Once you do this, you won't be able to reverse it, and you won't be able to use\
   \ your account normally again.\nAlso, please ensure that you've set this current\
   \ account as the account you're moving from."

From bcfadc086bd523c408da31c69db24c228b8d7d51 Mon Sep 17 00:00:00 2001
From: jolupa <jolupameister@gmail.com>
Date: Sat, 29 Apr 2023 11:53:12 +0000
Subject: [PATCH 58/96] chore: Translated using Weblate (Catalan)

Currently translated at 37.1% (644 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/ca/
---
 locales/ca-ES.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 24e4b6d7d..b226d641e 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -675,3 +675,48 @@ useGlobalSetting: Fes servir els ajustos globals
 useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del
   teu compte. Si es desactiva , es poden fer configuracions individuals.
 other: Altres
+menu: Menú
+addItem: Afegeix un element
+divider: Divisor
+relays: Relés
+addRelay: Afegeix un Relé
+inboxUrl: Adreça de la safata d'entrada
+addedRelays: Relés afegits
+serviceworkerInfo: Ha de estar activat per les notificacions push.
+poll: Enquesta
+deletedNote: Article eliminat
+disablePlayer: Tancar el reproductor de vídeo
+fileIdOrUrl: ID o adreça URL del fitxer
+behavior: Comportament
+regenerateLoginTokenDescription: Regenera el token que es fa servir de manera interna
+  durant l'inici de sessió. Normalment això no és necessari. Si es torna a genera
+  el token, es tancarà la sessió a tots els dispositius.
+setMultipleBySeparatingWithSpace: Separa diferents entrades amb espais.
+reportAbuseOf: Informa sobre {name}
+sample: Exemple
+abuseReports: Informes
+reportAbuse: Informe
+reporter: Informador
+reporterOrigin: Origen d'el informador
+forwardReport: Envia l'informe a una instancia remota
+abuseReported: El teu informe ha sigut enviat. Moltes gràcies.
+reporteeOrigin: Origen de l'informe
+send: Enviar
+abuseMarkAsResolved: Marcar l'informe com a resolt
+visibility: Visibilitat
+useCw: Amaga el contingut
+enablePlayer: Obre el reproductor de vídeo
+yourAccountSuspendedDescription: Aquest compte ha sigut suspesa per no seguir els
+  termes de servei del servidor o quelcom similar. Contacte amb l'administrador si
+  vols conèixer la raó amb més detall. Si us plau no facis un compte nou.
+invisibleNote: Article ocult
+enableInfiniteScroll: Carregar més de forma automàtica
+fillAbuseReportDescription: Si us plau omple els detalls sobre aquest informe. Si
+  es sobre un article en concret, si us plau inclou l'adreça URL.
+forwardReportIsAnonymous: Com a informador a l'instància remota no es mostrarà el
+  teu compte, si no un compte anònim.
+openInNewTab: Obrir en una pestanya nova
+openInSideView: Obrir a la vista lateral
+defaultNavigationBehaviour: Navegació per defecte
+editTheseSettingsMayBreakAccount: Si edites aquestes configuracions pots fer mal bé
+  el teu compte.

From b2dc43aa01d4b809d0a869b264b24557a719461f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ville=20Lepist=C3=B6?= <ville.lepisto@protonmail.com>
Date: Sat, 29 Apr 2023 08:52:45 +0000
Subject: [PATCH 59/96] chore: Translated using Weblate (Finnish)

Currently translated at 11.7% (204 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/fi/
---
 locales/fi.yml | 179 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 179 insertions(+)

diff --git a/locales/fi.yml b/locales/fi.yml
index c9e5e06e0..558d1afc2 100644
--- a/locales/fi.yml
+++ b/locales/fi.yml
@@ -41,3 +41,182 @@ favorite: Lisää kirjanmerkkeihin
 copyContent: Kopioi sisältö
 deleteAndEdit: Poista ja muokkaa
 copyLink: Kopioi linkki
+makeFollowManuallyApprove: Seuraajapyyntö vaatii hyväksymistä
+follow: Seuraa
+pinned: Kiinnitä profiiliin
+followRequestPending: Seuraajapyyntö odottaa
+you: Sinä
+unrenote: Peruuta buustaus
+reaction: Reaktiot
+reactionSettingDescription2: Vedä uudelleenjärjestelläksesi, napsauta poistaaksesi,
+  paina "+" lisätäksesi.
+attachCancel: Poista liite
+enterFileName: Anna tiedostonimi
+mute: Hiljennä
+unmute: Poista hiljennys
+headlineMisskey: Avoimen lähdekoodin, hajautettu sosiaalisen median alusta, joka on
+  ikuisesti ilmainen! 🚀
+monthAndDay: '{day}/{month}'
+deleteAndEditConfirm: Oletko varma, että haluat poistaa tämän lähetyksen ja muokata
+  sitä? Menetät kaikki reaktiot, buustaukset ja vastaukset lähetyksestäsi.
+addToList: Lisää listaan
+sendMessage: Lähetä viesti
+reply: Vastaa
+loadMore: Lataa enemmän
+showMore: Näytä enemmän
+receiveFollowRequest: Seuraajapyyntö vastaanotettu
+followRequestAccepted: Seuraajapyyntö hyväksytty
+mentions: Maininnat
+importAndExport: Tuo/Vie Tietosisältö
+import: Tuo
+export: Vie
+files: Tiedostot
+download: Lataa
+unfollowConfirm: Oletko varma, ettet halua seurata enää käyttäjää {name}?
+noLists: Sinulla ei ole listoja
+note: Lähetys
+notes: Lähetykset
+following: Seuraa
+createList: Luo lista
+manageLists: Hallitse listoja
+error: Virhe
+somethingHappened: On tapahtunut virhe
+retry: Yritä uudelleen
+pageLoadError: Virhe ladattaessa sivua.
+serverIsDead: Tämä palvelin ei vastaa. Yritä hetken kuluttua uudelleen.
+youShouldUpgradeClient: Nähdäksesi tämän sivun, virkistä päivittääksesi asiakasohjelmasi.
+privacy: Tietosuoja
+defaultNoteVisibility: Oletusnäkyvyys
+followRequest: Seuraajapyyntö
+followRequests: Seuraajapyynnöt
+unfollow: Poista seuraaminen
+enterEmoji: Syötä emoji
+renote: Buustaa
+renoted: Buustattu.
+cantRenote: Tätä lähetystä ei voi buustata.
+cantReRenote: Buustausta ei voi buustata.
+quote: Lainaus
+pinnedNote: Lukittu lähetys
+clickToShow: Napsauta nähdäksesi
+sensitive: Herkkää sisältöä (NSFW)
+add: Lisää
+enableEmojiReactions: Ota käyttöön emoji-reaktiot
+showEmojisInReactionNotifications: Näytä emojit reaktioilmoituksissa
+reactionSetting: Reaktiot näytettäväksi reaktiovalitsimessa
+rememberNoteVisibility: Muista lähetyksen näkyvyysasetukset
+markAsSensitive: Merkitse herkäksi sisällöksi (NSFW)
+unmarkAsSensitive: Poista merkintä herkkää sisältöä (NSFW)
+renoteMute: Hiljennä buustit
+renoteUnmute: Poista buustien hiljennys
+block: Estä
+unblock: Poista esto
+unsuspend: Poista keskeytys
+suspend: Keskeytys
+blockConfirm: Oletko varma, että haluat estää tämän tilin?
+unblockConfirm: Oletko varma, että haluat poistaa tämän tilin eston?
+selectAntenna: Valitse antenni
+selectWidget: Valitse vimpain
+editWidgets: Muokkaa vimpaimia
+editWidgetsExit: Valmis
+emoji: Emoji
+emojis: Emojit
+emojiName: Emojin nimi
+emojiUrl: Emojin URL-linkki
+cacheRemoteFiles: Taltioi etätiedostot välimuistiin
+flagAsBot: Merkitse tili botiksi
+flagAsBotDescription: Ota tämä vaihtoehto käyttöön, jos tätä tiliä ohjaa ohjelma.
+  Jos se on käytössä, se toimii lippuna muille kehittäjille, jotta estetään loputtomat
+  vuorovaikutusketjut muiden bottien kanssa ja säädetään Calckeyn sisäiset järjestelmät
+  käsittelemään tätä tiliä botina.
+flagAsCat: Oletko kissa? 🐱
+flagAsCatDescription: Saat kissan korvat ja puhut kuin kissa!
+flagSpeakAsCat: Puhu kuin kissa
+flagShowTimelineReplies: Näytä vastaukset aikajanalla
+addAccount: Lisää tili
+loginFailed: Kirjautuminen epäonnistui
+showOnRemote: Katsele etäinstanssilla
+general: Yleistä
+accountMoved: 'Käyttäjä on muuttanut uuteen tiliin:'
+wallpaper: Taustakuva
+setWallpaper: Aseta taustakuva
+searchWith: 'Etsi: {q}'
+youHaveNoLists: Sinulla ei ole listoja
+followConfirm: Oletko varma, että haluat seurata käyttäjää {name}?
+host: Isäntä
+selectUser: Valitse käyttäjä
+annotation: Kommentit
+registeredAt: Rekisteröity
+latestRequestReceivedAt: Viimeisin pyyntö vastaanotettu
+latestRequestSentAt: Viimeisin pyyntö lähetetty
+storageUsage: Tallennustilan käyttö
+charts: Kaaviot
+stopActivityDelivery: Lopeta toimintojen lähettäminen
+blockThisInstance: Estä tämä instanssi
+operations: Toiminnot
+metadata: Metatieto
+monitor: Seuranta
+jobQueue: Työjono
+cpuAndMemory: Prosessori ja muisti
+network: Verkko
+disk: Levy
+clearCachedFiles: Tyhjennä välimuisti
+clearCachedFilesConfirm: Oletko varma, että haluat tyhjentää kaikki välimuistiin tallennetut
+  etätiedostot?
+blockedInstances: Estetyt instanssit
+hiddenTags: Piilotetut asiatunnisteet
+mention: Maininta
+copyUsername: Kopioi käyttäjänimi
+searchUser: Etsi käyttäjää
+showLess: Sulje
+youGotNewFollower: seurasi sinua
+directNotes: Yksityisviestit
+driveFileDeleteConfirm: Oletko varma, että haluat poistaa tiedoston " {name}"? Lähetykset,
+  jotka sisältyvät tiedostoon, poistuvat myös.
+importRequested: Olet pyytänyt viemistä. Tämä voi viedä hetken.
+exportRequested: Olet pyytänyt tuomista. Tämä voi viedä hetken. Se lisätään asemaan
+  kun tuonti valmistuu.
+lists: Listat
+followers: Seuraajat
+followsYou: Seuraa sinua
+pageLoadErrorDescription: Tämä yleensä johtuu verkkovirheistä tai selaimen välimuistista.
+  Kokeile tyhjentämällä välimuisti ja yritä sitten hetken kuluttua uudelleen.
+enterListName: Anna listalle nimi
+withNFiles: '{n} tiedosto(t)'
+instanceInfo: Instanssin tiedot
+clearQueue: Tyhjennä jono
+suspendConfirm: Oletko varma, että haluat keskeyttää tämän tilin?
+unsuspendConfirm: Oletko varma, että haluat poistaa tämän tilin keskeytyksen?
+selectList: Valitse lista
+customEmojis: Kustomoitu Emoji
+addEmoji: Lisää
+settingGuide: Suositellut asetukset
+cacheRemoteFilesDescription: Kun tämä asetus ei ole käytössä, etätiedostot on ladattu
+  suoraan etäinstanssilta. Asetuksen poistaminen käytöstä vähentää tallennustilan
+  käyttöä, mutta lisää verkkoliikennettä kun pienoiskuvat eivät muodostu.
+flagSpeakAsCatDescription: Lähetyksesi nyanifioidaan, kun olet kissatilassa
+flagShowTimelineRepliesDescription: Näyttää käyttäjien vastaukset muiden käyttäjien
+  lähetyksiin aikajanalla, jos se on päällä.
+autoAcceptFollowed: Automaattisesti hyväksy seuraamispyynnöt käyttäjiltä, joita seuraat
+perHour: Tunnissa
+removeWallpaper: Poista taustakuva
+recipient: Vastaanottaja(t)
+federation: Federaatio
+software: Ohjelmisto
+proxyAccount: Proxy-tili
+proxyAccountDescription: Välitystili (Proxy-tili) on tili, joka toimii käyttäjien
+  etäseuraajana tietyin edellytyksin. Kun käyttäjä esimerkiksi lisää etäkäyttäjän
+  luetteloon, etäkäyttäjän toimintaa ei toimiteta instanssiin, jos yksikään paikallinen
+  käyttäjä ei seuraa kyseistä käyttäjää, joten välitystili seuraa sen sijaan.
+latestStatus: Viimeisin tila
+selectInstance: Valitse instanssi
+instances: Instanssit
+perDay: Päivässä
+version: Versio
+statistics: Tilastot
+clearQueueConfirmTitle: Oletko varma, että haluat tyhjentää jonon?
+introMisskey: Tervetuloa! Calckey on avoimen lähdekoodin, hajautettu sosiaalisen median
+  alusta, joka on ikuisesti ilmainen! 🚀
+clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jonossa, eivät
+  federoidu. Yleensä tätä toimintoa ei tarvita.
+blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
+  Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.

From 8db240756e996edcacb811102b6c8dd25c508179 Mon Sep 17 00:00:00 2001
From: Kainoa Kanter <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 08:11:18 +0000
Subject: [PATCH 60/96] chore: Translated using Weblate (Finnish)

Currently translated at 11.7% (204 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/fi/
---
 locales/fi.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/locales/fi.yml b/locales/fi.yml
index 558d1afc2..fd6c160e6 100644
--- a/locales/fi.yml
+++ b/locales/fi.yml
@@ -3,7 +3,7 @@ fetchingAsApObject: Hae Fedeversestä
 gotIt: Selvä!
 cancel: Peruuta
 enterUsername: Anna käyttäjänimi
-renotedBy: Buustannut {käyttäjä}
+renotedBy: Buustannut {user}
 noNotes: Ei lähetyksiä
 noNotifications: Ei ilmoituksia
 instance: Instanssi
@@ -220,3 +220,4 @@ clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jo
   federoidu. Yleensä tätä toimintoa ei tarvita.
 blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
   Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.
+_lang_: Suomi

From ce606601282afb2381c4b634aa595a379a1cff15 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 07:27:55 -0400
Subject: [PATCH 61/96] instance silence

---
 .../1682844825247-InstanceSilence.js          | 63 +++++++++++++++++++
 .../backend/src/misc/should-block-instance.ts | 17 +++++
 packages/backend/src/models/entities/meta.ts  |  5 ++
 .../src/models/repositories/instance.ts       |  5 +-
 .../src/models/schema/federation-instance.ts  |  5 ++
 .../src/server/api/endpoints/admin/meta.ts    | 11 ++++
 .../server/api/endpoints/admin/update-meta.ts | 16 +++++
 .../api/endpoints/federation/instances.ts     | 17 +++++
 .../backend/src/services/following/create.ts  |  7 ++-
 packages/backend/src/services/note/create.ts  | 21 ++++++-
 packages/calckey-js/src/api.types.ts          |  1 +
 .../client/src/pages/admin/instance-block.vue | 30 ++++++++-
 12 files changed, 188 insertions(+), 10 deletions(-)
 create mode 100644 packages/backend/migration/1682844825247-InstanceSilence.js

diff --git a/packages/backend/migration/1682844825247-InstanceSilence.js b/packages/backend/migration/1682844825247-InstanceSilence.js
new file mode 100644
index 000000000..5689c4f16
--- /dev/null
+++ b/packages/backend/migration/1682844825247-InstanceSilence.js
@@ -0,0 +1,63 @@
+export class InstanceSilence1682844825247 {
+    name = 'InstanceSilence1682844825247'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`);
+        await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`);
+        await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `);
+        await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
+        await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`);
+        await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`);
+        await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+}
diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts
index 6e4623242..66b5832c7 100644
--- a/packages/backend/src/misc/should-block-instance.ts
+++ b/packages/backend/src/misc/should-block-instance.ts
@@ -18,3 +18,20 @@ export async function shouldBlockInstance(
 		(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
 	);
 }
+
+/**
+ * Returns whether a specific host (punycoded) should be limited.
+ *
+ * @param host punycoded instance host
+ * @param meta a resolved Meta table
+ * @returns whether the given host should be limited
+ */
+export async function shouldSilenceInstance(
+	host: Instance["host"],
+	meta?: Meta,
+): Promise<boolean> {
+	const { silencedHosts } = meta ?? (await fetchMeta());
+	return silencedHosts.some(
+		(limitedHost) => host === limitedHost || host.endsWith(`.${limitedHost}`),
+	);
+}
diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts
index 2f77796c4..84f9af479 100644
--- a/packages/backend/src/models/entities/meta.ts
+++ b/packages/backend/src/models/entities/meta.ts
@@ -97,6 +97,11 @@ export class Meta {
 	})
 	public blockedHosts: string[];
 
+	@Column('varchar', {
+		length: 256, array: true, default: '{}',
+	})
+	public silencedHosts: string[];
+
 	@Column('boolean', {
 		default: false,
 	})
diff --git a/packages/backend/src/models/repositories/instance.ts b/packages/backend/src/models/repositories/instance.ts
index fb4498911..bae32b571 100644
--- a/packages/backend/src/models/repositories/instance.ts
+++ b/packages/backend/src/models/repositories/instance.ts
@@ -1,12 +1,10 @@
 import { db } from "@/db/postgre.js";
 import { Instance } from "@/models/entities/instance.js";
 import type { Packed } from "@/misc/schema.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { shouldBlockInstance } from "@/misc/should-block-instance.js";
+import { shouldBlockInstance, shouldSilenceInstance } from "@/misc/should-block-instance.js";
 
 export const InstanceRepository = db.getRepository(Instance).extend({
 	async pack(instance: Instance): Promise<Packed<"FederationInstance">> {
-		const meta = await fetchMeta();
 		return {
 			id: instance.id,
 			caughtAt: instance.caughtAt.toISOString(),
@@ -22,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
 			isNotResponding: instance.isNotResponding,
 			isSuspended: instance.isSuspended,
 			isBlocked: await shouldBlockInstance(instance.host),
+			isSilenced: await shouldSilenceInstance(instance.host),
 			softwareName: instance.softwareName,
 			softwareVersion: instance.softwareVersion,
 			openRegistrations: instance.openRegistrations,
diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/schema/federation-instance.ts
index ed3369bf1..f793d40f6 100644
--- a/packages/backend/src/models/schema/federation-instance.ts
+++ b/packages/backend/src/models/schema/federation-instance.ts
@@ -68,6 +68,11 @@ export const packedFederationInstanceSchema = {
 			optional: false,
 			nullable: false,
 		},
+		isSilenced: {
+			type: "boolean",
+			optional: false,
+			nullable: false,
+		},
 		softwareName: {
 			type: "string",
 			optional: false,
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index f0ac57892..89928af11 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -259,6 +259,16 @@ export const meta = {
 					nullable: false,
 				},
 			},
+			silencedHosts: {
+				type: "array",
+				optional: true,
+				nullable: false,
+				items: {
+					type: "string",
+					optional: false,
+					nullable: false,
+				},
+			},
 			allowedHosts: {
 				type: "array",
 				optional: true,
@@ -524,6 +534,7 @@ export default define(meta, paramDef, async (ps, me) => {
 		customSplashIcons: instance.customSplashIcons,
 		hiddenTags: instance.hiddenTags,
 		blockedHosts: instance.blockedHosts,
+		silencedHosts: instance.silencedHosts,
 		allowedHosts: instance.allowedHosts,
 		privateMode: instance.privateMode,
 		secureMode: instance.secureMode,
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index a23000732..7f92e5e29 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -61,6 +61,13 @@ export const paramDef = {
 				type: "string",
 			},
 		},
+		silencedHosts: {
+			type: "array",
+			nullable: true,
+			items: {
+				type: "string",
+			},
+		},
 		allowedHosts: {
 			type: "array",
 			nullable: true,
@@ -219,6 +226,15 @@ export default define(meta, paramDef, async (ps, me) => {
 		});
 	}
 
+	if (Array.isArray(ps.silencedHosts)) {
+		let lastValue = "";
+		set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
+			const lv = lastValue;
+			lastValue = h;
+			return h !== "" && h !== lv;
+		});
+	}
+
 	if (ps.themeColor !== undefined) {
 		set.themeColor = ps.themeColor;
 	}
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 8f6184b19..646f38282 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -34,6 +34,7 @@ export const paramDef = {
 		notResponding: { type: "boolean", nullable: true },
 		suspended: { type: "boolean", nullable: true },
 		federating: { type: "boolean", nullable: true },
+		silenced: { type: "boolean", nullable: true },
 		subscribing: { type: "boolean", nullable: true },
 		publishing: { type: "boolean", nullable: true },
 		limit: { type: "integer", minimum: 1, maximum: 100, default: 30 },
@@ -115,6 +116,22 @@ export default define(meta, paramDef, async (ps, me) => {
 		}
 	}
 
+	if (typeof ps.silenced === "boolean") {
+		const meta = await fetchMeta(true);
+		if (ps.silenced) {
+			if (meta.silencedHosts.length === 0) {
+				return [];
+			}
+			query.andWhere("instance.host IN (:...silences)", {
+				silences: meta.silencedHosts,
+			});
+		} else if (meta.silencedHosts.length > 0) {
+			query.andWhere("instance.host NOT IN (:...silences)", {
+				silences: meta.silencedHosts,
+			});
+		}
+	}
+
 	if (typeof ps.notResponding === "boolean") {
 		if (ps.notResponding) {
 			query.andWhere("instance.isNotResponding = TRUE");
diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts
index 61a8c6b26..cb5a888be 100644
--- a/packages/backend/src/services/following/create.ts
+++ b/packages/backend/src/services/following/create.ts
@@ -27,6 +27,7 @@ import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js
 import type { Packed } from "@/misc/schema.js";
 import { getActiveWebhooks } from "@/misc/webhook-cache.js";
 import { webhookDeliver } from "@/queue/index.js";
+import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
 
 const logger = new Logger("following/create");
 
@@ -227,12 +228,14 @@ export default async function (
 
 	// フォロー対象が鍵アカウントである or
 	// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
-	// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
+	// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
+	// The follower is remote, the followee is local, and the follower is in a silenced instance.
 	// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
 	if (
 		followee.isLocked ||
 		(followeeProfile.carefulBot && follower.isBot) ||
-		(Users.isLocalUser(follower) && Users.isRemoteUser(followee))
+		(Users.isLocalUser(follower) && Users.isRemoteUser(followee)) ||
+		(Users.isRemoteUser(follower) && Users.isLocalUser(followee) && await shouldSilenceInstance(follower.host))
 	) {
 		let autoAccept = false;
 
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index 5dd324d89..3bccf33f7 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -39,7 +39,7 @@ import {
 } from "@/models/index.js";
 import type { DriveFile } from "@/models/entities/drive-file.js";
 import type { App } from "@/models/entities/app.js";
-import { Not, In } from "typeorm";
+import { Not, In, IsNull } from "typeorm";
 import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
 import { genId } from "@/misc/gen-id.js";
 import {
@@ -66,6 +66,7 @@ import { Cache } from "@/misc/cache.js";
 import type { UserProfile } from "@/models/entities/user-profile.js";
 import { db } from "@/db/postgre.js";
 import { getActiveWebhooks } from "@/misc/webhook-cache.js";
+import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
 
 const mutedWordsCache = new Cache<
 	{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@@ -166,7 +167,8 @@ export default async (
 	data: Option,
 	silent = false,
 ) =>
-	new Promise<Note>(async (res, rej) => {
+// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
+new  Promise<Note>(async (res, rej) => {
 		// If you reply outside the channel, match the scope of the target.
 		// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
 		if (
@@ -203,6 +205,13 @@ export default async (
 			data.visibility = "home";
 		}
 
+		const inSilencedInstance = Users.isRemoteUser(user) && await shouldSilenceInstance(user.host);
+
+		// If the
+		if (data.visibility === "public" && inSilencedInstance) {
+			data.visibility = "home";
+		}
+
 		// Reject if the target of the renote is a public range other than "Home or Entire".
 		if (
 			data.renote &&
@@ -307,6 +316,14 @@ export default async (
 			}
 		}
 
+		// Remove from mention the local users who aren't following the remote user in the silenced instance.
+		if (inSilencedInstance) {
+			const relations = await Followings.findBy([
+				{ followeeId: user.id, followerHost: IsNull() }, // a local user following the silenced user
+			]).then(rels => rels.map(rel => rel.followerId));
+			mentionedUsers = mentionedUsers.filter(mentioned => relations.includes(mentioned.id));
+		}
+
 		const note = await insertNote(user, data, tags, emojis, mentionedUsers);
 
 		res(note);
diff --git a/packages/calckey-js/src/api.types.ts b/packages/calckey-js/src/api.types.ts
index bef00da4e..478b86721 100644
--- a/packages/calckey-js/src/api.types.ts
+++ b/packages/calckey-js/src/api.types.ts
@@ -55,6 +55,7 @@ export type Endpoints = {
 	"admin/get-table-stats": { req: TODO; res: TODO };
 	"admin/invite": { req: TODO; res: TODO };
 	"admin/logs": { req: TODO; res: TODO };
+	"admin/meta": { req: TODO; res: TODO };
 	"admin/reset-password": { req: TODO; res: TODO };
 	"admin/resolve-abuse-user-report": { req: TODO; res: TODO };
 	"admin/resync-chart": { req: TODO; res: TODO };
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
index 80231b11e..688578ff4 100644
--- a/packages/client/src/pages/admin/instance-block.vue
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -2,18 +2,25 @@
 	<MkStickyContainer>
 		<template #header
 			><MkPageHeader
+				v-model:tab="tab"
 				:actions="headerActions"
 				:tabs="headerTabs"
 				:display-back-button="true"
 		/></template>
 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
 			<FormSuspense :p="init">
-				<FormTextarea v-model="blockedHosts" class="_formBlock">
+				<FormTextarea v-if="tab === 'block'" v-model="blockedHosts" class="_formBlock">
 					<span>{{ i18n.ts.blockedInstances }}</span>
 					<template #caption>{{
 						i18n.ts.blockedInstancesDescription
 					}}</template>
 				</FormTextarea>
+				<FormTextarea v-else-if="tab === 'silence'" v-model="silencedHosts" class="_formBlock">
+					<span>{{ i18n.ts.silencedInstances }}</span>
+					<template #caption>{{
+						i18n.ts.silencedInstancesDescription
+					}}</template>
+				</FormTextarea>
 
 				<FormButton primary class="_formBlock" @click="save"
 					><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
@@ -35,15 +42,21 @@ import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 
 let blockedHosts: string = $ref("");
+let silencedHosts: string = $ref("");
+let tab = $ref("block");
 
 async function init() {
 	const meta = await os.api("admin/meta");
-	blockedHosts = meta.blockedHosts.join("\n");
+	if (meta) {
+		blockedHosts = meta.blockedHosts.join("\n");
+		silencedHosts = meta.silencedHosts.join("\n");
+	}
 }
 
 function save() {
 	os.apiWithDialog("admin/update-meta", {
 		blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [],
+		silencedHosts: silencedHosts.split("\n").map((h) => h.trim()) || [],
 	}).then(() => {
 		fetchInstance();
 	});
@@ -51,7 +64,18 @@ function save() {
 
 const headerActions = $computed(() => []);
 
-const headerTabs = $computed(() => []);
+const headerTabs = $computed(() => [
+	{
+		key: "block",
+		title: i18n.ts.block,
+		icon: "ph-prohibit ph-bold ph-lg",
+	},
+	{
+		key: "silence",
+		title: i18n.ts.silence,
+		icon: "ph-eye-slash ph-bold ph-lg",
+	},
+]);
 
 definePageMetadata({
 	title: i18n.ts.instanceBlocking,

From ada759a9e50756d7756c8ff364aaa25a2a412b84 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 07:58:03 -0400
Subject: [PATCH 62/96] rename and comment

---
 packages/backend/src/misc/should-block-instance.ts | 2 +-
 packages/backend/src/services/note/create.ts       | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts
index 66b5832c7..47f9200d4 100644
--- a/packages/backend/src/misc/should-block-instance.ts
+++ b/packages/backend/src/misc/should-block-instance.ts
@@ -32,6 +32,6 @@ export async function shouldSilenceInstance(
 ): Promise<boolean> {
 	const { silencedHosts } = meta ?? (await fetchMeta());
 	return silencedHosts.some(
-		(limitedHost) => host === limitedHost || host.endsWith(`.${limitedHost}`),
+		(silencedHost) => host === silencedHost || host.endsWith(`.${silencedHost}`),
 	);
 }
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index 3bccf33f7..bd3a0224a 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -207,7 +207,7 @@ new  Promise<Note>(async (res, rej) => {
 
 		const inSilencedInstance = Users.isRemoteUser(user) && await shouldSilenceInstance(user.host);
 
-		// If the
+		// Enforce home visibility if the user is in a silenced instance.
 		if (data.visibility === "public" && inSilencedInstance) {
 			data.visibility = "home";
 		}

From c35f03832d0c2ed6418f9a588cc120813784d192 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 08:08:45 -0400
Subject: [PATCH 63/96] format

---
 .../1682844825247-InstanceSilence.js          | 220 +++++++++++++-----
 .../backend/src/misc/should-block-instance.ts |   3 +-
 .../src/models/repositories/instance.ts       |   5 +-
 .../backend/src/services/following/create.ts  |   4 +-
 packages/backend/src/services/note/create.ts  |  13 +-
 .../client/src/pages/admin/instance-block.vue |  12 +-
 6 files changed, 188 insertions(+), 69 deletions(-)

diff --git a/packages/backend/migration/1682844825247-InstanceSilence.js b/packages/backend/migration/1682844825247-InstanceSilence.js
index 5689c4f16..4c05b349d 100644
--- a/packages/backend/migration/1682844825247-InstanceSilence.js
+++ b/packages/backend/migration/1682844825247-InstanceSilence.js
@@ -1,63 +1,165 @@
 export class InstanceSilence1682844825247 {
-    name = 'InstanceSilence1682844825247'
+	name = "InstanceSilence1682844825247";
 
-    async up(queryRunner) {
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`);
-        await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`);
-        await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `);
-        await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
+	async up(queryRunner) {
+		await queryRunner.query(
+			`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_renote_muting_createdAt"`,
+		);
+		await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
+		await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
+		await queryRunner.query(
+			`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `,
+		);
+		await queryRunner.query(
+			`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+		);
+	}
 
-    async down(queryRunner) {
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
-        await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`);
-        await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`);
-        await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
+	async down(queryRunner) {
+		await queryRunner.query(
+			`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`,
+		);
+		await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+		);
+	}
 }
diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts
index 47f9200d4..35ed30793 100644
--- a/packages/backend/src/misc/should-block-instance.ts
+++ b/packages/backend/src/misc/should-block-instance.ts
@@ -32,6 +32,7 @@ export async function shouldSilenceInstance(
 ): Promise<boolean> {
 	const { silencedHosts } = meta ?? (await fetchMeta());
 	return silencedHosts.some(
-		(silencedHost) => host === silencedHost || host.endsWith(`.${silencedHost}`),
+		(silencedHost) =>
+			host === silencedHost || host.endsWith(`.${silencedHost}`),
 	);
 }
diff --git a/packages/backend/src/models/repositories/instance.ts b/packages/backend/src/models/repositories/instance.ts
index bae32b571..667ec948d 100644
--- a/packages/backend/src/models/repositories/instance.ts
+++ b/packages/backend/src/models/repositories/instance.ts
@@ -1,7 +1,10 @@
 import { db } from "@/db/postgre.js";
 import { Instance } from "@/models/entities/instance.js";
 import type { Packed } from "@/misc/schema.js";
-import { shouldBlockInstance, shouldSilenceInstance } from "@/misc/should-block-instance.js";
+import {
+	shouldBlockInstance,
+	shouldSilenceInstance,
+} from "@/misc/should-block-instance.js";
 
 export const InstanceRepository = db.getRepository(Instance).extend({
 	async pack(instance: Instance): Promise<Packed<"FederationInstance">> {
diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts
index cb5a888be..c987a01e5 100644
--- a/packages/backend/src/services/following/create.ts
+++ b/packages/backend/src/services/following/create.ts
@@ -235,7 +235,9 @@ export default async function (
 		followee.isLocked ||
 		(followeeProfile.carefulBot && follower.isBot) ||
 		(Users.isLocalUser(follower) && Users.isRemoteUser(followee)) ||
-		(Users.isRemoteUser(follower) && Users.isLocalUser(followee) && await shouldSilenceInstance(follower.host))
+		(Users.isRemoteUser(follower) &&
+			Users.isLocalUser(followee) &&
+			(await shouldSilenceInstance(follower.host)))
 	) {
 		let autoAccept = false;
 
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index bd3a0224a..ad50bd97a 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -167,8 +167,8 @@ export default async (
 	data: Option,
 	silent = false,
 ) =>
-// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
-new  Promise<Note>(async (res, rej) => {
+	// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
+	new Promise<Note>(async (res, rej) => {
 		// If you reply outside the channel, match the scope of the target.
 		// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
 		if (
@@ -205,7 +205,8 @@ new  Promise<Note>(async (res, rej) => {
 			data.visibility = "home";
 		}
 
-		const inSilencedInstance = Users.isRemoteUser(user) && await shouldSilenceInstance(user.host);
+		const inSilencedInstance =
+			Users.isRemoteUser(user) && (await shouldSilenceInstance(user.host));
 
 		// Enforce home visibility if the user is in a silenced instance.
 		if (data.visibility === "public" && inSilencedInstance) {
@@ -320,8 +321,10 @@ new  Promise<Note>(async (res, rej) => {
 		if (inSilencedInstance) {
 			const relations = await Followings.findBy([
 				{ followeeId: user.id, followerHost: IsNull() }, // a local user following the silenced user
-			]).then(rels => rels.map(rel => rel.followerId));
-			mentionedUsers = mentionedUsers.filter(mentioned => relations.includes(mentioned.id));
+			]).then((rels) => rels.map((rel) => rel.followerId));
+			mentionedUsers = mentionedUsers.filter((mentioned) =>
+				relations.includes(mentioned.id),
+			);
 		}
 
 		const note = await insertNote(user, data, tags, emojis, mentionedUsers);
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
index 688578ff4..3a9a17fa0 100644
--- a/packages/client/src/pages/admin/instance-block.vue
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -9,13 +9,21 @@
 		/></template>
 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
 			<FormSuspense :p="init">
-				<FormTextarea v-if="tab === 'block'" v-model="blockedHosts" class="_formBlock">
+				<FormTextarea
+					v-if="tab === 'block'"
+					v-model="blockedHosts"
+					class="_formBlock"
+				>
 					<span>{{ i18n.ts.blockedInstances }}</span>
 					<template #caption>{{
 						i18n.ts.blockedInstancesDescription
 					}}</template>
 				</FormTextarea>
-				<FormTextarea v-else-if="tab === 'silence'" v-model="silencedHosts" class="_formBlock">
+				<FormTextarea
+					v-else-if="tab === 'silence'"
+					v-model="silencedHosts"
+					class="_formBlock"
+				>
 					<span>{{ i18n.ts.silencedInstances }}</span>
 					<template #caption>{{
 						i18n.ts.silencedInstancesDescription

From f2a8d1f680831db9d6ed23299c989b0bbbe2b356 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 09:01:42 -0400
Subject: [PATCH 64/96] add toggler

---
 locales/en-US.yml                             |  8 +++--
 locales/ja-JP.yml                             |  5 +++-
 .../server/api/endpoints/admin/update-meta.ts |  2 +-
 packages/client/src/pages/instance-info.vue   | 30 ++++++++++++++++++-
 4 files changed, 40 insertions(+), 5 deletions(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index f9d4d23f0..6c22e0066 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -197,6 +197,7 @@ perHour: "Per Hour"
 perDay: "Per Day"
 stopActivityDelivery: "Stop sending activities"
 blockThisInstance: "Block this instance"
+silenceThisInstance: "Silence this instance"
 operations: "Operations"
 software: "Software"
 version: "Version"
@@ -218,10 +219,13 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
 blockedInstances: "Blocked Instances"
 blockedInstancesDescription: "List the hostnames of the instances that you want to\
   \ block. Listed instances will no longer be able to communicate with this instance."
+silencedInstances: "Silenced Instances"
+silencedInstancesDescription: "List the hostnames of the instances that you want to\
+  \ silence. Accounts in the listed instances are treated as \"Silenced\", can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
 hiddenTags: "Hidden Hashtags"
 hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\
   \ to hide from trending and explore. Hidden hashtags are still discoverable via\
-  \ other means."
+  \ other means. Blocked instances are not affected even if listed here."
 muteAndBlock: "Mutes and Blocks"
 mutedUsers: "Muted users"
 blockedUsers: "Blocked users"
@@ -829,7 +833,7 @@ active: "Active"
 offline: "Offline"
 notRecommended: "Not recommended"
 botProtection: "Bot Protection"
-instanceBlocking: "Blocked Instances"
+instanceBlocking: "Blocked/Silenced Instances"
 selectAccount: "Select account"
 switchAccount: "Switch account"
 enabled: "Enabled"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 466212ba2..7db2a7c82 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -183,6 +183,7 @@ perHour: "1時間ごと"
 perDay: "1日ごと"
 stopActivityDelivery: "アクティビティの配送を停止"
 blockThisInstance: "このインスタンスをブロック"
+silenceThisInstance: "このインスタンスをサイレンス"
 operations: "操作"
 software: "ソフトウェア"
 version: "バージョン"
@@ -202,6 +203,8 @@ clearCachedFiles: "キャッシュをクリア"
 clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
 blockedInstances: "ブロックしたインスタンス"
 blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
+silencedInstances: "サイレンスしたインスタンス"
+silencedInstancesDescription: "サイレンスしたいインスタンスのホストを改行で区切って設定します。サイレンスされたインスタンスに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
 muteAndBlock: "ミュートとブロック"
 mutedUsers: "ミュートしたユーザー"
 blockedUsers: "ブロックしたユーザー"
@@ -768,7 +771,7 @@ active: "アクティブ"
 offline: "オフライン"
 notRecommended: "非推奨"
 botProtection: "Botプロテクション"
-instanceBlocking: "インスタンスブロック"
+instanceBlocking: "インスタンスブロック・サイレンス"
 selectAccount: "アカウントを選択"
 switchAccount: "アカウントを切り替え"
 enabled: "有効"
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 7f92e5e29..60a9e85a3 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -231,7 +231,7 @@ export default define(meta, paramDef, async (ps, me) => {
 		set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
 			const lv = lastValue;
 			lastValue = h;
-			return h !== "" && h !== lv;
+			return h !== "" && h !== lv && !set.blockedHosts?.includes(h);
 		});
 	}
 
diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
index 5c8147351..7aeb2cd1c 100644
--- a/packages/client/src/pages/instance-info.vue
+++ b/packages/client/src/pages/instance-info.vue
@@ -98,6 +98,14 @@
 									@update:modelValue="toggleBlock"
 									>{{ i18n.ts.blockThisInstance }}</FormSwitch
 								>
+								<FormSwitch
+									v-model="isSilenced"
+									class="_formBlock"
+									@update:modelValue="toggleSilence"
+									>{{
+										i18n.ts.silenceThisInstance
+									}}</FormSwitch
+								>
 							</FormSuspense>
 							<MkButton @click="refreshMetadata"
 								><i
@@ -354,9 +362,11 @@ import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
 
 type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
 	blockedHosts: string[];
+	silencedHosts: string[];
 };
 type AugmentedInstance = misskey.entities.Instance & {
 	isBlocked: boolean;
+	isSilenced: boolean;
 };
 
 const props = defineProps<{
@@ -373,6 +383,7 @@ let meta = $ref<AugmentedInstanceMetadata | null>(null);
 let instance = $ref<AugmentedInstance | null>(null);
 let suspended = $ref(false);
 let isBlocked = $ref(false);
+let isSilenced = $ref(false);
 let faviconUrl = $ref(null);
 
 const usersPagination = {
@@ -387,7 +398,7 @@ const usersPagination = {
 };
 
 async function init() {
-	meta = await os.api("admin/meta");
+	meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
 }
 
 async function fetch() {
@@ -396,6 +407,7 @@ async function fetch() {
 	})) as AugmentedInstance;
 	suspended = instance.isSuspended;
 	isBlocked = instance.isBlocked;
+	isSilenced = instance.isSilenced;
 	faviconUrl =
 		getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
 		getProxiedImageUrlNullable(instance.iconUrl, "preview");
@@ -417,6 +429,22 @@ async function toggleBlock() {
 	});
 }
 
+async function toggleSilence() {
+	if (meta == null) return;
+	if (!instance) {
+		throw new Error(`Instance info not loaded`);
+	}
+	let silencedHosts: string[];
+	if (isSilenced) {
+		silencedHosts = meta.silencedHosts.concat([instance.host]);
+	} else {
+		silencedHosts = meta.silencedHosts.filter((x) => x !== instance!.host);
+	}
+	await os.api("admin/update-meta", {
+		silencedHosts,
+	});
+}
+
 async function toggleSuspend(v) {
 	await os.api("admin/federation/update-instance", {
 		host: instance.host,

From cec5813ab2d555f5172e01d5a369b2ecadb15e1f Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 09:04:07 -0400
Subject: [PATCH 65/96] can overlap with blocked hosts

---
 packages/backend/src/server/api/endpoints/admin/update-meta.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 60a9e85a3..7f92e5e29 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -231,7 +231,7 @@ export default define(meta, paramDef, async (ps, me) => {
 		set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
 			const lv = lastValue;
 			lastValue = h;
-			return h !== "" && h !== lv && !set.blockedHosts?.includes(h);
+			return h !== "" && h !== lv;
 		});
 	}
 

From 426f47fdad70b24b00c35eaeeb6b455ce0084283 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 30 Apr 2023 23:18:39 +0900
Subject: [PATCH 66/96] add blockMath

---
 packages/client/src/pages/mfm-cheat-sheet.vue | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index 1946d17ce..a52cacdd4 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -137,6 +137,18 @@
 						</div>
 					</div>
 				</div>
+				<div class="section _block">
+					<div class="title">{{ i18n.ts._mfm.blockMath }}</div>
+					<div class="content">
+						<p>{{ i18n.ts._mfm.blockMathDescription }}</p>
+						<div class="preview">
+							<Mfm :text="preview_blockMath" />
+							<MkTextarea v-model="preview_blockMath"
+								><template #label>MFM</template></MkTextarea
+							>
+						</div>
+					</div>
+				</div>
 				<!-- deprecated
 		<div class="section _block">
 			<div class="title">{{ i18n.ts._mfm.search }}</div>
@@ -427,6 +439,7 @@ let preview_blockCode = $ref(
 	'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```'
 );
 let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
+let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
 let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
 let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`);
 let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");

From 0db337b977c234006ff6ed8e4919a70932baa8e0 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 30 Apr 2023 23:24:19 +0900
Subject: [PATCH 67/96] more search syntax (although it doesn't appear since
 the description of the search syntax is commented out)

---
 packages/client/src/pages/mfm-cheat-sheet.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index a52cacdd4..f531a1911 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -441,7 +441,7 @@ let preview_blockCode = $ref(
 let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
 let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
 let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
-let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`);
+let preview_search = $ref(`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`);
 let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");
 let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
 let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");

From 1d37e78ca2b09e38a48de686a06101d1825aa4a9 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 30 Apr 2023 23:28:17 +0900
Subject: [PATCH 68/96] use new styling method

---
 packages/client/src/pages/mfm-cheat-sheet.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index f531a1911..71eba67c5 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -2,7 +2,7 @@
 	<MkStickyContainer>
 		<template #header><MkPageHeader /></template>
 		<MkSpacer :content-max="800">
-			<div class="mwysmxbg">
+			<div :class="$style.root">
 				<div>{{ i18n.ts._mfm.intro }}</div>
 				<br />
 				<div class="section _block">
@@ -478,8 +478,8 @@ definePageMetadata({
 });
 </script>
 
-<style lang="scss" scoped>
-.mwysmxbg {
+<style lang="scss" module>
+.root {
 	background: var(--bg);
 
 	> .section {

From 9d193340350baf99ff9cf4507816699c045aa255 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 30 Apr 2023 23:49:08 +0900
Subject: [PATCH 69/96] blockMath is not necessarily multi-line (is this
 copy-pasted from blockCode?)

---
 locales/en-US.yml | 2 +-
 locales/ja-JP.yml | 2 +-
 locales/zh-CN.yml | 2 +-
 locales/zh-TW.yml | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index f9d4d23f0..e5377dc54 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1197,7 +1197,7 @@ _mfm:
   inlineMath: "Math (Inline)"
   inlineMathDescription: "Display math formulas (KaTeX) in-line"
   blockMath: "Math (Block)"
-  blockMathDescription: "Display multi-line math formulas (KaTeX) in a block"
+  blockMathDescription: "Display math formulas (KaTeX) in a block"
   quote: "Quote"
   quoteDescription: "Displays content as a quote."
   emoji: "Custom Emoji"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 466212ba2..90a775714 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1079,7 +1079,7 @@ _mfm:
   inlineMath: "数式(インライン)"
   inlineMathDescription: "数式(KaTeX)をインラインで表示します。"
   blockMath: "数式(ブロック)"
-  blockMathDescription: "複数行の数式(KaTeX)をブロックで表示します。"
+  blockMathDescription: "数式(KaTeX)をブロックで表示します。"
   quote: "引用"
   quoteDescription: "内容が引用であることを示せます。"
   emoji: "カスタム絵文字"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index c652b52b7..3e90e4fb7 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1011,7 +1011,7 @@ _mfm:
   inlineMath: "数学公式(内嵌)"
   inlineMathDescription: "显示内嵌的KaTex公式。"
   blockMath: "数学公式(块)"
-  blockMathDescription: "显示整块的多行KaTex数学公式。"
+  blockMathDescription: "显示整块的KaTex数学公式。"
   quote: "引用"
   quoteDescription: "可以用来表示引用的内容。"
   emoji: "自定义表情符号"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index eb640b7dd..d1826f772 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -1014,7 +1014,7 @@ _mfm:
   inlineMath: "數學公式(內嵌)"
   inlineMathDescription: "顯示內嵌的KaTex數學公式。"
   blockMath: "數學公式(方塊)"
-  blockMathDescription: "以區塊顯示複數行的KaTex數學式。"
+  blockMathDescription: "以區塊顯示KaTex數學式。"
   quote: "引用"
   quoteDescription: "可以用來表示引用的内容。"
   emoji: "自訂表情符號"

From 44f6c141242ef7ad1bf8f32bdfdc0d4382391adf Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 1 May 2023 00:02:16 +0900
Subject: [PATCH 70/96] KaTex -> KaTeX

---
 locales/zh-CN.yml | 4 ++--
 locales/zh-TW.yml | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 3e90e4fb7..645f11f56 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1009,9 +1009,9 @@ _mfm:
   blockCode: "代码(块)"
   blockCodeDescription: "语法高亮显示整块程序代码。"
   inlineMath: "数学公式(内嵌)"
-  inlineMathDescription: "显示内嵌的KaTex公式。"
+  inlineMathDescription: "显示内嵌的KaTeX公式。"
   blockMath: "数学公式(块)"
-  blockMathDescription: "显示整块的KaTex数学公式。"
+  blockMathDescription: "显示整块的KaTeX数学公式。"
   quote: "引用"
   quoteDescription: "可以用来表示引用的内容。"
   emoji: "自定义表情符号"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index d1826f772..c2dfd1ce0 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -1012,9 +1012,9 @@ _mfm:
   blockCode: "程式碼(區塊)"
   blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。"
   inlineMath: "數學公式(內嵌)"
-  inlineMathDescription: "顯示內嵌的KaTex數學公式。"
+  inlineMathDescription: "顯示內嵌的KaTeX數學公式。"
   blockMath: "數學公式(方塊)"
-  blockMathDescription: "以區塊顯示KaTex數學式。"
+  blockMathDescription: "以區塊顯示KaTeX數學式。"
   quote: "引用"
   quoteDescription: "可以用來表示引用的内容。"
   emoji: "自訂表情符號"

From f0b4e10735424be72772f33744664eead6d1e5da Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 11:39:01 -0400
Subject: [PATCH 71/96] use tab instead of page header

---
 locales/en-US.yml                             |  3 ++-
 locales/ja-JP.yml                             |  3 ++-
 .../client/src/pages/about.federation.vue     |  6 ++++--
 .../client/src/pages/admin/federation.vue     |  1 -
 .../client/src/pages/admin/instance-block.vue | 19 ++++++-------------
 5 files changed, 14 insertions(+), 18 deletions(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 6c22e0066..ec1bae34c 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -244,6 +244,7 @@ noCustomEmojis: "There are no emoji"
 noJobs: "There are no jobs"
 federating: "Federating"
 blocked: "Blocked"
+silenced: "Silenced"
 suspended: "Suspended"
 all: "All"
 subscribing: "Subscribing"
@@ -833,7 +834,7 @@ active: "Active"
 offline: "Offline"
 notRecommended: "Not recommended"
 botProtection: "Bot Protection"
-instanceBlocking: "Blocked/Silenced Instances"
+instanceBlocking: "Federation Block/Silence"
 selectAccount: "Select account"
 switchAccount: "Switch account"
 enabled: "Enabled"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 7db2a7c82..641aca938 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -223,6 +223,7 @@ noCustomEmojis: "絵文字はありません"
 noJobs: "ジョブはありません"
 federating: "連合中"
 blocked: "ブロック中"
+silenced: "サイレンス中"
 suspended: "配信停止"
 all: "全て"
 subscribing: "購読中"
@@ -771,7 +772,7 @@ active: "アクティブ"
 offline: "オフライン"
 notRecommended: "非推奨"
 botProtection: "Botプロテクション"
-instanceBlocking: "インスタンスブロック・サイレンス"
+instanceBlocking: "連合ブロック・サイレンス"
 selectAccount: "アカウントを選択"
 switchAccount: "アカウントを切り替え"
 enabled: "有効"
diff --git a/packages/client/src/pages/about.federation.vue b/packages/client/src/pages/about.federation.vue
index ac4d4c876..390f7b433 100644
--- a/packages/client/src/pages/about.federation.vue
+++ b/packages/client/src/pages/about.federation.vue
@@ -18,6 +18,7 @@
 					<option value="publishing">{{ i18n.ts.publishing }}</option>
 					<option value="suspended">{{ i18n.ts.suspended }}</option>
 					<option value="blocked">{{ i18n.ts.blocked }}</option>
+					<option value="silenced">{{ i18n.ts.silenced }}</option>
 					<option value="notResponding">
 						{{ i18n.ts.notResponding }}
 					</option>
@@ -105,13 +106,11 @@
 
 <script lang="ts" setup>
 import { computed } from "vue";
-import MkButton from "@/components/MkButton.vue";
 import MkInput from "@/components/form/input.vue";
 import MkSelect from "@/components/form/select.vue";
 import MkPagination from "@/components/MkPagination.vue";
 import MkInstanceCardMini from "@/components/MkInstanceCardMini.vue";
 import FormSplit from "@/components/form/split.vue";
-import * as os from "@/os";
 import { i18n } from "@/i18n";
 
 let host = $ref("");
@@ -133,6 +132,8 @@ const pagination = {
 			: state === "suspended"
 			? { suspended: true }
 			: state === "blocked"
+			? { silenced: true }
+			: state === "silenced"
 			? { blocked: true }
 			: state === "notResponding"
 			? { notResponding: true }
@@ -143,6 +144,7 @@ const pagination = {
 function getStatus(instance) {
 	if (instance.isSuspended) return "Suspended";
 	if (instance.isBlocked) return "Blocked";
+	if (instance.isSilenced) return "Silenced";
 	if (instance.isNotResponding) return "Error";
 	return "Alive";
 }
diff --git a/packages/client/src/pages/admin/federation.vue b/packages/client/src/pages/admin/federation.vue
index 325ba32be..d288a56bc 100644
--- a/packages/client/src/pages/admin/federation.vue
+++ b/packages/client/src/pages/admin/federation.vue
@@ -3,7 +3,6 @@
 		<MkStickyContainer>
 			<template #header
 				><MkPageHeader
-					v-model:tab="tab"
 					:actions="headerActions"
 					:tabs="headerTabs"
 					:display-back-button="true"
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
index 3a9a17fa0..f3c4f542c 100644
--- a/packages/client/src/pages/admin/instance-block.vue
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -2,12 +2,15 @@
 	<MkStickyContainer>
 		<template #header
 			><MkPageHeader
-				v-model:tab="tab"
 				:actions="headerActions"
 				:tabs="headerTabs"
 				:display-back-button="true"
 		/></template>
 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+			<MkTab v-model="tab" class="_formBlock">
+				<option value="block">{{ i18n.ts.blockedInstances }}</option>
+				<option value="silence">{{ i18n.ts.silencedInstances }}</option>
+			</MkTab>
 			<FormSuspense :p="init">
 				<FormTextarea
 					v-if="tab === 'block'"
@@ -44,6 +47,7 @@ import {} from "vue";
 import FormButton from "@/components/MkButton.vue";
 import FormTextarea from "@/components/form/textarea.vue";
 import FormSuspense from "@/components/form/suspense.vue";
+import MkTab from "@/components/MkTab.vue";
 import * as os from "@/os";
 import { fetchInstance } from "@/instance";
 import { i18n } from "@/i18n";
@@ -72,18 +76,7 @@ function save() {
 
 const headerActions = $computed(() => []);
 
-const headerTabs = $computed(() => [
-	{
-		key: "block",
-		title: i18n.ts.block,
-		icon: "ph-prohibit ph-bold ph-lg",
-	},
-	{
-		key: "silence",
-		title: i18n.ts.silence,
-		icon: "ph-eye-slash ph-bold ph-lg",
-	},
-]);
+const headerTabs = $computed(() => []);
 
 definePageMetadata({
 	title: i18n.ts.instanceBlocking,

From affeb55c0116104b7b6b9b8a2c3827825ad6929f Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 30 Apr 2023 11:58:36 -0400
Subject: [PATCH 72/96] Fix jumping to top of page when opening menu

I also thought this would maybe fix one of the focustrap errors in the console, but no.
---
 packages/client/src/components/MkMenu.vue | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index c71e3ac58..41611bf0b 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,6 +1,6 @@
 <template>
-	<FocusTrap v-bind:active="isActive">
-		<div tabindex="-1" v-focus>
+	<FocusTrap :active="false" ref="focusTrap">
+		<div tabindex="-1">
 			<div
 				ref="itemsEl"
 				class="rrevdjwt _popup _shadow"
@@ -206,6 +206,7 @@ import { i18n } from "@/i18n";
 import { FocusTrap } from 'focus-trap-vue';
 
 const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
+const focusTrap = ref();
 
 const props = defineProps<{
 	items: MenuItem[];
@@ -316,6 +317,8 @@ function focusDown() {
 }
 
 onMounted(() => {
+	focusTrap.value.activate();
+
 	if (props.viaKeyboard) {
 		nextTick(() => {
 			focusNext(itemsEl.children[0], true, false);

From 0cf2e71b2e361b050b2d16224ce18a5db99abc64 Mon Sep 17 00:00:00 2001
From: fruye <fruye@unix.dog>
Date: Sun, 30 Apr 2023 19:34:52 +0000
Subject: [PATCH 73/96] Use numeric ids everywhere in mastodon API (#9970)

Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9970
Co-authored-by: fruye <fruye@unix.dog>
Co-committed-by: fruye <fruye@unix.dog>
---
 packages/backend/src/server/api/index.ts      |   5 +-
 .../mastodon/ApiMastodonCompatibleService.ts  |  10 +-
 .../src/server/api/mastodon/converters.ts     |  44 +++++
 .../server/api/mastodon/endpoints/account.ts  | 163 ++++--------------
 .../server/api/mastodon/endpoints/filter.ts   |  20 ++-
 .../api/mastodon/endpoints/notifications.ts   |  17 +-
 .../server/api/mastodon/endpoints/search.ts   |  21 ++-
 .../server/api/mastodon/endpoints/status.ts   |  97 +++++++----
 .../server/api/mastodon/endpoints/timeline.ts |  73 +++++---
 9 files changed, 238 insertions(+), 212 deletions(-)
 create mode 100644 packages/backend/src/server/api/mastodon/converters.ts

diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts
index 06b3ea4ef..3568a27b2 100644
--- a/packages/backend/src/server/api/index.ts
+++ b/packages/backend/src/server/api/index.ts
@@ -29,6 +29,7 @@ import {
 	convertId,
 	IdConvertType as IdType,
 } from "../../../native-utils/built/index.js";
+import { convertAttachment } from "./mastodon/converters.js";
 
 // re-export native rust id conversion (function and enum)
 export { IdType, convertId };
@@ -93,7 +94,7 @@ mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
 			return;
 		}
 		const data = await client.uploadMedia(multipartData);
-		ctx.body = data.data;
+		ctx.body = convertAttachment(data.data as Entity.Attachment);
 	} catch (e: any) {
 		console.error(e);
 		ctx.status = 401;
@@ -112,7 +113,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
 			return;
 		}
 		const data = await client.uploadMedia(multipartData);
-		ctx.body = data.data;
+		ctx.body = convertAttachment(data.data as Entity.Attachment);
 	} catch (e: any) {
 		console.error(e);
 		ctx.status = 401;
diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts
index e8dfe5281..0c59b38f4 100644
--- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts
+++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts
@@ -8,6 +8,8 @@ import { apiTimelineMastodon } from "./endpoints/timeline.js";
 import { apiNotificationsMastodon } from "./endpoints/notifications.js";
 import { apiSearchMastodon } from "./endpoints/search.js";
 import { getInstance } from "./endpoints/meta.js";
+import { convertAnnouncement, convertFilter } from "./converters.js";
+import { convertId, IdType } from "../index.js";
 
 export function getClient(
 	BASE_URL: string,
@@ -68,7 +70,7 @@ export function apiMastodonCompatible(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = await client.getInstanceAnnouncements();
-			ctx.body = data.data;
+			ctx.body = data.data.map(announcement => convertAnnouncement(announcement));
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -83,7 +85,9 @@ export function apiMastodonCompatible(router: Router): void {
 			const accessTokens = ctx.request.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.dismissInstanceAnnouncement(ctx.params.id);
+				const data = await client.dismissInstanceAnnouncement(
+					convertId(ctx.params.id, IdType.CalckeyId),
+				);
 				ctx.body = data.data;
 			} catch (e: any) {
 				console.error(e);
@@ -100,7 +104,7 @@ export function apiMastodonCompatible(router: Router): void {
 		// displayed without being logged in
 		try {
 			const data = await client.getFilters();
-			ctx.body = data.data;
+			ctx.body = data.data.map(filter => convertFilter(filter));
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
new file mode 100644
index 000000000..d9a4f90ef
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -0,0 +1,44 @@
+import { Entity } from "@calckey/megalodon";
+import { convertId, IdType } from "../index.js";
+
+function simpleConvert(data: any) {
+	data.id = convertId(data.id, IdType.MastodonId);
+	return data;
+}
+
+export function convertAccount(account: Entity.Account) { return simpleConvert(account); }
+export function convertAnnouncement(announcement: Entity.Announcement) { return simpleConvert(announcement); }
+export function convertAttachment(attachment: Entity.Attachment) { return simpleConvert(attachment); }
+export function convertFilter(filter: Entity.Filter) { return simpleConvert(filter); }
+export function convertList(list: Entity.List) { return simpleConvert(list); }
+
+export function convertNotification(notification: Entity.Notification) {
+	notification.account = convertAccount(notification.account);
+	notification.id = convertId(notification.id, IdType.MastodonId);
+	if (notification.status)
+		notification.status = convertStatus(notification.status);
+	return notification;
+}
+
+export function convertPoll(poll: Entity.Poll) { return simpleConvert(poll); }
+export function convertRelationship(relationship: Entity.Relationship) { return simpleConvert(relationship); }
+
+export function convertStatus(status: Entity.Status) {
+	status.account = convertAccount(status.account);
+	status.id = convertId(status.id, IdType.MastodonId);
+	if (status.in_reply_to_account_id)
+		status.in_reply_to_account_id = convertId(status.in_reply_to_account_id, IdType.MastodonId);
+	if (status.in_reply_to_id)
+		status.in_reply_to_id = convertId(status.in_reply_to_id, IdType.MastodonId);
+	status.media_attachments = status.media_attachments.map(attachment => convertAttachment(attachment));
+	status.mentions = status.mentions.map(mention => ({
+		...mention,
+		id: convertId(mention.id, IdType.MastodonId),
+	}));
+	if (status.poll)
+		status.poll = convertPoll(status.poll);
+	if (status.reblog)
+		status.reblog = convertStatus(status.reblog);
+
+	return status;
+}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 749058193..2984c20e3 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -3,8 +3,9 @@ import { resolveUser } from "@/remote/resolve-user.js";
 import Router from "@koa/router";
 import { FindOptionsWhere, IsNull } from "typeorm";
 import { getClient } from "../ApiMastodonCompatibleService.js";
-import { argsToBools, limitToInt } from "./timeline.js";
+import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js";
 import { convertId, IdType } from "../../index.js";
+import { convertAccount, convertList, convertRelationship, convertStatus } from "../converters.js";
 
 const relationshipModel = {
 	id: "",
@@ -62,9 +63,7 @@ export function apiAccountMastodon(router: Router): void {
 			const data = await client.updateCredentials(
 				(ctx.request as any).body as any,
 			);
-			let resp = data.data;
-			resp.id = convertId(resp.id, IdType.MastodonId);
-			ctx.body = resp;
+			ctx.body = convertAccount(data.data);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -81,9 +80,7 @@ export function apiAccountMastodon(router: Router): void {
 				(ctx.request.query as any).acct,
 				"accounts",
 			);
-			let resp = data.data.accounts[0];
-			resp.id = convertId(resp.id, IdType.MastodonId);
-			ctx.body = resp;
+			ctx.body = convertAccount(data.data.accounts[0]);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -115,11 +112,7 @@ export function apiAccountMastodon(router: Router): void {
 			}
 
 			const data = await client.getRelationships(reqIds);
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
+			ctx.body = data.data.map(relationship => convertRelationship(relationship));
 		} catch (e: any) {
 			console.error(e);
 			let data = e.response.data;
@@ -136,9 +129,7 @@ export function apiAccountMastodon(router: Router): void {
 		try {
 			const calcId = convertId(ctx.params.id, IdType.CalckeyId);
 			const data = await client.getAccount(calcId);
-			let resp = data.data;
-			resp.id = convertId(resp.id, IdType.MastodonId);
-			ctx.body = resp;
+			ctx.body = convertAccount(data.data);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -155,27 +146,9 @@ export function apiAccountMastodon(router: Router): void {
 			try {
 				const data = await client.getAccountStatuses(
 					convertId(ctx.params.id, IdType.CalckeyId),
-					argsToBools(limitToInt(ctx.query as any)),
+					convertTimelinesArgsId(argsToBools(limitToInt(ctx.query as any))),
 				);
-				let resp = data.data;
-				for (let statIdx = 0; statIdx < resp.length; statIdx++) {
-					resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
-					resp[statIdx].in_reply_to_account_id = resp[statIdx]
-						.in_reply_to_account_id
-						? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
-						: null;
-					resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
-						? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
-						: null;
-					let mentions = resp[statIdx].mentions;
-					for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
-						resp[statIdx].mentions[mtnIdx].id = convertId(
-							mentions[mtnIdx].id,
-							IdType.MastodonId,
-						);
-					}
-				}
-				ctx.body = resp;
+				ctx.body = data.data.map(status => convertStatus(status));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -193,13 +166,9 @@ export function apiAccountMastodon(router: Router): void {
 			try {
 				const data = await client.getAccountFollowers(
 					convertId(ctx.params.id, IdType.CalckeyId),
-					limitToInt(ctx.query as any),
+					convertTimelinesArgsId(limitToInt(ctx.query as any)),
 				);
-				let resp = data.data;
-				for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-					resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-				}
-				ctx.body = resp;
+				ctx.body = data.data.map(account => convertAccount(account));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -217,13 +186,9 @@ export function apiAccountMastodon(router: Router): void {
 			try {
 				const data = await client.getAccountFollowing(
 					convertId(ctx.params.id, IdType.CalckeyId),
-					limitToInt(ctx.query as any),
+					convertTimelinesArgsId(limitToInt(ctx.query as any)),
 				);
-				let resp = data.data;
-				for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-					resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-				}
-				ctx.body = resp;
+				ctx.body = data.data.map(account => convertAccount(account));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -239,8 +204,10 @@ export function apiAccountMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.getAccountLists(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.getAccountLists(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = data.data.map(list => convertList(list));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -259,9 +226,8 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.followAccount(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let acct = data.data;
+				let acct = convertRelationship(data.data);
 				acct.following = true;
-				acct.id = convertId(acct.id, IdType.MastodonId);
 				ctx.body = acct;
 			} catch (e: any) {
 				console.error(e);
@@ -281,8 +247,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.unfollowAccount(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let acct = data.data;
-				acct.id = convertId(acct.id, IdType.MastodonId);
+				let acct = convertRelationship(data.data);
 				acct.following = false;
 				ctx.body = acct;
 			} catch (e: any) {
@@ -303,9 +268,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.blockAccount(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -324,9 +287,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.unblockAccount(
 					convertId(ctx.params.id, IdType.MastodonId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -346,9 +307,7 @@ export function apiAccountMastodon(router: Router): void {
 					convertId(ctx.params.id, IdType.CalckeyId),
 					(ctx.request as any).body as any,
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -367,9 +326,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.unmuteAccount(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -384,27 +341,9 @@ export function apiAccountMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = (await client.getBookmarks(
-				limitToInt(ctx.query as any),
-			)) as any;
-			let resp = data.data;
-			for (let statIdx = 0; statIdx < resp.length; statIdx++) {
-				resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
-				resp[statIdx].in_reply_to_account_id = resp[statIdx]
-					.in_reply_to_account_id
-					? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
-					: null;
-				resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
-					? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
-					: null;
-				let mentions = resp[statIdx].mentions;
-				for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
-					resp[statIdx].mentions[mtnIdx].id = convertId(
-						mentions[mtnIdx].id,
-						IdType.MastodonId,
-					);
-				}
-			}
-			ctx.body = resp;
+				convertTimelinesArgsId(limitToInt(ctx.query as any)),
+			));
+			ctx.body = data.data.map(status => convertStatus(status));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -417,26 +356,8 @@ export function apiAccountMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getFavourites(limitToInt(ctx.query as any));
-			let resp = data.data;
-			for (let statIdx = 0; statIdx < resp.length; statIdx++) {
-				resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
-				resp[statIdx].in_reply_to_account_id = resp[statIdx]
-					.in_reply_to_account_id
-					? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
-					: null;
-				resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
-					? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
-					: null;
-				let mentions = resp[statIdx].mentions;
-				for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
-					resp[statIdx].mentions[mtnIdx].id = convertId(
-						mentions[mtnIdx].id,
-						IdType.MastodonId,
-					);
-				}
-			}
-			ctx.body = resp;
+			const data = await client.getFavourites(convertTimelinesArgsId(limitToInt(ctx.query as any)));
+			ctx.body = data.data.map(status => convertStatus(status));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -449,12 +370,8 @@ export function apiAccountMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getMutes(limitToInt(ctx.query as any));
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
+			const data = await client.getMutes(convertTimelinesArgsId(limitToInt(ctx.query as any)));
+			ctx.body = data.data.map(account => convertAccount(account));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -467,12 +384,8 @@ export function apiAccountMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getBlocks(limitToInt(ctx.query as any));
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
+			const data = await client.getBlocks(convertTimelinesArgsId(limitToInt(ctx.query as any)));
+			ctx.body = data.data.map(account => convertAccount(account));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -488,11 +401,7 @@ export function apiAccountMastodon(router: Router): void {
 			const data = await client.getFollowRequests(
 				((ctx.query as any) || { limit: 20 }).limit,
 			);
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
+			ctx.body = data.data.map(account => convertAccount(account));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -510,9 +419,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.acceptFollowRequest(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -531,9 +438,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.rejectFollowRequest(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
index d21bc1d33..7343fc337 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
@@ -1,6 +1,8 @@
 import megalodon, { MegalodonInterface } from "@calckey/megalodon";
 import Router from "@koa/router";
 import { getClient } from "../ApiMastodonCompatibleService.js";
+import { IdType, convertId } from "../../index.js";
+import { convertFilter } from "../converters.js";
 
 export function apiFilterMastodon(router: Router): void {
 	router.get("/v1/filters", async (ctx) => {
@@ -10,7 +12,7 @@ export function apiFilterMastodon(router: Router): void {
 		const body: any = ctx.request.body;
 		try {
 			const data = await client.getFilters();
-			ctx.body = data.data;
+			ctx.body = data.data.map(filter => convertFilter(filter));
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -24,8 +26,10 @@ export function apiFilterMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const data = await client.getFilter(ctx.params.id);
-			ctx.body = data.data;
+			const data = await client.getFilter(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
+			ctx.body = convertFilter(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -40,7 +44,7 @@ export function apiFilterMastodon(router: Router): void {
 		const body: any = ctx.request.body;
 		try {
 			const data = await client.createFilter(body.phrase, body.context, body);
-			ctx.body = data.data;
+			ctx.body = convertFilter(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -55,11 +59,11 @@ export function apiFilterMastodon(router: Router): void {
 		const body: any = ctx.request.body;
 		try {
 			const data = await client.updateFilter(
-				ctx.params.id,
+				convertId(ctx.params.id, IdType.CalckeyId),
 				body.phrase,
 				body.context,
 			);
-			ctx.body = data.data;
+			ctx.body = convertFilter(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -73,7 +77,9 @@ export function apiFilterMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const data = await client.deleteFilter(ctx.params.id);
+			const data = await client.deleteFilter(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
 			ctx.body = data.data;
 		} catch (e: any) {
 			console.error(e);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
index 8508f1d48..868377b78 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
@@ -1,8 +1,10 @@
 import megalodon, { MegalodonInterface } from "@calckey/megalodon";
 import Router from "@koa/router";
 import { koaBody } from "koa-body";
+import { convertId, IdType } from "../../index.js";
 import { getClient } from "../ApiMastodonCompatibleService.js";
-import { toTextWithReaction } from "./timeline.js";
+import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js";
+import { convertNotification } from "../converters.js";
 function toLimitToInt(q: any) {
 	if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
 	return q;
@@ -15,9 +17,10 @@ export function apiNotificationsMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const data = await client.getNotifications(toLimitToInt(ctx.query));
+			const data = await client.getNotifications(convertTimelinesArgsId(toLimitToInt(ctx.query)));
 			const notfs = data.data;
 			const ret = notfs.map((n) => {
+				n = convertNotification(n);
 				if (n.type !== "follow" && n.type !== "follow_request") {
 					if (n.type === "reaction") n.type = "favourite";
 					n.status = toTextWithReaction(
@@ -43,8 +46,10 @@ export function apiNotificationsMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const dataRaw = await client.getNotification(ctx.params.id);
-			const data = dataRaw.data;
+			const dataRaw = await client.getNotification(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
+			const data = convertNotification(dataRaw.data);
 			if (data.type !== "follow" && data.type !== "follow_request") {
 				if (data.type === "reaction") data.type = "favourite";
 				ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0];
@@ -79,7 +84,9 @@ export function apiNotificationsMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const data = await client.dismissNotification(ctx.params.id);
+			const data = await client.dismissNotification(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
 			ctx.body = data.data;
 		} catch (e: any) {
 			console.error(e);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts
index e4990811a..98349cfd2 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/search.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts
@@ -3,7 +3,8 @@ import Router from "@koa/router";
 import { getClient } from "../ApiMastodonCompatibleService.js";
 import axios from "axios";
 import { Converter } from "@calckey/megalodon";
-import { limitToInt } from "./timeline.js";
+import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
+import { convertAccount, convertStatus } from "../converters.js";
 
 export function apiSearchMastodon(router: Router): void {
 	router.get("/v1/search", async (ctx) => {
@@ -12,7 +13,7 @@ export function apiSearchMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const query: any = limitToInt(ctx.query);
+			const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
 			const type = query.type || "";
 			const data = await client.search(query.q, type, query);
 			ctx.body = data.data;
@@ -27,18 +28,18 @@ export function apiSearchMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const query: any = limitToInt(ctx.query);
+			const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
 			const type = query.type;
 			if (type) {
 				const data = await client.search(query.q, type, query);
-				ctx.body = data.data;
+				ctx.body = data.data.accounts.map(account => convertAccount(account));
 			} else {
 				const acct = await client.search(query.q, "accounts", query);
 				const stat = await client.search(query.q, "statuses", query);
 				const tags = await client.search(query.q, "hashtags", query);
 				ctx.body = {
-					accounts: acct.data.accounts,
-					statuses: stat.data.statuses,
+					accounts: acct.data.accounts.map(account => convertAccount(account)),
+					statuses: stat.data.statuses.map(status => convertStatus(status)),
 					hashtags: tags.data.hashtags,
 				};
 			}
@@ -57,7 +58,7 @@ export function apiSearchMastodon(router: Router): void {
 				ctx.request.hostname,
 				accessTokens,
 			);
-			ctx.body = data;
+			ctx.body = data.map(status => convertStatus(status));
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -69,12 +70,16 @@ export function apiSearchMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		try {
 			const query: any = ctx.query;
-			const data = await getFeaturedUser(
+			let data = await getFeaturedUser(
 				BASE_URL,
 				ctx.request.hostname,
 				accessTokens,
 				query.limit || 20,
 			);
+			data = data.map(suggestion => {
+				suggestion.account = convertAccount(suggestion.account);
+				return suggestion;
+			});
 			console.log(data);
 			ctx.body = data;
 		} catch (e: any) {
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index f7589569c..27b30d113 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -4,7 +4,9 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
 import axios from "axios";
 import querystring from "node:querystring";
 import qs from "qs";
-import { limitToInt } from "./timeline.js";
+import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
+import { convertId, IdType } from "../../index.js";
+import { convertAccount, convertAttachment, convertPoll, convertStatus } from "../converters.js";
 
 function normalizeQuery(data: any) {
 	const str = querystring.stringify(data);
@@ -18,6 +20,8 @@ export function apiStatusMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			let body: any = ctx.request.body;
+			if (body.in_reply_to_id)
+				body.in_reply_to_id = convertId(body.in_reply_to_id, IdType.CalckeyId);
 			if (
 				(!body.poll && body["poll[options][]"]) ||
 				(!body.media_ids && body["media_ids[]"])
@@ -54,7 +58,7 @@ export function apiStatusMastodon(router: Router): void {
 			body.sensitive =
 				typeof sensitive === "string" ? sensitive === "true" : sensitive;
 			const data = await client.postStatus(text, body);
-			ctx.body = data.data;
+			ctx.body = convertStatus(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -66,8 +70,10 @@ export function apiStatusMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getStatus(ctx.params.id);
-			ctx.body = data.data;
+			const data = await client.getStatus(
+				convertId(ctx.params.id, IdType.CalckeyId),
+			);
+			ctx.body = convertStatus(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -79,7 +85,9 @@ export function apiStatusMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.deleteStatus(ctx.params.id);
+			const data = await client.deleteStatus(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
 			ctx.body = data.data;
 		} catch (e: any) {
 			console.error(e.response.data, request.params.id);
@@ -100,10 +108,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const id = ctx.params.id;
+				const id = convertId(ctx.params.id, IdType.CalckeyId);
 				const data = await client.getStatusContext(
 					id,
-					limitToInt(ctx.query as any),
+					convertTimelinesArgsId(limitToInt(ctx.query as any)),
 				);
 				const status = await client.getStatus(id);
 				let reqInstance = axios.create({
@@ -126,6 +134,8 @@ export function apiStatusMastodon(router: Router): void {
 						text,
 					),
 				);
+				data.data.ancestors = data.data.ancestors.map(status => convertStatus(status));
+				data.data.descendants = data.data.descendants.map(status => convertStatus(status));
 				ctx.body = data.data;
 			} catch (e: any) {
 				console.error(e);
@@ -141,8 +151,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.getStatusRebloggedBy(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.getStatusRebloggedBy(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = data.data.map(account => convertAccount(account));
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -165,11 +177,11 @@ export function apiStatusMastodon(router: Router): void {
 			const react = await getFirstReaction(BASE_URL, accessTokens);
 			try {
 				const a = (await client.createEmojiReaction(
-					ctx.params.id,
+					convertId(ctx.params.id, IdType.CalckeyId),
 					react,
 				)) as any;
 				//const data = await client.favouriteStatus(ctx.params.id) as any;
-				ctx.body = a.data;
+				ctx.body = convertStatus(a.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -186,8 +198,11 @@ export function apiStatusMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			const react = await getFirstReaction(BASE_URL, accessTokens);
 			try {
-				const data = await client.deleteEmojiReaction(ctx.params.id, react);
-				ctx.body = data.data;
+				const data = await client.deleteEmojiReaction(
+					convertId(ctx.params.id, IdType.CalckeyId),
+					react
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -203,8 +218,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.reblogStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.reblogStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -220,8 +237,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.unreblogStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.unreblogStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -237,8 +256,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.bookmarkStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.bookmarkStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -254,8 +275,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = (await client.unbookmarkStatus(ctx.params.id)) as any;
-				ctx.body = data.data;
+				const data = await client.unbookmarkStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -271,8 +294,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.pinStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.pinStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -288,8 +313,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.unpinStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.unpinStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -302,8 +329,10 @@ export function apiStatusMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getMedia(ctx.params.id);
-			ctx.body = data.data;
+			const data = await client.getMedia(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
+			ctx.body = convertAttachment(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -316,10 +345,10 @@ export function apiStatusMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = await client.updateMedia(
-				ctx.params.id,
+				convertId(ctx.params.id, IdType.CalckeyId),
 				ctx.request.body as any,
 			);
-			ctx.body = data.data;
+			ctx.body = convertAttachment(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -331,8 +360,10 @@ export function apiStatusMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getPoll(ctx.params.id);
-			ctx.body = data.data;
+			const data = await client.getPoll(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
+			ctx.body = convertPoll(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -347,10 +378,10 @@ export function apiStatusMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.votePoll(
-					ctx.params.id,
+					convertId(ctx.params.id, IdType.CalckeyId),
 					(ctx.request.body as any).choices,
 				);
-				ctx.body = data.data;
+				ctx.body = convertPoll(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
index 57e5d9bb0..268c6a161 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
@@ -4,6 +4,8 @@ import { getClient } from "../ApiMastodonCompatibleService.js";
 import { statusModel } from "./status.js";
 import Autolinker from "autolinker";
 import { ParsedUrlQuery } from "querystring";
+import { convertAccount, convertList, convertStatus } from "../converters.js";
+import { convertId, IdType } from "../../index.js";
 
 export function limitToInt(q: ParsedUrlQuery) {
 	let object: any = q;
@@ -29,6 +31,16 @@ export function argsToBools(q: ParsedUrlQuery) {
 	return q;
 }
 
+export function convertTimelinesArgsId(q: ParsedUrlQuery) {
+	if (typeof q.min_id === "string")
+		q.min_id = convertId(q.min_id, IdType.CalckeyId);
+	if (typeof q.max_id === "string")
+		q.max_id = convertId(q.max_id, IdType.CalckeyId);
+	if (typeof q.since_id === "string")
+		q.since_id = convertId(q.since_id, IdType.CalckeyId);
+	return q;
+}
+
 export function toTextWithReaction(status: Entity.Status[], host: string) {
 	return status.map((t) => {
 		if (!t) return statusModel(null, null, [], "no content");
@@ -97,9 +109,10 @@ export function apiTimelineMastodon(router: Router): void {
 		try {
 			const query: any = ctx.query;
 			const data = query.local
-				? await client.getLocalTimeline(argsToBools(limitToInt(query)))
-				: await client.getPublicTimeline(argsToBools(limitToInt(query)));
-			ctx.body = toTextWithReaction(data.data, ctx.hostname);
+				? await client.getLocalTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))))
+				: await client.getPublicTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))));
+			let resp = data.data.map(status => convertStatus(status));
+			ctx.body = toTextWithReaction(resp, ctx.hostname);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -116,9 +129,10 @@ export function apiTimelineMastodon(router: Router): void {
 			try {
 				const data = await client.getTagTimeline(
 					ctx.params.hashtag,
-					argsToBools(limitToInt(ctx.query)),
+					convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))),
 				);
-				ctx.body = toTextWithReaction(data.data, ctx.hostname);
+				let resp = data.data.map(status => convertStatus(status));
+				ctx.body = toTextWithReaction(resp, ctx.hostname);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -132,8 +146,9 @@ export function apiTimelineMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getHomeTimeline(limitToInt(ctx.query));
-			ctx.body = toTextWithReaction(data.data, ctx.hostname);
+			const data = await client.getHomeTimeline(convertTimelinesArgsId(limitToInt(ctx.query)));
+			let resp = data.data.map(status => convertStatus(status));
+			ctx.body = toTextWithReaction(resp, ctx.hostname);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -149,10 +164,11 @@ export function apiTimelineMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.getListTimeline(
-					ctx.params.listId,
-					limitToInt(ctx.query),
+					convertId(ctx.params.listId, IdType.CalckeyId),
+					convertTimelinesArgsId(limitToInt(ctx.query)),
 				);
-				ctx.body = toTextWithReaction(data.data, ctx.hostname);
+				let resp = data.data.map(status => convertStatus(status));
+				ctx.body = toTextWithReaction(resp, ctx.hostname);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -166,7 +182,7 @@ export function apiTimelineMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getConversationTimeline(limitToInt(ctx.query));
+			const data = await client.getConversationTimeline(convertTimelinesArgsId(limitToInt(ctx.query)));
 			ctx.body = data.data;
 		} catch (e: any) {
 			console.error(e);
@@ -181,7 +197,7 @@ export function apiTimelineMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = await client.getLists();
-			ctx.body = data.data;
+			ctx.body = data.data.map(list => convertList(list));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -196,8 +212,10 @@ export function apiTimelineMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.getList(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.getList(
+					convertId(ctx.params.id, IdType.CalckeyId),
+				);
+				ctx.body = convertList(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -212,7 +230,7 @@ export function apiTimelineMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = await client.createList((ctx.request.body as any).title);
-			ctx.body = data.data;
+			ctx.body = convertList(data.data);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -227,8 +245,11 @@ export function apiTimelineMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.updateList(ctx.params.id, (ctx.request.body as any).title);
-				ctx.body = data.data;
+				const data = await client.updateList(
+					convertId(ctx.params.id, IdType.CalckeyId),
+					(ctx.request.body as any).title
+				);
+				ctx.body = convertList(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -244,7 +265,9 @@ export function apiTimelineMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.deleteList(ctx.params.id);
+				const data = await client.deleteList(
+					convertId(ctx.params.id, IdType.CalckeyId),
+				);
 				ctx.body = data.data;
 			} catch (e: any) {
 				console.error(e);
@@ -262,10 +285,10 @@ export function apiTimelineMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.getAccountsInList(
-					ctx.params.id,
-					ctx.query as any,
+					convertId(ctx.params.id, IdType.CalckeyId),
+					convertTimelinesArgsId(ctx.query as any),
 				);
-				ctx.body = data.data;
+				ctx.body = data.data.map(account => convertAccount(account));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -282,8 +305,8 @@ export function apiTimelineMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.addAccountsToList(
-					ctx.params.id,
-					(ctx.query as any).account_ids,
+					convertId(ctx.params.id, IdType.CalckeyId),
+					(ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)),
 				);
 				ctx.body = data.data;
 			} catch (e: any) {
@@ -302,8 +325,8 @@ export function apiTimelineMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.deleteAccountsFromList(
-					ctx.params.id,
-					(ctx.query as any).account_ids,
+					convertId(ctx.params.id, IdType.CalckeyId),
+					(ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)),
 				);
 				ctx.body = data.data;
 			} catch (e: any) {

From 6f9a9dbfa13ca5aba0a2aacd6f8ef1b449919667 Mon Sep 17 00:00:00 2001
From: waon <waonme@gmail.com>
Date: Sun, 30 Apr 2023 07:55:50 +0000
Subject: [PATCH 74/96] chore: Translated using Weblate (Japanese)

Currently translated at 99.3% (1724 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/ja/
---
 locales/ja-JP.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 466212ba2..c0bb851a8 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1120,6 +1120,7 @@ _mfm:
   rotateDescription: "指定した角度で回転させます。"
   plain: "プレーン"
   plainDescription: "内側の構文を全て無効にします。"
+  position: 位置
 _instanceTicker:
   none: "表示しない"
   remote: "リモートユーザーに表示"
@@ -1128,7 +1129,7 @@ _serverDisconnectedBehavior:
   reload: "自動でリロード"
   dialog: "ダイアログで警告"
   quiet: "控えめに警告"
-  nothing: "何も起こらない"
+  nothing: "何もしない"
 _channel:
   create: "チャンネルを作成"
   edit: "チャンネルを編集"

From 3191853c32af7fcb7e44ceda260caff65e45c398 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 13:13:43 -0700
Subject: [PATCH 75/96] chore: up browser-image-resizer

---
 packages/client/package.json |   2 +-
 pnpm-lock.yaml               | 165 ++++++++++++++++++++++++++++++++---
 2 files changed, 154 insertions(+), 13 deletions(-)

diff --git a/packages/client/package.json b/packages/client/package.json
index 49c175b15..173585503 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -32,7 +32,7 @@
 		"autosize": "5.0.2",
 		"blurhash": "1.1.5",
 		"broadcast-channel": "4.19.1",
-		"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git",
+		"browser-image-resizer": "github:misskey-dev/browser-image-resizer",
 		"calckey-js": "workspace:*",
 		"chart.js": "4.1.1",
 		"chartjs-adapter-date-fns": "2.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 60e42e11a..77b0e0d4c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,7 +24,7 @@ importers:
         version: 7.2.0
       focus-trap-vue:
         specifier: ^4.0.1
-        version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
+        version: 4.0.1(focus-trap@7.2.0)(vue@3.2.47)
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
@@ -726,8 +726,8 @@ importers:
         specifier: 4.19.1
         version: 4.19.1
       browser-image-resizer:
-        specifier: https://github.com/misskey-dev/browser-image-resizer.git
-        version: github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c
+        specifier: github:misskey-dev/browser-image-resizer
+        version: github.com/misskey-dev/browser-image-resizer/56f504427ad7f6500e141a6d9f3aee42023d7f3e
       calckey-js:
         specifier: workspace:*
         version: link:../calckey-js
@@ -1071,6 +1071,11 @@ packages:
     resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
     engines: {node: '>=6.9.0'}
 
+  /@babel/helper-string-parser@7.21.5:
+    resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
+    engines: {node: '>=6.9.0'}
+    dev: false
+
   /@babel/helper-validator-identifier@7.19.1:
     resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
     engines: {node: '>=6.9.0'}
@@ -1114,6 +1119,14 @@ packages:
       '@babel/types': 7.21.4
     dev: true
 
+  /@babel/parser@7.21.5:
+    resolution: {integrity: sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+    dependencies:
+      '@babel/types': 7.21.5
+    dev: false
+
   /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.4):
     resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
     peerDependencies:
@@ -1300,6 +1313,15 @@ packages:
       to-fast-properties: 2.0.0
     dev: true
 
+  /@babel/types@7.21.5:
+    resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-string-parser': 7.21.5
+      '@babel/helper-validator-identifier': 7.19.1
+      to-fast-properties: 2.0.0
+    dev: false
+
   /@bcoe/v8-coverage@0.2.3:
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
     dev: true
@@ -3809,12 +3831,30 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       source-map: 0.6.1
+    dev: true
+
+  /@vue/compiler-core@3.2.47:
+    resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==}
+    dependencies:
+      '@babel/parser': 7.21.5
+      '@vue/shared': 3.2.47
+      estree-walker: 2.0.2
+      source-map: 0.6.1
+    dev: false
 
   /@vue/compiler-dom@3.2.45:
     resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
     dependencies:
       '@vue/compiler-core': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
+
+  /@vue/compiler-dom@3.2.47:
+    resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
+    dependencies:
+      '@vue/compiler-core': 3.2.47
+      '@vue/shared': 3.2.47
+    dev: false
 
   /@vue/compiler-sfc@2.7.14:
     resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@@ -3837,12 +3877,36 @@ packages:
       magic-string: 0.25.9
       postcss: 8.4.21
       source-map: 0.6.1
+    dev: true
+
+  /@vue/compiler-sfc@3.2.47:
+    resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
+    dependencies:
+      '@babel/parser': 7.21.5
+      '@vue/compiler-core': 3.2.47
+      '@vue/compiler-dom': 3.2.47
+      '@vue/compiler-ssr': 3.2.47
+      '@vue/reactivity-transform': 3.2.47
+      '@vue/shared': 3.2.47
+      estree-walker: 2.0.2
+      magic-string: 0.25.9
+      postcss: 8.4.23
+      source-map: 0.6.1
+    dev: false
 
   /@vue/compiler-ssr@3.2.45:
     resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
     dependencies:
       '@vue/compiler-dom': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
+
+  /@vue/compiler-ssr@3.2.47:
+    resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
+    dependencies:
+      '@vue/compiler-dom': 3.2.47
+      '@vue/shared': 3.2.47
+    dev: false
 
   /@vue/reactivity-transform@3.2.45:
     resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@@ -3852,17 +3916,43 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       magic-string: 0.25.9
+    dev: true
+
+  /@vue/reactivity-transform@3.2.47:
+    resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==}
+    dependencies:
+      '@babel/parser': 7.21.5
+      '@vue/compiler-core': 3.2.47
+      '@vue/shared': 3.2.47
+      estree-walker: 2.0.2
+      magic-string: 0.25.9
+    dev: false
 
   /@vue/reactivity@3.2.45:
     resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
     dependencies:
       '@vue/shared': 3.2.45
+    dev: true
+
+  /@vue/reactivity@3.2.47:
+    resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==}
+    dependencies:
+      '@vue/shared': 3.2.47
+    dev: false
 
   /@vue/runtime-core@3.2.45:
     resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
     dependencies:
       '@vue/reactivity': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
+
+  /@vue/runtime-core@3.2.47:
+    resolution: {integrity: sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==}
+    dependencies:
+      '@vue/reactivity': 3.2.47
+      '@vue/shared': 3.2.47
+    dev: false
 
   /@vue/runtime-dom@3.2.45:
     resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@@ -3870,6 +3960,15 @@ packages:
       '@vue/runtime-core': 3.2.45
       '@vue/shared': 3.2.45
       csstype: 2.6.21
+    dev: true
+
+  /@vue/runtime-dom@3.2.47:
+    resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==}
+    dependencies:
+      '@vue/runtime-core': 3.2.47
+      '@vue/shared': 3.2.47
+      csstype: 2.6.21
+    dev: false
 
   /@vue/server-renderer@3.2.45(vue@3.2.45):
     resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@@ -3879,9 +3978,25 @@ packages:
       '@vue/compiler-ssr': 3.2.45
       '@vue/shared': 3.2.45
       vue: 3.2.45
+    dev: true
+
+  /@vue/server-renderer@3.2.47(vue@3.2.47):
+    resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==}
+    peerDependencies:
+      vue: 3.2.47
+    dependencies:
+      '@vue/compiler-ssr': 3.2.47
+      '@vue/shared': 3.2.47
+      vue: 3.2.47
+    dev: false
 
   /@vue/shared@3.2.45:
     resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
+    dev: true
+
+  /@vue/shared@3.2.47:
+    resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==}
+    dev: false
 
   /@webassemblyjs/ast@1.11.1:
     resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@@ -7439,14 +7554,14 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
-  /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
+  /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.47):
     resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
     peerDependencies:
       focus-trap: ^7.0.0
       vue: ^3.0.0
     dependencies:
       focus-trap: 7.2.0
-      vue: 3.2.45
+      vue: 3.2.47
     dev: false
 
   /focus-trap@7.2.0:
@@ -7606,7 +7721,7 @@ packages:
     resolution: {integrity: sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==}
     engines: {node: '>= 0.10'}
     dependencies:
-      graceful-fs: 4.2.11
+      graceful-fs: 4.2.10
       through2: 2.0.5
     dev: true
 
@@ -9695,7 +9810,7 @@ packages:
   /jsonfile@4.0.0:
     resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
     optionalDependencies:
-      graceful-fs: 4.2.11
+      graceful-fs: 4.2.10
 
   /jsonfile@5.0.0:
     resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==}
@@ -9709,7 +9824,7 @@ packages:
     dependencies:
       universalify: 2.0.0
     optionalDependencies:
-      graceful-fs: 4.2.11
+      graceful-fs: 4.2.10
     dev: true
 
   /jsonld@6.0.0:
@@ -10832,6 +10947,12 @@ packages:
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
+  /nanoid@3.3.6:
+    resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+    dev: false
+
   /nanomatch@1.2.13:
     resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
     engines: {node: '>=0.10.0'}
@@ -11953,6 +12074,15 @@ packages:
       picocolors: 1.0.0
       source-map-js: 1.0.2
 
+  /postcss@8.4.23:
+    resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==}
+    engines: {node: ^10 || ^12 || >=14}
+    dependencies:
+      nanoid: 3.3.6
+      picocolors: 1.0.0
+      source-map-js: 1.0.2
+    dev: false
+
   /postgres-array@2.0.0:
     resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
     engines: {node: '>=4'}
@@ -14675,7 +14805,7 @@ packages:
     dependencies:
       append-buffer: 1.0.2
       convert-source-map: 1.9.0
-      graceful-fs: 4.2.11
+      graceful-fs: 4.2.10
       normalize-path: 2.1.1
       now-and-later: 2.0.1
       remove-bom-buffer: 3.0.0
@@ -14788,6 +14918,17 @@ packages:
       '@vue/runtime-dom': 3.2.45
       '@vue/server-renderer': 3.2.45(vue@3.2.45)
       '@vue/shared': 3.2.45
+    dev: true
+
+  /vue@3.2.47:
+    resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==}
+    dependencies:
+      '@vue/compiler-dom': 3.2.47
+      '@vue/compiler-sfc': 3.2.47
+      '@vue/runtime-dom': 3.2.47
+      '@vue/server-renderer': 3.2.47(vue@3.2.47)
+      '@vue/shared': 3.2.47
+    dev: false
 
   /vuedraggable@4.1.0(vue@3.2.45):
     resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
@@ -15402,10 +15543,10 @@ packages:
     resolution: {integrity: sha512-+MLeeUcLTlnzVo5xDn9+LVN9oX4esvgZ7qfZczBN+YVUvZBafIrPPVyG2WdjMWU2Qkb2ZAh2M8lpqf1wIoGqJQ==}
     dev: false
 
-  github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c:
-    resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/0380d12c8e736788ea7f4e6e985175521ea7b23c}
+  github.com/misskey-dev/browser-image-resizer/56f504427ad7f6500e141a6d9f3aee42023d7f3e:
+    resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/56f504427ad7f6500e141a6d9f3aee42023d7f3e}
     name: browser-image-resizer
-    version: 2.2.1-misskey.3
+    version: 2.2.1-misskey.4
     dev: true
 
   github.com/sampotts/plyr/d434c9af16e641400aaee93188594208d88f2658:

From 1eb19b698847cf7e88fe15291bec982e997089d1 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 13:26:51 -0700
Subject: [PATCH 76/96] fix(ap): Use unique identifier for each follow request

Closes #9677

Co-authored-by: GitHub <hutchisr>
---
 packages/backend/src/server/activitypub.ts    | 47 ++++++++++++++++++-
 .../src/services/following/requests/create.ts |  3 +-
 2 files changed, 47 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index 29ac726ef..d47e6f300 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -10,7 +10,7 @@ import { renderPerson } from "@/remote/activitypub/renderer/person.js";
 import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
 import { inbox as processInbox } from "@/queue/index.js";
 import { isSelfHost, toPuny } from "@/misc/convert-host.js";
-import { Notes, Users, Emojis, NoteReactions } from "@/models/index.js";
+import { Notes, Users, Emojis, NoteReactions, FollowRequests } from "@/models/index.js";
 import type { ILocalUser, User } from "@/models/entities/user.js";
 import { renderLike } from "@/remote/activitypub/renderer/like.js";
 import { getUserKeypair } from "@/misc/keypair-store.js";
@@ -330,7 +330,7 @@ router.get("/likes/:like", async (ctx) => {
 });
 
 // follow
-router.get("/follows/:follower/:followee", async (ctx) => {
+router.get("/follows/:follower/:followee", async (ctx: Router.RouterContext) => {
 	const verify = await checkFetch(ctx.req);
 	if (verify !== 200) {
 		ctx.status = verify;
@@ -365,4 +365,47 @@ router.get("/follows/:follower/:followee", async (ctx) => {
 	setResponseType(ctx);
 });
 
+// follow request
+router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
+	const verify = await checkFetch(ctx.req);
+	if (verify !== 200) {
+		ctx.status = verify;
+		return;
+	}
+
+	const followRequest = await FollowRequests.findOneBy({
+		id: ctx.params.followRequestId,
+	});
+
+	if (followRequest == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const [follower, followee] = await Promise.all([
+		Users.findOneBy({
+			id: followRequest.followerId,
+			host: IsNull(),
+		}),
+		Users.findOneBy({
+			id: followRequest.followeeId,
+			host: Not(IsNull()),
+		}),
+	]);
+
+	if (follower == null || followee == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
+	} else {
+		ctx.set("Cache-Control", "public, max-age=180");
+	}
+	ctx.body = renderActivity(renderFollow(follower, followee));
+	setResponseType(ctx);
+});
+
 export default router;
diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts
index 8b2e86ab5..27f9144d0 100644
--- a/packages/backend/src/services/following/requests/create.ts
+++ b/packages/backend/src/services/following/requests/create.ts
@@ -6,6 +6,7 @@ import type { User } from "@/models/entities/user.js";
 import { Blockings, FollowRequests, Users } from "@/models/index.js";
 import { genId } from "@/misc/gen-id.js";
 import { createNotification } from "../../create-notification.js";
+import config from "@/config/index.js";
 
 export default async function (
 	follower: {
@@ -79,7 +80,7 @@ export default async function (
 	}
 
 	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		const content = renderActivity(renderFollow(follower, followee));
+		const content = renderActivity(renderFollow(follower, followee, requestId ?? `${config.url}/follows/${followRequest.id}`));
 		deliver(follower, content, followee.inbox);
 	}
 }

From 1fb5635a4d299da01d2b44df2f5193c637e26e99 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 13:27:27 -0700
Subject: [PATCH 77/96] chore: format

---
 packages/client/src/components/MkCwButton.vue | 10 +++---
 .../client/src/components/MkEmojiPicker.vue   |  2 +-
 .../client/src/components/MkLaunchPad.vue     |  3 +-
 .../client/src/components/MkMenu.child.vue    | 18 +++++-----
 packages/client/src/components/MkMenu.vue     | 35 +++++++++++++------
 packages/client/src/components/MkModal.vue    |  9 +++--
 .../client/src/components/MkModalWindow.vue   |  2 +-
 packages/client/src/components/MkNote.vue     |  2 +-
 .../src/components/MkSubNoteContent.vue       | 31 +++++++++++-----
 .../client/src/components/MkSuperMenu.vue     |  3 +-
 .../client/src/components/MkUserPreview.vue   | 17 +++++----
 .../src/components/global/RouterView.vue      |  2 +-
 packages/client/src/pages/admin/_header_.vue  |  4 ++-
 .../src/pages/admin/overview.moderators.vue   |  7 +++-
 packages/client/src/pages/mfm-cheat-sheet.vue | 16 ++++++---
 packages/client/src/ui/_common_/navbar.vue    |  2 +-
 16 files changed, 110 insertions(+), 53 deletions(-)

diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 1f6340510..5e59853b6 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -28,7 +28,7 @@ const emit = defineEmits<{
 	(ev: "update:modelValue", v: boolean): void;
 }>();
 
-const el = ref<HTMLElement>(); 
+const el = ref<HTMLElement>();
 
 const label = computed(() => {
 	return concat([
@@ -52,7 +52,7 @@ function focus() {
 }
 
 defineExpose({
-	focus
+	focus,
 });
 </script>
 
@@ -73,7 +73,8 @@ defineExpose({
 			}
 		}
 	}
-	&:hover > span, &:focus > span {
+	&:hover > span,
+	&:focus > span {
 		background: var(--cwFg) !important;
 		color: var(--cwBg) !important;
 	}
@@ -93,7 +94,8 @@ defineExpose({
 			border-radius: 999px;
 			box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 		}
-		&:hover, &:focus {
+		&:hover,
+		&:focus {
 			> span {
 				background: var(--panelHighlight);
 			}
diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index 88d207bab..3dd54ed84 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -174,7 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
 import { emojiCategories, instance } from "@/instance";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
-import { FocusTrap } from 'focus-trap-vue';
+import { FocusTrap } from "focus-trap-vue";
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index 759c215f7..b1f42ec76 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -139,7 +139,8 @@ function close() {
 			height: 100px;
 			border-radius: 10px;
 
-			&:hover, &:focus-visible {
+			&:hover,
+			&:focus-visible {
 				color: var(--accent);
 				background: var(--accentedBg);
 				text-decoration: none;
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index e5ca9e4ee..83ae6b5a1 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,14 +1,14 @@
 <template>
 	<div ref="el" class="sfhdhdhr" tabindex="-1">
-			<MkMenu
-				ref="menu"
-				:items="items"
-				:align="align"
-				:width="width"
-				:as-drawer="false"
-				@close="onChildClosed"
-			/>
-		</div>
+		<MkMenu
+			ref="menu"
+			:items="items"
+			:align="align"
+			:width="width"
+			:as-drawer="false"
+			@close="onChildClosed"
+		/>
+	</div>
 </template>
 
 <script lang="ts" setup>
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 41611bf0b..e976eee4d 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -14,7 +14,9 @@
 				<template v-for="(item, i) in items2">
 					<div v-if="item === null" class="divider"></div>
 					<span v-else-if="item.type === 'label'" class="label item">
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 					</span>
 					<span
 						v-else-if="item.type === 'pending'"
@@ -48,7 +50,9 @@
 							class="avatar"
 							disableLink
 						/>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 						<span v-if="item.indicate" class="indicator"
 							><i class="ph-circle ph-fill"></i
 						></span>
@@ -75,7 +79,9 @@
 								:class="icon"
 							></i>
 						</span>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 						<span v-if="item.indicate" class="indicator"
 							><i class="ph-circle ph-fill"></i
 						></span>
@@ -89,9 +95,11 @@
 						@mouseenter.passive="onItemMouseEnter(item)"
 						@mouseleave.passive="onItemMouseLeave(item)"
 					>
-						<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
+						<MkAvatar
 							:user="item.user"
-						/>
+							class="avatar"
+							disableLink
+						/><MkUserName :user="item.user" />
 						<span v-if="item.indicate" class="indicator"
 							><i class="ph-circle ph-fill"></i
 						></span>
@@ -129,9 +137,13 @@
 								:class="icon"
 							></i>
 						</span>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 						<span class="caret"
-							><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
+							><i
+								class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"
+							></i
 						></span>
 					</button>
 					<button
@@ -161,7 +173,9 @@
 							class="avatar"
 							disableLink
 						/>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 						<span v-if="item.indicate" class="indicator"
 							><i class="ph-circle ph-fill"></i
 						></span>
@@ -203,7 +217,7 @@ import FormSwitch from "@/components/form/switch.vue";
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
-import { FocusTrap } from 'focus-trap-vue';
+import { FocusTrap } from "focus-trap-vue";
 
 const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
 const focusTrap = ref();
@@ -383,7 +397,8 @@ onBeforeUnmount(() => {
 			transform: translateY(0em);
 		}
 
-		&:not(:disabled):hover, &:focus-visible {
+		&:not(:disabled):hover,
+		&:focus-visible {
 			color: var(--accent);
 			text-decoration: none;
 
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 12e79f428..0a07f57e0 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -26,13 +26,16 @@
 					$style.root,
 					{
 						[$style.drawer]: type === 'drawer',
-						[$style.dialog]: type === 'dialog' || type === 'dialog:top',
+						[$style.dialog]:
+							type === 'dialog' || type === 'dialog:top',
 						[$style.popup]: type === 'popup',
 					},
 				]"
 				:style="{
 					zIndex,
-					pointerEvents: (manualShowing != null ? manualShowing : showing)
+					pointerEvents: (
+						manualShowing != null ? manualShowing : showing
+					)
 						? 'auto'
 						: 'none',
 					'--transformOrigin': transformOrigin,
@@ -76,7 +79,7 @@ import * as os from "@/os";
 import { isTouchUsing } from "@/scripts/touch";
 import { defaultStore } from "@/store";
 import { deviceKind } from "@/scripts/device-kind";
-import { FocusTrap } from 'focus-trap-vue';
+import { FocusTrap } from "focus-trap-vue";
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === "BODY") return null;
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 017bfae8c..ea5a9aca0 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -60,7 +60,7 @@
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted } from "vue";
-import { FocusTrap } from 'focus-trap-vue';
+import { FocusTrap } from "focus-trap-vue";
 import MkModal from "./MkModal.vue";
 
 const props = withDefaults(
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index 5d9c40d38..7e3fd6be5 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -279,7 +279,7 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
-const footerEl = ref<HTMLElement>(); 
+const footerEl = ref<HTMLElement>();
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 3d6f97e8b..fc587379c 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -35,10 +35,19 @@
 			class="content"
 			:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
 		>
-			<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" />
-			<div 
+			<XCwButton
+				ref="cwButton"
+				v-if="note.cw && !showContent"
+				v-model="showContent"
+				:note="note"
+				v-on:keydown="focusFooter"
+			/>
+			<div
 				class="body"
-				v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
+				v-bind="{
+					'aria-label': !showContent ? '' : null,
+					tabindex: !showContent ? '-1' : null,
+				}"
 			>
 				<span v-if="note.deletedAt" style="opacity: 0.5"
 					>({{ i18n.ts.deleted }})</span
@@ -106,14 +115,21 @@
 					v-on:focus="cwButton?.focus()"
 				></div>
 			</div>
-			<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
-			<XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" />
+			<XShowMoreButton
+				v-if="isLong"
+				v-model="collapsed"
+			></XShowMoreButton>
+			<XCwButton
+				v-if="note.cw && showContent"
+				v-model="showContent"
+				:note="note"
+			/>
 		</div>
 	</div>
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue"; 
+import { ref } from "vue";
 import * as misskey from "calckey-js";
 import * as mfm from "mfm-js";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
@@ -137,7 +153,7 @@ const emit = defineEmits<{
 	(ev: "focusfooter"): void;
 }>();
 
-const cwButton = ref<HTMLElement>(); 
+const cwButton = ref<HTMLElement>();
 const isLong =
 	!props.detailedView &&
 	props.note.cw == null &&
@@ -150,7 +166,6 @@ const urls = props.note.text
 
 let showContent = $ref(false);
 
-
 function focusFooter(ev) {
 	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
 		emit("focusfooter");
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index 83c667070..d66f838d1 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -96,7 +96,8 @@ export default defineComponent({
 				font-size: 0.9em;
 				margin-bottom: 0.3rem;
 
-				&:hover, &:focus-visible {
+				&:hover,
+				&:focus-visible {
 					text-decoration: none;
 					background: var(--panelHighlight);
 				}
diff --git a/packages/client/src/components/MkUserPreview.vue b/packages/client/src/components/MkUserPreview.vue
index 1e6db1442..a8a6bb90a 100644
--- a/packages/client/src/components/MkUserPreview.vue
+++ b/packages/client/src/components/MkUserPreview.vue
@@ -46,7 +46,10 @@
 					/></MkA>
 					<p class="username"><MkAcct :user="user" /></p>
 				</div>
-				<div class="description" :class="{ collapsed: isLong && collapsed }">
+				<div
+					class="description"
+					:class="{ collapsed: isLong && collapsed }"
+				>
 					<Mfm
 						v-if="user.description"
 						:text="user.description"
@@ -149,14 +152,15 @@ let user = $ref<misskey.entities.UserDetailed | null>(null);
 let top = $ref(0);
 let left = $ref(0);
 
-
 let isLong = $ref(false);
 let collapsed = $ref(!isLong);
 
 onMounted(() => {
 	if (typeof props.q === "object") {
 		user = props.q;
-		isLong = (user.description.split("\n").length > 9 || user.description.length > 400);
+		isLong =
+			user.description.split("\n").length > 9 ||
+			user.description.length > 400;
 	} else {
 		const query = props.q.startsWith("@")
 			? Acct.parse(props.q.substr(1))
@@ -165,10 +169,11 @@ onMounted(() => {
 		os.api("users/show", query).then((res) => {
 			if (!props.showing) return;
 			user = res;
-			isLong = (user.description.split("\n").length > 9 || user.description.length > 400);
+			isLong =
+				user.description.split("\n").length > 9 ||
+				user.description.length > 400;
 		});
 	}
-	
 
 	const rect = props.source.getBoundingClientRect();
 	const x =
@@ -313,7 +318,7 @@ onMounted(() => {
 
 		> .fields {
 			padding: 0 16px;
-			font-size: .8em;
+			font-size: 0.8em;
 			margin-top: 1em;
 
 			> .field {
diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index 437b7c53e..0fa244f8f 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -7,7 +7,7 @@
 				v-bind="Object.fromEntries(currentPageProps)"
 				tabindex="-1"
 				v-focus
-				style="outline: none;"
+				style="outline: none"
 			/>
 
 			<template #fallback>
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index bf070e269..a1a8c979b 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -313,7 +313,9 @@ onUnmounted(() => {
 			font-weight: normal;
 			opacity: 0.7;
 
-			&:hover, &:focus-visible, &.active {
+			&:hover,
+			&:focus-visible,
+			&.active {
 				opacity: 1;
 			}
 
diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue
index db953b890..a29cc20b9 100644
--- a/packages/client/src/pages/admin/overview.moderators.vue
+++ b/packages/client/src/pages/admin/overview.moderators.vue
@@ -12,7 +12,12 @@
 					class="user"
 					:to="`/user-info/${user.id}`"
 				>
-					<MkAvatar :user="user" class="avatar" indicator disableLink />
+					<MkAvatar
+						:user="user"
+						class="avatar"
+						indicator
+						disableLink
+					/>
 				</MkA>
 			</div>
 		</Transition>
diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index 71eba67c5..754f19e93 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -441,7 +441,9 @@ let preview_blockCode = $ref(
 let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
 let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
 let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
-let preview_search = $ref(`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`);
+let preview_search = $ref(
+	`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`
+);
 let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");
 let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
 let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");
@@ -463,9 +465,15 @@ let preview_x4 = $ref("$[x4 🍮]");
 let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
 let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
 let preview_sparkle = $ref("$[sparkle 🍮]");
-let preview_rotate = $ref("$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]");
-let preview_position = $ref("$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]");
-let preview_scale = $ref("$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]");
+let preview_rotate = $ref(
+	"$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]"
+);
+let preview_position = $ref(
+	"$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]"
+);
+let preview_scale = $ref(
+	"$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]"
+);
 let preview_fg = $ref("$[fg.color=ff0000 Text color]");
 let preview_bg = $ref("$[bg.color=ff0000 Background color]");
 let preview_plain = $ref(
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 20c177f37..14e1f1cd1 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -440,7 +440,7 @@ function more(ev: MouseEvent) {
 						color: var(--navActive);
 					}
 
-					&:hover, 
+					&:hover,
 					&:focus-within,
 					&.active {
 						color: var(--accent);

From 7c08ea32324fe832ac333eee1641c599362a3fc8 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 17:06:43 -0400
Subject: [PATCH 78/96] fix meta fetch

---
 packages/client/src/pages/instance-info.vue | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
index 7aeb2cd1c..15c853078 100644
--- a/packages/client/src/pages/instance-info.vue
+++ b/packages/client/src/pages/instance-info.vue
@@ -337,7 +337,7 @@
 import { watch } from "vue";
 import { Virtual } from "swiper";
 import { Swiper, SwiperSlide } from "swiper/vue";
-import type * as misskey from "calckey-js";
+import type * as calckey from "calckey-js";
 import MkChart from "@/components/MkChart.vue";
 import MkObjectView from "@/components/MkObjectView.vue";
 import FormLink from "@/components/form/link.vue";
@@ -360,11 +360,11 @@ import "swiper/scss";
 import "swiper/scss/virtual";
 import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
 
-type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
+type AugmentedInstanceMetadata = calckey.entities.DetailedInstanceMetadata & {
 	blockedHosts: string[];
 	silencedHosts: string[];
 };
-type AugmentedInstance = misskey.entities.Instance & {
+type AugmentedInstance = calckey.entities.Instance & {
 	isBlocked: boolean;
 	isSilenced: boolean;
 };
@@ -397,11 +397,8 @@ const usersPagination = {
 	offsetMode: true,
 };
 
-async function init() {
-	meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
-}
-
 async function fetch() {
+	meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
 	instance = (await os.api("federation/show-instance", {
 		host: props.host,
 	})) as AugmentedInstance;

From 688d283986395498d35e5c72f01f82b59b4baaf5 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 17:32:14 -0400
Subject: [PATCH 79/96] fix params

---
 packages/client/src/pages/about.federation.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/pages/about.federation.vue b/packages/client/src/pages/about.federation.vue
index 390f7b433..ca9a6d75c 100644
--- a/packages/client/src/pages/about.federation.vue
+++ b/packages/client/src/pages/about.federation.vue
@@ -132,9 +132,9 @@ const pagination = {
 			: state === "suspended"
 			? { suspended: true }
 			: state === "blocked"
-			? { silenced: true }
-			: state === "silenced"
 			? { blocked: true }
+			: state === "silenced"
+			? { silenced: true }
 			: state === "notResponding"
 			? { notResponding: true }
 			: {}),

From c8de7d818c50fb3a80af40d0362f874e43c557e2 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 17:42:01 -0400
Subject: [PATCH 80/96] add silenced colour

---
 .../src/components/MkInstanceCardMini.vue     | 20 +++++++++++++++++--
 1 file changed, 18 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkInstanceCardMini.vue b/packages/client/src/components/MkInstanceCardMini.vue
index 0a3fbbea2..6bc46c0e4 100644
--- a/packages/client/src/components/MkInstanceCardMini.vue
+++ b/packages/client/src/components/MkInstanceCardMini.vue
@@ -5,6 +5,7 @@
 			{
 				yellow: instance.isNotResponding,
 				red: instance.isBlocked,
+				purple: instance.isSilenced,
 				gray: instance.isSuspended,
 			},
 		]"
@@ -23,13 +24,13 @@
 </template>
 
 <script lang="ts" setup>
-import * as misskey from "calckey-js";
+import * as calckey from "calckey-js";
 import MkMiniChart from "@/components/MkMiniChart.vue";
 import * as os from "@/os";
 import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
 
 const props = defineProps<{
-	instance: misskey.entities.Instance;
+	instance: calckey.entities.Instance;
 }>();
 
 let chartValues = $ref<number[] | null>(null);
@@ -135,6 +136,21 @@ function getInstanceIcon(instance): string {
 		background-size: 16px 16px;
 	}
 
+	&:global(.purple) {
+		--c: rgba(196, 0, 255, 0.15);
+		background-image: linear-gradient(
+			45deg,
+			var(--c) 16.67%,
+			transparent 16.67%,
+			transparent 50%,
+			var(--c) 50%,
+			var(--c) 66.67%,
+			transparent 66.67%,
+			transparent 100%
+		);
+		background-size: 16px 16px;
+	}
+
 	&:global(.gray) {
 		--c: var(--bg);
 		background-image: linear-gradient(

From 8128ef5f011a787a86e4fcbe4ab8a1be4a29c3d5 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 18:00:19 -0400
Subject: [PATCH 81/96] add db migration

---
 .../1682844825247-InstanceSilence.js          | 165 ------------------
 .../1682891890317-InstanceSilence.js          |  63 +++++++
 2 files changed, 63 insertions(+), 165 deletions(-)
 delete mode 100644 packages/backend/migration/1682844825247-InstanceSilence.js
 create mode 100644 packages/backend/migration/1682891890317-InstanceSilence.js

diff --git a/packages/backend/migration/1682844825247-InstanceSilence.js b/packages/backend/migration/1682844825247-InstanceSilence.js
deleted file mode 100644
index 4c05b349d..000000000
--- a/packages/backend/migration/1682844825247-InstanceSilence.js
+++ /dev/null
@@ -1,165 +0,0 @@
-export class InstanceSilence1682844825247 {
-	name = "InstanceSilence1682844825247";
-
-	async up(queryRunner) {
-		await queryRunner.query(
-			`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`,
-		);
-		await queryRunner.query(
-			`DROP INDEX "public"."IDX_renote_muting_createdAt"`,
-		);
-		await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
-		await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
-		await queryRunner.query(
-			`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`,
-		);
-		await queryRunner.query(
-			`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `,
-		);
-		await queryRunner.query(
-			`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `,
-		);
-		await queryRunner.query(
-			`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `,
-		);
-		await queryRunner.query(
-			`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `,
-		);
-		await queryRunner.query(
-			`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
-		);
-	}
-
-	async down(queryRunner) {
-		await queryRunner.query(
-			`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`,
-		);
-		await queryRunner.query(
-			`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`,
-		);
-		await queryRunner.query(
-			`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`,
-		);
-		await queryRunner.query(
-			`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`,
-		);
-		await queryRunner.query(
-			`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`,
-		);
-		await queryRunner.query(
-			`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`,
-		);
-		await queryRunner.query(
-			`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`,
-		);
-		await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`,
-		);
-		await queryRunner.query(
-			`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `,
-		);
-		await queryRunner.query(
-			`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `,
-		);
-		await queryRunner.query(
-			`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `,
-		);
-		await queryRunner.query(
-			`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
-		);
-	}
-}
diff --git a/packages/backend/migration/1682891890317-InstanceSilence.js b/packages/backend/migration/1682891890317-InstanceSilence.js
new file mode 100644
index 000000000..b5c9539bc
--- /dev/null
+++ b/packages/backend/migration/1682891890317-InstanceSilence.js
@@ -0,0 +1,63 @@
+export class InstanceSilence1682891890317 {
+    name = 'InstanceSilence1682891890317'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`);
+        await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`);
+        await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `);
+        await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
+        await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`);
+        await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`);
+        await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+}

From 5fc77586f27db52a5f72e0bba74e55a32204ff41 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 18:01:06 -0400
Subject: [PATCH 82/96] format

---
 .../1682891890317-InstanceSilence.js          | 220 +++++++++++++-----
 1 file changed, 161 insertions(+), 59 deletions(-)

diff --git a/packages/backend/migration/1682891890317-InstanceSilence.js b/packages/backend/migration/1682891890317-InstanceSilence.js
index b5c9539bc..f487111f7 100644
--- a/packages/backend/migration/1682891890317-InstanceSilence.js
+++ b/packages/backend/migration/1682891890317-InstanceSilence.js
@@ -1,63 +1,165 @@
 export class InstanceSilence1682891890317 {
-    name = 'InstanceSilence1682891890317'
+	name = "InstanceSilence1682891890317";
 
-    async up(queryRunner) {
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`);
-        await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`);
-        await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `);
-        await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
+	async up(queryRunner) {
+		await queryRunner.query(
+			`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_renote_muting_createdAt"`,
+		);
+		await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
+		await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
+		await queryRunner.query(
+			`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `,
+		);
+		await queryRunner.query(
+			`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+		);
+	}
 
-    async down(queryRunner) {
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
-        await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`);
-        await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`);
-        await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
+	async down(queryRunner) {
+		await queryRunner.query(
+			`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`,
+		);
+		await queryRunner.query(
+			`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`,
+		);
+		await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `,
+		);
+		await queryRunner.query(
+			`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+		);
+	}
 }

From ec97ccd4c3d5bb677fd09d7cd2cc96a6dfc87f31 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 19:45:53 -0400
Subject: [PATCH 83/96] do not notify if the target is not following

---
 .../src/services/note/reaction/create.ts      | 25 ++++++++++++++++---
 1 file changed, 22 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts
index 1a3c52eb5..20a724f3d 100644
--- a/packages/backend/src/services/note/reaction/create.ts
+++ b/packages/backend/src/services/note/reaction/create.ts
@@ -12,6 +12,7 @@ import {
 	Notes,
 	Emojis,
 	Blockings,
+	Followings,
 } from "@/models/index.js";
 import { IsNull, Not } from "typeorm";
 import { perUserReactionsChart } from "@/services/chart/index.js";
@@ -21,6 +22,7 @@ import deleteReaction from "./delete.js";
 import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
 import type { NoteReaction } from "@/models/entities/note-reaction.js";
 import { IdentifiableError } from "@/misc/identifiable-error.js";
+import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
 
 export default async (
 	user: { id: User["id"]; host: User["host"] },
@@ -118,8 +120,25 @@ export default async (
 		userId: user.id,
 	});
 
-	// リアクションされたユーザーがローカルユーザーなら通知を作成
-	if (note.userHost === null) {
+	// Create notification if the reaction target is a local user.
+	if (
+		note.userHost === null &&
+		// if a local user reacted, or
+		(Users.isLocalUser(user) ||
+			// if a remote user not in a silenced instance reacted
+			(Users.isRemoteUser(user) &&
+			// if a remote user is in a silenced instance and the target is a local follower.
+				!(
+					(await shouldSilenceInstance(user.host)) &&
+					!(await Followings.exist({
+						where: {
+							followerId: note.userId,
+							followerHost: IsNull(),
+							followeeId: user.id,
+						},
+					}))
+				)))
+	) {
 		createNotification(note.userId, "reaction", {
 			notifierId: user.id,
 			note: note,
@@ -143,7 +162,7 @@ export default async (
 		}
 	});
 
-	//#region 配信
+	//#region deliver
 	if (Users.isLocalUser(user) && !note.localOnly) {
 		const content = renderActivity(await renderLike(record, note));
 		const dm = new DeliverManager(user, content);

From 1faa47f558a593653b631cc8dc2311292a3a805f Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 17:14:04 -0700
Subject: [PATCH 84/96] favicon

---
 packages/backend/assets/favicon.ico | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/assets/favicon.ico b/packages/backend/assets/favicon.ico
index 8d46b0c1d..9fb005f7b 100644
--- a/packages/backend/assets/favicon.ico
+++ b/packages/backend/assets/favicon.ico
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:c414a146ef32ee9017444a977d5b6bb40079be9cdfeaab9a8a58f836fb6eea62
-size 4286
+oid sha256:03a16bbee6313e52ca17a04db25c4a8b312a3208421a7e351e734f4ad0184900
+size 483961

From fbc8d5666e5ad0b99e9ef834e427a91563e67af2 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 17:14:31 -0700
Subject: [PATCH 85/96] patrons

---
 patrons.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/patrons.json b/patrons.json
index 979fa3d8d..54dd6498e 100644
--- a/patrons.json
+++ b/patrons.json
@@ -26,6 +26,7 @@
 		"@jovikowi@calckey.social",
 		"@padraig@calckey.social",
 		"@pancakes@cats.city",
+		"@theresmiling@calckey.social",
 		"Interkosmos Link"
 	]
 }

From e3e07590d74be1f88bcd360a2c47646ca05f5003 Mon Sep 17 00:00:00 2001
From: s1idewhist1e <s1idewhist1e@noreply.codeberg.org>
Date: Mon, 1 May 2023 00:15:26 +0000
Subject: [PATCH 86/96] FIx: incorrect/poorly worded warning when deleting
 drive files (#9982)

Closes #9047

Co-authored-by: s1idewhist1e <trombonedude05@gmail.com>
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9982
Co-authored-by: s1idewhist1e <s1idewhist1e@noreply.codeberg.org>
Co-committed-by: s1idewhist1e <s1idewhist1e@noreply.codeberg.org>
---
 locales/en-US.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index e5377dc54..662ae24e4 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -68,8 +68,8 @@ import: "Import"
 export: "Export"
 files: "Files"
 download: "Download"
-driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Posts\
-  \ with this file attached will also be deleted."
+driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? It\
+  \ will be removed from all posts that contain it as an attachment."
 unfollowConfirm: "Are you sure that you want to unfollow {name}?"
 exportRequested: "You've requested an export. This may take a while. It will be added\
   \ to your Drive once completed."

From d3f7b4c18a00cd9753d0983ca9eafd317386a50d Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 17:57:04 -0700
Subject: [PATCH 87/96] chore: theme refactor

---
 packages/client/src/themes/d-astro.json5      |  2 +-
 packages/client/src/themes/d-botanical.json5  |  2 +-
 .../src/themes/d-catppuccin-frappe.json5      |  2 +-
 .../src/themes/d-catppuccin-mocha.json5       |  2 +-
 packages/client/src/themes/d-cherry.json5     |  2 +-
 packages/client/src/themes/d-future.json5     |  2 +-
 packages/client/src/themes/d-green-lime.json5 |  2 +-
 .../client/src/themes/d-green-orange.json5    | 24 -----
 packages/client/src/themes/d-ice.json5        |  2 +-
 packages/client/src/themes/d-persimmon.json5  |  2 +-
 packages/client/src/themes/d-u0.json5         |  2 +-
 packages/client/src/themes/l-apricot.json5    |  2 +-
 .../src/themes/l-catppuccin-latte.json5       | 94 +++++++++++++++++++
 packages/client/src/themes/l-cherry.json5     |  2 +-
 packages/client/src/themes/l-coffee.json5     |  2 +-
 packages/client/src/themes/l-rainy.json5      |  2 +-
 packages/client/src/themes/l-sushi.json5      |  2 +-
 packages/client/src/themes/l-u0.json5         |  2 +-
 packages/client/src/themes/l-vivid.json5      |  2 +-
 19 files changed, 111 insertions(+), 41 deletions(-)
 delete mode 100644 packages/client/src/themes/d-green-orange.json5
 create mode 100644 packages/client/src/themes/l-catppuccin-latte.json5

diff --git a/packages/client/src/themes/d-astro.json5 b/packages/client/src/themes/d-astro.json5
index c6a927ec3..a51ee94ae 100644
--- a/packages/client/src/themes/d-astro.json5
+++ b/packages/client/src/themes/d-astro.json5
@@ -1,7 +1,7 @@
 {
 	id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
 	base: 'dark',
-	name: 'Mi Astro Dark',
+	name: 'Astro Dark',
 	author: 'syuilo',
 	props: {
 		bg: '#232125',
diff --git a/packages/client/src/themes/d-botanical.json5 b/packages/client/src/themes/d-botanical.json5
index c03b95e2d..08fabf86a 100644
--- a/packages/client/src/themes/d-botanical.json5
+++ b/packages/client/src/themes/d-botanical.json5
@@ -1,7 +1,7 @@
 {
 	id: '504debaf-4912-6a4c-5059-1db08a76b737',
 
-	name: 'Mi Botanical Dark',
+	name: 'Botanical Dark',
 	author: 'syuilo',
 
 	base: 'dark',
diff --git a/packages/client/src/themes/d-catppuccin-frappe.json5 b/packages/client/src/themes/d-catppuccin-frappe.json5
index 891fe1805..ed3aac3c5 100644
--- a/packages/client/src/themes/d-catppuccin-frappe.json5
+++ b/packages/client/src/themes/d-catppuccin-frappe.json5
@@ -1,7 +1,7 @@
 {
 	id: 'ffcd3328-5c57-4ca3-9dac-4580cbf7742f',
 	base: 'dark',
-	name: 'Catppuccin frappe',
+	name: 'Catppuccin Frappe',
 	props: {
 		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
diff --git a/packages/client/src/themes/d-catppuccin-mocha.json5 b/packages/client/src/themes/d-catppuccin-mocha.json5
index 94e1381c7..83d464f88 100644
--- a/packages/client/src/themes/d-catppuccin-mocha.json5
+++ b/packages/client/src/themes/d-catppuccin-mocha.json5
@@ -1,7 +1,7 @@
 {
 	id: 'd413f41f-a489-48be-9e20-3532ffbb4363',
 	base: 'dark',
-	name: 'Catppuccin mocha',
+	name: 'Catppuccin Mocha',
 	props: {
 		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
diff --git a/packages/client/src/themes/d-cherry.json5 b/packages/client/src/themes/d-cherry.json5
index a7e1ad1c8..e39e9ce66 100644
--- a/packages/client/src/themes/d-cherry.json5
+++ b/packages/client/src/themes/d-cherry.json5
@@ -1,7 +1,7 @@
 {
 	id: '679b3b87-a4e9-4789-8696-b56c15cc33b0',
 
-	name: 'Mi Cherry Dark',
+	name: 'Cherry Dark',
 	author: 'syuilo',
 
 	base: 'dark',
diff --git a/packages/client/src/themes/d-future.json5 b/packages/client/src/themes/d-future.json5
index b6fa1ab0c..8b6345708 100644
--- a/packages/client/src/themes/d-future.json5
+++ b/packages/client/src/themes/d-future.json5
@@ -1,7 +1,7 @@
 {
 	id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
 
-	name: 'Mi Future Dark',
+	name: 'Future Dark',
 	author: 'syuilo',
 
 	base: 'dark',
diff --git a/packages/client/src/themes/d-green-lime.json5 b/packages/client/src/themes/d-green-lime.json5
index a6983b9ac..c06e9af71 100644
--- a/packages/client/src/themes/d-green-lime.json5
+++ b/packages/client/src/themes/d-green-lime.json5
@@ -1,7 +1,7 @@
 {
 	id: '02816013-8107-440f-877e-865083ffe194',
 
-	name: 'Mi Green+Lime Dark',
+	name: 'Mi Dark',
 	author: 'syuilo',
 
 	base: 'dark',
diff --git a/packages/client/src/themes/d-green-orange.json5 b/packages/client/src/themes/d-green-orange.json5
deleted file mode 100644
index 62adc39e2..000000000
--- a/packages/client/src/themes/d-green-orange.json5
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-	id: 'dc489603-27b5-424a-9b25-1ff6aec9824a',
-
-	name: 'Mi Green+Orange Dark',
-	author: 'syuilo',
-
-	base: 'dark',
-
-	props: {
-		accent: '#e97f00',
-		bg: '#0C1210',
-		fg: '#dee7e4',
-		fgHighlighted: '#fff',
-		fgOnAccent: '#192320',
-		divider: '#e7fffb24',
-		panel: '#192320',
-		panelHeaderBg: '@panel',
-		panelHeaderDivider: '@divider',
-		popup: '#293330',
-		renote: '@accent',
-		mentionMe: '#b4e900',
-		link: '#24d7ce',
-	},
-}
diff --git a/packages/client/src/themes/d-ice.json5 b/packages/client/src/themes/d-ice.json5
index 179b060dc..c095bf287 100644
--- a/packages/client/src/themes/d-ice.json5
+++ b/packages/client/src/themes/d-ice.json5
@@ -1,7 +1,7 @@
 {
 	id: '66e7e5a9-cd43-42cd-837d-12f47841fa34',
 
-	name: 'Mi Ice Dark',
+	name: 'Ice Dark',
 	author: 'syuilo',
 
 	base: 'dark',
diff --git a/packages/client/src/themes/d-persimmon.json5 b/packages/client/src/themes/d-persimmon.json5
index e36265ff1..ec1728d07 100644
--- a/packages/client/src/themes/d-persimmon.json5
+++ b/packages/client/src/themes/d-persimmon.json5
@@ -1,7 +1,7 @@
 {
 	id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
 
-	name: 'Mi Persimmon Dark',
+	name: 'Persimmon Dark',
 	author: 'syuilo',
 
 	base: 'dark',
diff --git a/packages/client/src/themes/d-u0.json5 b/packages/client/src/themes/d-u0.json5
index 67c9235df..b5fe92cbc 100644
--- a/packages/client/src/themes/d-u0.json5
+++ b/packages/client/src/themes/d-u0.json5
@@ -1,7 +1,7 @@
 {
 	id: '7a5bc13b-df8f-4d44-8e94-4452f0c634bb',
 	base: 'dark',
-	name: 'Mi U0 Dark',
+	name: 'U0 Dark',
 	props: {
 		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
diff --git a/packages/client/src/themes/l-apricot.json5 b/packages/client/src/themes/l-apricot.json5
index 1ed552557..c1a8b29c0 100644
--- a/packages/client/src/themes/l-apricot.json5
+++ b/packages/client/src/themes/l-apricot.json5
@@ -1,7 +1,7 @@
 {
 	id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
 
-	name: 'Mi Apricot Light',
+	name: 'Apricot Light',
 	author: 'syuilo',
 
 	base: 'light',
diff --git a/packages/client/src/themes/l-catppuccin-latte.json5 b/packages/client/src/themes/l-catppuccin-latte.json5
new file mode 100644
index 000000000..085e66df6
--- /dev/null
+++ b/packages/client/src/themes/l-catppuccin-latte.json5
@@ -0,0 +1,94 @@
+{
+	id: "169661d2-5a17-4dfc-b71b-9938cbbbed3e",
+	base: "light",
+	name: "Catppuccin Latte",
+	props: {
+		X2: ":darken<2<@panel",
+		X3: "rgba(255, 255, 255, 0.05)",
+		X4: "rgba(255, 255, 255, 0.1)",
+		X5: "rgba(255, 255, 255, 0.05)",
+		X6: "rgba(255, 255, 255, 0.15)",
+		X7: "rgba(255, 255, 255, 0.05)",
+		X8: ":lighten<5<@accent",
+		X9: ":darken<5<@accent",
+		bg: "#dce0e8",
+		fg: "#4c4f69",
+		X10: ":alpha<0.4<@accent",
+		X11: "rgba(0, 0, 0, 0.3)",
+		X12: "rgba(255, 255, 255, 0.1)",
+		X13: "rgba(255, 255, 255, 0.15)",
+		X14: ":alpha<0.5<@navBg",
+		X15: ":alpha<0<@panel",
+		X16: ":alpha<0.7<@panel",
+		X17: ":alpha<0.8<@bg",
+		cwBg: "#bcc0cc",
+		cwFg: "#5c5f77",
+		link: "#1e66f5",
+		warn: "#fe640b",
+		badge: "#1e66f5",
+		error: "#d20f39",
+		focus: ":alpha<0.3<@accent",
+		navBg: "@panel",
+		navFg: "@fg",
+		panel: ":lighten<3<@bg",
+		popup: ":lighten<3<@panel",
+		accent: "#8839ef",
+		header: ":alpha<0.7<@panel",
+		infoBg: "#ccd0da",
+		infoFg: "#6c6f85",
+		renote: "#1e66f5",
+		shadow: "rgba(0, 0, 0, 0.3)",
+		divider: "rgba(255, 255, 255, 0.1)",
+		hashtag: "#209fb5",
+		mention: "@accent",
+		modalBg: "rgba(0, 0, 0, 0.5)",
+		success: "#40a02b",
+		buttonBg: "rgba(255, 255, 255, 0.05)",
+		switchBg: "rgba(255, 255, 255, 0.15)",
+		acrylicBg: ":alpha<0.5<@bg",
+		cwHoverBg: "#acb0be",
+		indicator: "@accent",
+		mentionMe: "@mention",
+		messageBg: "@bg",
+		navActive: "@accent",
+		accentedBg: ":alpha<0.15<@accent",
+		codeNumber: "#40a02b",
+		codeString: "#fe640b",
+		fgOnAccent: "#eff1f5",
+		infoWarnBg: "#ccd0da",
+		infoWarnFg: "#5c5f77",
+		navHoverFg: ":lighten<17<@fg",
+		swutchOnBg: "@accentedBg",
+		swutchOnFg: "@accent",
+		codeBoolean: "@accent",
+		dateLabelFg: "@fg",
+		deckDivider: "#9ca0b0",
+		inputBorder: "rgba(255, 255, 255, 0.1)",
+		panelBorder: "solid 1px var(--divider)",
+		swutchOffBg: "rgba(255, 255, 255, 0.1)",
+		swutchOffFg: "@fg",
+		accentDarken: ":darken<10<@accent",
+		acrylicPanel: ":alpha<0.5<@panel",
+		navIndicator: "@indicator",
+		windowHeader: ":alpha<0.85<@panel",
+		accentLighten: ":lighten<10<@accent",
+		buttonHoverBg: "rgba(255, 255, 255, 0.1)",
+		driveFolderBg: ":alpha<0.3<@accent",
+		fgHighlighted: ":lighten<3<@fg",
+		fgTransparent: ":alpha<0.5<@fg",
+		panelHeaderBg: ":lighten<3<@panel",
+		panelHeaderFg: "@fg",
+		buttonGradateA: "@accent",
+		buttonGradateB: ":hue<20<@accent",
+		htmlThemeColor: "@bg",
+		panelHighlight: ":lighten<3<@panel",
+		listItemHoverBg: "rgba(255, 255, 255, 0.03)",
+		scrollbarHandle: "rgba(255, 255, 255, 0.2)",
+		inputBorderHover: "rgba(255, 255, 255, 0.2)",
+		wallpaperOverlay: "rgba(0, 0, 0, 0.5)",
+		fgTransparentWeak: ":alpha<0.75<@fg",
+		panelHeaderDivider: "rgba(0, 0, 0, 0)",
+		scrollbarHandleHover: "rgba(255, 255, 255, 0.4)",
+	},
+	author: "somebody ¯_(ツ)_/¯",
+}
diff --git a/packages/client/src/themes/l-cherry.json5 b/packages/client/src/themes/l-cherry.json5
index 5ad240241..9aab308fc 100644
--- a/packages/client/src/themes/l-cherry.json5
+++ b/packages/client/src/themes/l-cherry.json5
@@ -1,7 +1,7 @@
 {
 	id: 'ac168876-f737-4074-a3fc-a370c732ef48',
 
-	name: 'Mi Cherry Light',
+	name: 'Cherry Light',
 	author: 'syuilo',
 
 	base: 'light',
diff --git a/packages/client/src/themes/l-coffee.json5 b/packages/client/src/themes/l-coffee.json5
index fbcd4fa9e..252b5989c 100644
--- a/packages/client/src/themes/l-coffee.json5
+++ b/packages/client/src/themes/l-coffee.json5
@@ -1,7 +1,7 @@
 {
 	id: '6ed80faa-74f0-42c2-98e4-a64d9e138eab',
 
-	name: 'Mi Coffee Light',
+	name: 'Coffee Light',
 	author: 'syuilo',
 
 	base: 'light',
diff --git a/packages/client/src/themes/l-rainy.json5 b/packages/client/src/themes/l-rainy.json5
index 283dd74c6..a7b871806 100644
--- a/packages/client/src/themes/l-rainy.json5
+++ b/packages/client/src/themes/l-rainy.json5
@@ -1,7 +1,7 @@
 {
 	id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
 
-	name: 'Mi Rainy Light',
+	name: 'Rainy Light',
 	author: 'syuilo',
 
 	base: 'light',
diff --git a/packages/client/src/themes/l-sushi.json5 b/packages/client/src/themes/l-sushi.json5
index 5846927d6..d14f32293 100644
--- a/packages/client/src/themes/l-sushi.json5
+++ b/packages/client/src/themes/l-sushi.json5
@@ -1,7 +1,7 @@
 {
 	id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c',
 
-	name: 'Mi Sushi Light',
+	name: 'Sushi Light',
 	author: 'syuilo',
 
 	base: 'light',
diff --git a/packages/client/src/themes/l-u0.json5 b/packages/client/src/themes/l-u0.json5
index 03b114ba3..1d89e0543 100644
--- a/packages/client/src/themes/l-u0.json5
+++ b/packages/client/src/themes/l-u0.json5
@@ -1,7 +1,7 @@
 {
 	id: 'e2c940b5-6e9a-4c03-b738-261c720c426d',
 	base: 'light',
-	name: 'Mi U0 Light',
+	name: 'U0 Light',
 	props: {
 		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
diff --git a/packages/client/src/themes/l-vivid.json5 b/packages/client/src/themes/l-vivid.json5
index b3c08f38a..cd35626a1 100644
--- a/packages/client/src/themes/l-vivid.json5
+++ b/packages/client/src/themes/l-vivid.json5
@@ -1,7 +1,7 @@
 {
 	id: '6128c2a9-5c54-43fe-a47d-17942356470b',
 
-	name: 'Mi Vivid Light',
+	name: 'Vivid Light',
 	author: 'syuilo',
 
 	base: 'light',

From faa5fc5dd40e4abc3729f17d25eb58ad218a16a7 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 20:22:50 -0400
Subject: [PATCH 88/96] suppress notification from silenced users and instances

---
 .../src/services/create-notification.ts       | 22 +++++++++++++++++++
 .../src/services/following/requests/create.ts |  8 ++++++-
 packages/backend/src/services/note/create.ts  | 19 +++++-----------
 .../src/services/note/reaction/create.ts      | 21 +-----------------
 4 files changed, 35 insertions(+), 35 deletions(-)

diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts
index f6545b131..e2dd3fc33 100644
--- a/packages/backend/src/services/create-notification.ts
+++ b/packages/backend/src/services/create-notification.ts
@@ -6,11 +6,13 @@ import {
 	NoteThreadMutings,
 	UserProfiles,
 	Users,
+	Followings,
 } from "@/models/index.js";
 import { genId } from "@/misc/gen-id.js";
 import type { User } from "@/models/entities/user.js";
 import type { Notification } from "@/models/entities/notification.js";
 import { sendEmailNotification } from "./send-email-notification.js";
+import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
 
 export async function createNotification(
 	notifieeId: User["id"],
@@ -21,6 +23,26 @@ export async function createNotification(
 		return null;
 	}
 
+	if (
+		data.notifierId &&
+		["mention", "reply", "renote", "quote", "reaction"].includes(type)
+	) {
+		const notifier = await Users.findOneBy({ id: data.notifierId });
+		// suppress if the notifier does not exist or is silenced.
+		if (!notifier) return null;
+
+		// suppress if the notifier is silenced or in a silenced instance, and not followed by the notifiee.
+		if (
+			(notifier.isSilenced ||
+				(Users.isRemoteUser(notifier) &&
+					(await shouldSilenceInstance(notifier.host)))) &&
+			!(await Followings.exist({
+				where: { followerId: notifieeId, followeeId: data.notifierId },
+			}))
+		)
+			return null;
+	}
+
 	const profile = await UserProfiles.findOneBy({ userId: notifieeId });
 
 	const isMuted = profile?.mutingNotificationTypes.includes(type);
diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts
index 27f9144d0..50dbd9b3b 100644
--- a/packages/backend/src/services/following/requests/create.ts
+++ b/packages/backend/src/services/following/requests/create.ts
@@ -80,7 +80,13 @@ export default async function (
 	}
 
 	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		const content = renderActivity(renderFollow(follower, followee, requestId ?? `${config.url}/follows/${followRequest.id}`));
+		const content = renderActivity(
+			renderFollow(
+				follower,
+				followee,
+				requestId ?? `${config.url}/follows/${followRequest.id}`,
+			),
+		);
 		deliver(follower, content, followee.inbox);
 	}
 }
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index ad50bd97a..f1164c9c6 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -205,11 +205,12 @@ export default async (
 			data.visibility = "home";
 		}
 
-		const inSilencedInstance =
-			Users.isRemoteUser(user) && (await shouldSilenceInstance(user.host));
-
 		// Enforce home visibility if the user is in a silenced instance.
-		if (data.visibility === "public" && inSilencedInstance) {
+		if (
+			data.visibility === "public" &&
+			Users.isRemoteUser(user) &&
+			(await shouldSilenceInstance(user.host))
+		) {
 			data.visibility = "home";
 		}
 
@@ -317,16 +318,6 @@ export default async (
 			}
 		}
 
-		// Remove from mention the local users who aren't following the remote user in the silenced instance.
-		if (inSilencedInstance) {
-			const relations = await Followings.findBy([
-				{ followeeId: user.id, followerHost: IsNull() }, // a local user following the silenced user
-			]).then((rels) => rels.map((rel) => rel.followerId));
-			mentionedUsers = mentionedUsers.filter((mentioned) =>
-				relations.includes(mentioned.id),
-			);
-		}
-
 		const note = await insertNote(user, data, tags, emojis, mentionedUsers);
 
 		res(note);
diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts
index 20a724f3d..277393eb4 100644
--- a/packages/backend/src/services/note/reaction/create.ts
+++ b/packages/backend/src/services/note/reaction/create.ts
@@ -12,7 +12,6 @@ import {
 	Notes,
 	Emojis,
 	Blockings,
-	Followings,
 } from "@/models/index.js";
 import { IsNull, Not } from "typeorm";
 import { perUserReactionsChart } from "@/services/chart/index.js";
@@ -22,7 +21,6 @@ import deleteReaction from "./delete.js";
 import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
 import type { NoteReaction } from "@/models/entities/note-reaction.js";
 import { IdentifiableError } from "@/misc/identifiable-error.js";
-import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
 
 export default async (
 	user: { id: User["id"]; host: User["host"] },
@@ -121,24 +119,7 @@ export default async (
 	});
 
 	// Create notification if the reaction target is a local user.
-	if (
-		note.userHost === null &&
-		// if a local user reacted, or
-		(Users.isLocalUser(user) ||
-			// if a remote user not in a silenced instance reacted
-			(Users.isRemoteUser(user) &&
-			// if a remote user is in a silenced instance and the target is a local follower.
-				!(
-					(await shouldSilenceInstance(user.host)) &&
-					!(await Followings.exist({
-						where: {
-							followerId: note.userId,
-							followerHost: IsNull(),
-							followeeId: user.id,
-						},
-					}))
-				)))
-	) {
+	if (note.userHost === null) {
 		createNotification(note.userId, "reaction", {
 			notifierId: user.id,
 			note: note,

From 5013111bee6908d7664e972a88c3191f672f2b75 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Sun, 30 Apr 2023 21:43:56 -0400
Subject: [PATCH 89/96] enforce follow-request from silenced users

---
 packages/backend/src/services/following/create.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts
index c987a01e5..3a77676b3 100644
--- a/packages/backend/src/services/following/create.ts
+++ b/packages/backend/src/services/following/create.ts
@@ -227,12 +227,14 @@ export default async function (
 	});
 
 	// フォロー対象が鍵アカウントである or
+	// The follower is silenced, or
 	// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
 	// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
 	// The follower is remote, the followee is local, and the follower is in a silenced instance.
 	// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
 	if (
 		followee.isLocked ||
+		follower.isSilenced ||
 		(followeeProfile.carefulBot && follower.isBot) ||
 		(Users.isLocalUser(follower) && Users.isRemoteUser(followee)) ||
 		(Users.isRemoteUser(follower) &&

From 45ef53994c027481a67ebfcc6159191fe72f3d36 Mon Sep 17 00:00:00 2001
From: s1idewhist1e <trombonedude05@gmail.com>
Date: Sun, 30 Apr 2023 22:03:17 -0700
Subject: [PATCH 90/96] Wrap note fetching in a try/catch

---
 packages/backend/src/server/web/index.ts | 43 +++++++++++++-----------
 1 file changed, 23 insertions(+), 20 deletions(-)

diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index 642a17d57..270026dcc 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -399,28 +399,31 @@ router.get("/notes/:note", async (ctx, next) => {
 		visibility: In(["public", "home"]),
 	});
 
-	if (note) {
-		const _note = await Notes.pack(note);
-		const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
-		const meta = await fetchMeta();
-		await ctx.render("note", {
-			note: _note,
-			profile,
-			avatarUrl: await Users.getAvatarUrl(
-				await Users.findOneByOrFail({ id: note.userId }),
-			),
-			// TODO: Let locale changeable by instance setting
-			summary: getNoteSummary(_note),
-			instanceName: meta.name || "Calckey",
-			icon: meta.iconUrl,
-			privateMode: meta.privateMode,
-			themeColor: meta.themeColor,
-		});
+	try {
+		if (note) {
+			const _note = await Notes.pack(note);
+			
+			const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
+			const meta = await fetchMeta();
+			await ctx.render("note", {
+				note: _note,
+				profile,
+				avatarUrl: await Users.getAvatarUrl(
+					await Users.findOneByOrFail({ id: note.userId }),
+				),
+				// TODO: Let locale changeable by instance setting
+				summary: getNoteSummary(_note),
+				instanceName: meta.name || "Calckey",
+				icon: meta.iconUrl,
+				privateMode: meta.privateMode,
+				themeColor: meta.themeColor,
+			});
 
-		ctx.set("Cache-Control", "public, max-age=15");
+			ctx.set("Cache-Control", "public, max-age=15");
 
-		return;
-	}
+			return;
+		}
+	} catch {}
 
 	await next();
 });

From b1bbc3ac8eeb573f93c7471963ecc585c773937a Mon Sep 17 00:00:00 2001
From: s1idewhist1e <trombonedude05@gmail.com>
Date: Sun, 30 Apr 2023 22:50:43 -0700
Subject: [PATCH 91/96] fix email validation

---
 packages/backend/src/server/api/private/signup.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts
index d24a74c12..440d0e368 100644
--- a/packages/backend/src/server/api/private/signup.ts
+++ b/packages/backend/src/server/api/private/signup.ts
@@ -55,7 +55,7 @@ export default async (ctx: Koa.Context) => {
 			return;
 		}
 
-		const available = await validateEmailForAccount(emailAddress);
+		const { available } = await validateEmailForAccount(emailAddress);
 		if (!available) {
 			ctx.status = 400;
 			return;

From cb2fefef1972560f50936601801e40349700d857 Mon Sep 17 00:00:00 2001
From: Cleo <cutestnekoaqua@noreply.codeberg.org>
Date: Mon, 1 May 2023 15:42:27 +0000
Subject: [PATCH 92/96] Fixing post visibility patch

Co-authored-by: Laura Hausmann <laura@hausmann.dev>
---
 packages/client/src/components/MkPostForm.vue | 24 ++++++++++++-------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index f16d1775a..1e8d363d8 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -462,15 +462,21 @@ if (
 	props.reply &&
 	["home", "followers", "specified"].includes(props.reply.visibility)
 ) {
-	visibility = props.reply.visibility;
-	if (props.reply.visibility === "specified") {
-		os.api("users/show", {
-			userIds: props.reply.visibleUserIds.filter(
-				(uid) => uid !== $i.id && uid !== props.reply.userId
-			),
-		}).then((users) => {
-			users.forEach(pushVisibleUser);
-		});
+        if (props.reply.visibility === 'home' && visibility === 'followers') {
+		visibility = 'followers';
+	} else if (['home', 'followers'].includes(props.reply.visibility) && visibility === 'specified') {
+		visibility = 'specified';
+	} else {
+		visibility = props.reply.visibility;
+	}
+	if (visibility === 'specified') {
+		if (props.reply.visibleUserIds) {
+			os.api('users/show', {
+				userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
+			}).then(users => {
+				users.forEach(pushVisibleUser);
+			});
+		}
 
 		if (props.reply.userId !== $i.id) {
 			os.api("users/show", { userId: props.reply.userId }).then(

From 139a7f337495c2157934eec06d52361cd60f45cb Mon Sep 17 00:00:00 2001
From: Pyrox <pyrox@pyrox.dev>
Date: Mon, 1 May 2023 14:52:38 -0400
Subject: [PATCH 93/96] fix: Commit CI not running because cargo is not
 installed

---
 .woodpecker/commit.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.woodpecker/commit.yml b/.woodpecker/commit.yml
index 6bb1b2d81..827ebc81d 100644
--- a/.woodpecker/commit.yml
+++ b/.woodpecker/commit.yml
@@ -2,6 +2,9 @@ pipeline:
   testCommit:
     image: node:latest
     commands:
+      - apt-get update
+      - apt-get install -y cargo
+      - rm -rf /var/lib/apt/lists/*
       - cp .config/ci.yml .config/default.yml
       - corepack enable
       - corepack prepare pnpm@latest --activate

From c5c1fb73cf75cfb3b706d15214863f04d663bd12 Mon Sep 17 00:00:00 2001
From: Pyrox <pyrox@pyrox.dev>
Date: Mon, 1 May 2023 15:09:21 -0400
Subject: [PATCH 94/96] fix: Switch to node alpine image

---
 .woodpecker/commit.yml | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/.woodpecker/commit.yml b/.woodpecker/commit.yml
index 827ebc81d..f57fd9d1e 100644
--- a/.woodpecker/commit.yml
+++ b/.woodpecker/commit.yml
@@ -1,10 +1,8 @@
 pipeline:
   testCommit:
-    image: node:latest
+    image: node:alpine
     commands:
-      - apt-get update
-      - apt-get install -y cargo
-      - rm -rf /var/lib/apt/lists/*
+      - apk add --no-cache cargo python3 make g++
       - cp .config/ci.yml .config/default.yml
       - corepack enable
       - corepack prepare pnpm@latest --activate

From b6a12db2979393f36cfb4870b76000aacee90d11 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Mon, 1 May 2023 12:57:24 -0700
Subject: [PATCH 95/96] update patrons

---
 patrons.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/patrons.json b/patrons.json
index 54dd6498e..f27fd534a 100644
--- a/patrons.json
+++ b/patrons.json
@@ -27,6 +27,7 @@
 		"@padraig@calckey.social",
 		"@pancakes@cats.city",
 		"@theresmiling@calckey.social",
+		"@AlderForrest@calckey.social",
 		"Interkosmos Link"
 	]
 }

From d9beed624e36ac54ddd7aea289ad047c5b4397cd Mon Sep 17 00:00:00 2001
From: jolupa <jolupameister@gmail.com>
Date: Mon, 1 May 2023 15:44:14 +0000
Subject: [PATCH 96/96] chore: Translated using Weblate (Catalan)

Currently translated at 69.2% (1204 of 1739 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/ca/
---
 locales/ca-ES.yml | 704 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 703 insertions(+), 1 deletion(-)

diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index b226d641e..2a72376de 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -140,10 +140,80 @@ file: "Fitxers"
 _email:
   _follow:
     title: "t'ha seguit"
+  _receiveFollowRequest:
+    title: Heu rebut una sol·licitud de seguiment
 _mfm:
   mention: "Menció"
   quote: "Citar"
   search: "Cercar"
+  dummy: Calckey amplia el món del Fediverse
+  hashtag: Etiqueta
+  intro: MFM és un llenguatge de marques utilitzat a Misskey, Calckey, Akkoma i més
+    que es pot utilitzar en molts llocs. Aquí podeu veure una llista de tota la sintaxi
+    MFM disponible.
+  hashtagDescription: Podeu especificar un hashtag mitjançant un signe de coixinet
+    i un text.
+  url: URL
+  urlDescription: Es poden mostrar URL.
+  link: Enllaç
+  linkDescription: Parts específiques del text es poden mostrar com a URL.
+  bold: Negreta
+  boldDescription: Ressalta les lletres fent-les més gruixudes.
+  smallDescription: Mostra contingut petit i prim.
+  small: Petit
+  centerDescription: Mostra el contingut centrat.
+  inlineCode: Codi (en línia)
+  inlineMathDescription: Mostra fórmules matemàtiques (KaTeX) en línia
+  blockCode: Codi (Bloc)
+  blockCodeDescription: Mostra el ressaltat de sintaxi per al codi de diverses línies
+    (programa) en un bloc.
+  inlineMath: Matemàtiques (en línia)
+  jellyDescription: Dóna al contingut una animació semblant a una gelatina.
+  bounceDescription: Ofereix al contingut una animació de rebot.
+  jumpDescription: Dóna al contingut una animació de salt.
+  shake: Animació (Shake)
+  shakeDescription: Dóna al contingut una animació tremolosa.
+  bounce: Animació (Bounce)
+  x3Description: Mostra contingut encara més gran.
+  x2Description: Mostra contingut més gran.
+  twitchDescription: Ofereix al contingut una animació fortament convulsa.
+  spin: Animació (Spin)
+  spinDescription: Dóna al contingut una animació giratòria.
+  x2: Gran
+  x3: Molt gran
+  x4: Increïblement gran
+  blur: Desenfocament
+  x4Description: Mostra contingut fins i tot més gran que gran que gran.
+  rainbowDescription: Fa que el contingut aparegui en colors de l'arc de Sant Martí.
+  sparkle: Brillantor
+  sparkleDescription: Dóna al contingut un efecte de partícula brillant.
+  rotate: Girar
+  rotateDescription: Gira el contingut en un angle especificat.
+  positionDescription: Mou el contingut en una quantitat especificada.
+  fontDescription: Estableix el tipus de lletra en què voleu mostrar el contingut.
+  position: Posició
+  rainbow: Arc de Sant Martí
+  jelly: Animació (Jelly)
+  tada: Animació (Tada)
+  tadaDescription: Dóna al contingut una animació tipus "Tada!".
+  jump: Animació (Jump)
+  twitch: Animació (Twitch)
+  blurDescription: Desenfoca el contingut. Es mostrarà clarament quan passeu el cursor
+    per sobre.
+  font: Tipus de lletra
+  cheatSheet: Full de trucs de MFM
+  mentionDescription: Podeu especificar un usuari mitjançant un arrova i un nom d'usuari.
+  center: Centre
+  inlineCodeDescription: Mostra el ressaltat de sintaxi en línia per al codi (de programa).
+  blockMath: Matemàtiques (Bloc)
+  blockMathDescription: Mostra fórmules matemàtiques (KaTeX) en un bloc
+  quoteDescription: Mostra el contingut com una cita.
+  emoji: Emoji personalitzat
+  emojiDescription: Un emoji personalitzat és pot mostrar envoltant el nom amb dos
+    punts.
+  searchDescription: Mostra un quadre de cerca amb el text introduït prèviament.
+  flip: Capgirar
+  flipDescription: Capgira el contingut horitzontalment o verticalment.
 _theme:
   keys:
     mention: "Menció"
@@ -180,10 +250,101 @@ _pages:
         arg1: "Llistes"
       _seedRandomPick:
         arg2: "Llistes"
+        arg1: Llavor
       _pick:
         arg1: "Llistes"
       _listLen:
         arg1: "Llistes"
+      add: Afegir
+      _subtract:
+        arg1: A
+        arg2: B
+      subtract: Restar
+      _round:
+        arg1: Número
+      eq: A i B són iguals
+      _mod:
+        arg2: B
+        arg1: A
+      round: Arrodoniment decimal
+      _and:
+        arg1: A
+        arg2: B
+      or: A O B
+      _or:
+        arg1: A
+        arg2: B
+      lt: < A és menor que B
+      _lt:
+        arg1: A
+        arg2: B
+      gt: '> A és més gran que B'
+      _gt:
+        arg1: A
+        arg2: B
+      seedRannum: Nombre aleatori (amb llavor)
+      _seedRannum:
+        arg1: Llavor
+        arg2: Valor mínim
+        arg3: Valor màxim
+      _eq:
+        arg1: A
+        arg2: B
+      ltEq: <= A és menor o igual que B
+      _multiply:
+        arg2: B
+        arg1: A
+      divide: Dividir
+      notEq: A i B són diferents
+      _notEq:
+        arg1: A
+        arg2: B
+      and: A I B
+      _ltEq:
+        arg2: B
+        arg1: A
+      gtEq: '>= A és més gran o igual que B'
+      _gtEq:
+        arg1: A
+        arg2: B
+      if: Branca
+      _if:
+        arg1: Si
+        arg2: Aleshores
+        arg3: Altrament
+      not: NO
+      random: Aleatori
+      _dailyRandom:
+        arg1: Probabilitat
+      dailyRannum: Nombre aleatori (canvia un cop al dia per a cada usuari)
+      _add:
+        arg1: A
+        arg2: B
+      _divide:
+        arg1: A
+        arg2: B
+      mod: Resta
+      _not:
+        arg1: NO
+      _random:
+        arg1: Probabilitat
+      rannum: Nombre aleatori
+      _rannum:
+        arg1: Valor mínim
+        arg2: Valor màxim
+      randomPick: Tria aleatòriament de la llista
+      dailyRandom: Aleatori (canvia un cop al dia per a cada usuari)
+      _dailyRannum:
+        arg2: Valor màxim
+        arg1: Valor mínim
+      dailyRandomPick: Tria aleatòriament d'una llista (Canvis un cop al dia per a
+        cada usuari)
+      seedRandom: Aleatori (amb llavor)
+      _seedRandom:
+        arg1: Llavor
+        arg2: Probabilitat
+      seedRandomPick: Tria aleatòriament de la llista (amb llavor)
+      multiply: Multiplicar
     types:
       array: "Llistes"
 _notification:
@@ -580,7 +741,7 @@ preferencesBackups: Preferències de còpies de seguretat
 undeck: Treure el Deck
 useBlurEffectForModal: Fes servir efectes de difuminació en les finestres modals
 useFullReactionPicker: Fes servir el selector de reaccions a tamany complert
-deck: Deck
+deck: Taulell
 width: Amplada
 generateAccessToken: Genera un token d'accés
 medium: Mitja
@@ -720,3 +881,544 @@ openInSideView: Obrir a la vista lateral
 defaultNavigationBehaviour: Navegació per defecte
 editTheseSettingsMayBreakAccount: Si edites aquestes configuracions pots fer mal bé
   el teu compte.
+userSilenced: Aquest usuari ha sigut silenciat.
+instanceTicker: Informació de notes de l'instància
+waitingFor: Esperant a {x}
+random: Aleatori
+system: Sistema
+switchUi: Interfície d'usuari
+createNewClip: Crear un clip nou
+unclip: Treure clip
+public: Públic
+renotesCount: Nombre de re-notes fetes
+sentReactionsCount: Nombre de reaccions fetes
+receivedReactionsCount: Nombre de reaccions rebudes
+pollVotesCount: Nombre de vots fets en enquestes
+pollVotedCount: Nombre de vots rebuts en enquestes
+yes: Sí
+no: No
+noCrawle: Rebutjar la indexació dels restrejadors
+driveUsage: Espai fet servir al Disk
+noCrawleDescription: No permetre que els buscadors guardin la informació de les pàgines
+  de perfil, notes, Pàgines, etc.
+alwaysMarkSensitive: Marcar per defecte com a NSFW
+lockedAccountInfo: Només si has configurat la visibilitat del compte per "Només seguidors"
+  les teves notes no serem visibles per a ningú, inclús si has d'aprovar els teus
+  seguiments manualment.
+disableShowingAnimatedImages: No reproduir les imatges animades
+verificationEmailSent: S'ha enviat correu electrònic de verificació. Si us plau segueix
+  les instruccions per completar la verificació.
+notSet: Sense especificar
+emailVerified: Correu electrònic enviat
+loadRawImages: Carregar les imatges originals en comptes de mostrar les miniatures
+noteFavoritesCount: Nombre de notes afegides a favorits
+useSystemFont: Fes servir la font per defecte del sistema
+contact: Contacte
+clips: Clips
+experimentalFeatures: Característiques experimentals
+developer: Desenvolupador
+makeExplorableDescription: Si desactives aquesta funció el teu compte no sortirà a
+  la secció "Explora".
+showGapBetweenNotesInTimeline: Mostra un espai entre notes a la línea de temps
+makeExplorable: Fes el compte visible a "Explora"
+duplicate: Duplicar
+left: Esquerra
+wide: Ample
+narrow: Estret
+reloadToApplySetting: Aquesta configuració només sortirà efecte després de recarregar
+  la pàgina. Vols fer-ho ara?
+needReloadToApply: Es requereix recarregar la pàgina perquè això surti efecte.
+showTitlebar: Mostrar la barra de títol
+onlineUsersCount: Hi han {n} usuaris connectats
+nUsers: '{n} Usuaris'
+nNotes: '{n} Notes'
+sendErrorReports: Enviar informe d'error
+clearCache: Netejar memòria cau
+switchAccount: Canvia de compte
+enabled: Activat
+configure: Configurar
+noBotProtectionWarning: La protecció contra bots no està configurada.
+ads: Anuncis
+ratio: Ratio
+global: Global
+sent: Enviat
+received: Rebut
+whatIsNew: Mostra els canvis
+usernameInfo: Un nom que identifica el vostre compte d'altres en aquest servidor.
+  Podeu utilitzar l'alfabet (a~z, A~Z), els dígits (0~9) o el guió baix (_). Els noms
+  d'usuari no es poden canviar més tard.
+breakFollow: Suprimeix el seguidor
+makeReactionsPublicDescription: Això farà que la llista de totes les vostres reaccions
+  passades sigui visible públicament.
+hide: Amagar
+leaveGroupConfirm: Estàs segur que vols deixar "{nom}"?
+voteConfirm: Vols confirmar el teu vot per a "{choice}"?
+leaveGroup: Sortir del grup
+rateLimitExceeded: S'ha excedit el límit proporcionat
+cropImage: Retalla la imatge
+cropImageAsk: Vols retallar aquesta imatge?
+failedToFetchAccountInformation: No s'ha pogut obtenir la informació del compte
+driveCapOverrideCaption: Restableix la capacitat per defecte introduint un valor de
+  0 o inferior.
+type: Tipus
+label: Etiqueta
+beta: Beta
+navbar: Barra de navegació
+adminCustomCssWarn: Aquesta configuració només s'ha d'utilitzar si sabeu què fa. La
+  introducció de valors inadequats pot fer que els clients de TOTS deixin de funcionar
+  amb normalitat. Assegureu-vos que el vostre CSS funcioni correctament provant-lo
+  a la configuració de l'usuari.
+showUpdates: Mostra una finestra emergent quan Calckey s'actualitzi
+recommendedInstances: Instàncies recomanades
+recommendedInstancesDescription: Instàncies recomanades separades per salts de línia
+  per aparèixer a la línia de temps recomanada. NO afegiu `https://`, NOMÉS el domini.
+caption: Descripció Automàtica
+splash: Pantalla de Benvinguda
+swipeOnDesktop: Permet lliscar a l'estil del mòbil a l'escriptori
+updateAvailable: Pot ser que hi hagi una actualització disponible!
+logoImageUrl: URL de la imatge del logotip
+showAdminUpdates: Indica que hi ha disponible una versió nova de Calckey (només per
+  a administradors)
+replayTutorial: Repetició del tutorial
+migration: Migració
+moveAccountDescription: Aquest procés és irreversible. Assegureu-vos que hàgiu configurat
+  un àlies per a aquest compte al vostre compte nou abans de moure's. Introduïu l'etiqueta
+  del compte amb el format @person@instance.com
+moveToLabel: 'Compte al qual us moveu:'
+moveAccount: Mou el compte!
+moveFromDescription: Això establirà un àlies del vostre compte antic perquè pugueu
+  passar d'aquest compte a aquest actual. Feu això ABANS de moure's del vostre compte
+  anterior. Introduïu l'etiqueta del compte amb el format @person@instance.com
+_sensitiveMediaDetection:
+  description: Redueix l'esforç de moderació del servidor mitjançant el reconeixement
+    automàtic dels mitjans NSFW mitjançant l'aprenentatge automàtic. Això augmentarà
+    lleugerament la càrrega al servidor.
+  setSensitiveFlagAutomaticallyDescription: Els resultats de la detecció interna es
+    conservaran encara que aquesta opció estigui desactivada.
+  analyzeVideos: Activa l'anàlisi de vídeos
+  analyzeVideosDescription: Analitza vídeos a més d'imatges. Això augmentarà lleugerament
+    la càrrega al servidor.
+  setSensitiveFlagAutomatically: Marca com a NSFW
+  sensitivity: Sensibilitat de detecció
+  sensitivityDescription: La reducció de la sensibilitat comportarà menys deteccions
+    errònies (falsos positius), mentre que augmentar-la comportarà menys deteccions
+    falses (falsos negatius).
+_emailUnavailable:
+  used: Aquesta adreça de correu electrònic ja s'està utilitzant
+  format: El format d'aquesta adreça de correu electrònic no és vàlid
+  disposable: Les adreces de correu electrònic d'un sol ús no es poden utilitzar
+  mx: Aquest servidor de correu electrònic no és vàlid
+  smtp: Aquest servidor de correu electrònic no respon
+_ffVisibility:
+  public: Públic
+  followers: Visible només per als seguidors
+  private: Privat
+_signup:
+  emailAddressInfo: Introduïu la vostra adreça de correu electrònic. No es farà públic.
+  almostThere: Gairebé està
+  emailSent: S'ha enviat un correu electrònic de confirmació a la vostra adreça electrònica
+    ({email}). Feu clic a l'enllaç inclòs per completar la creació del compte.
+_accountDelete:
+  started: S'ha iniciat la supressió.
+  accountDelete: Suprimeix el compte
+  mayTakeTime: Com que la supressió del compte és un procés que requereix molts recursos,
+    pot ser que trigui algun temps a completar-se en funció de la quantitat de contingut
+    que hàgiu creat i de quants fitxers hàgiu penjat.
+  sendEmail: Un cop s'hagi completat la supressió del compte, s'enviarà un correu
+    electrònic a l'adreça de correu electrònic registrada en aquest compte.
+  inProgress: La supressió del compte està en curs
+  requestAccountDelete: Sol·licitar la supressió del compte
+_ad:
+  back: Enrera
+  reduceFrequencyOfThisAd: Mostrar aquest anunci menys
+_gallery:
+  my: La meva Galeria
+  liked: Notes que m'han agradat
+  unlike: Elimina m'agrada
+  like: M'agrada
+_forgotPassword:
+  contactAdmin: Aquesta instància no admet l'ús d'adreces de correu electrònic; poseu-vos
+    en contacte amb l'administrador de la instància per restablir la contrasenya.
+  ifNoEmail: Si no heu utilitzat cap correu electrònic durant el registre, poseu-vos
+    en contacte amb l'administrador de la instància.
+  enterEmail: Introduïu l'adreça de correu electrònic que heu utilitzat per registrar-vos.
+    A continuació, se li enviarà un enllaç amb el qual podeu restablir la vostra contrasenya.
+_plugin:
+  install: Instal·leu connectors
+  installWarn: Si us plau, no instal·leu connectors que no siguin fiables.
+  manage: Gestionar els connectors
+_preferencesBackups:
+  saveNew: Desa una còpia de seguretat nova
+  apply: Aplicar a aquest dispositiu
+  loadFile: Carrega des del fitxer
+  save: Desa els canvis
+  nameAlreadyExists: Ja existeix una còpia de seguretat anomenada "{name}". Introduïu
+    un nom diferent.
+  renameConfirm: Canviar el nom d'aquesta còpia de seguretat de "{old}" a "{new}"?
+  noBackups: No existeixen còpies de seguretat. Podeu fer una còpia de seguretat de
+    la configuració del vostre client en aquest servidor utilitzant "Crea una còpia
+    de seguretat nova".
+  deleteConfirm: Vols suprimir la còpia de seguretat anomanada {name}?
+  updatedAt: 'Actualitzat el: {time} {date}'
+  createdAt: 'Creat el: {time} {date}'
+  cannotLoad: No s'ha pogut carregar
+  inputName: Introduïu un nom per a aquesta còpia de seguretat
+  saveConfirm: Deseu la còpia de seguretat com a {name}?
+  invalidFile: Format de fitxer no vàlid
+  applyConfirm: Realment voleu aplicar la còpia de seguretat "{name}" a aquest dispositiu?
+    La configuració existent d'aquest dispositiu es sobreescriurà.
+  list: Còpies de seguretat creades
+  cannotSave: S'ha produït un error en desar
+_registry:
+  domain: Domini
+  createKey: Crea la clau
+  scope: Àmbit
+  key: Clau
+  keys: Claus
+silenced: Silenciat
+objectStorageUseSSL: Fes servir SSL
+yourAccountSuspendedTitle: Aquest compte està suspès
+i18nInfo: Calckey està sent traduïts a diversos idiomes per voluntaris. Pots ajudar
+  {link}.
+manageAccessTokens: Administrar tokens d'accés
+accountInfo: Informació del compte
+pageLikedCount: Nombre de m'agrada rebuts a Pàgines
+center: Centre
+registry: Registre
+closeAccount: Tancar el compte
+currentVersion: Versió actual
+latestVersion: Versió més nova
+newVersionOfClientAvailable: Aquesta és la versió del client més nova disponible.
+usageAmount: Ús
+capacity: Capacitat
+editCode: Editar codi
+apply: Aplicar
+repliesCount: Nombre de contestacions fetes
+repliedCount: Nombre de respostes rebudes
+renotedCount: Nombre d'impulsos rebuts
+followingCount: Nombre de comptes seguits
+followersCount: Nombre de seguidors
+goBack: Enrera
+quitFullView: Sortí de la vista complerta
+addDescription: Afegeix una descripció
+notSpecifiedMentionWarning: Aquesta nota conté mencions a usuaris no inclosos com
+  a destinataris
+info: Sobre
+hideOnlineStatus: Amagar l'estat de conexió
+onlineStatus: Estat de conexió
+online: En línea
+offline: Desconectat
+notRecommended: No recomanat
+botProtection: Protecció contra Bots
+instanceBlocking: Bloquejar/Silenciar Federació
+selectAccount: Seleccionar un compte
+disabled: Desactivat
+quickAction: Accions ràpides
+administration: Administració
+switch: Canviar
+gallery: Galeria
+popularPosts: Pàgines populars
+shareWithNote: Comparteix amb una nota
+expiration: Data límit
+memo: Memo
+priority: Prioritat
+high: Alt
+middle: Mitjana
+low: Baixa
+emailNotConfiguredWarning: L'adreça de correu electrònic no està definida.
+instanceSecurity: Seguretat de la instància
+privateMode: Mode Privat
+allowedInstances: Instàncies a la llista blanca
+allowedInstancesDescription: Amfitrions d'instàncies a la llista blanca per a la federació,
+  cadascuna separat per una línia nova (només s'aplica en mode privat).
+previewNoteText: Mostra la vista prèvia
+customCss: CSS personalitzat
+recommended: Recomanat
+seperateRenoteQuote: Botons d'impuls i de citació separats
+searchResult: Resultats de la cerca
+hashtags: Etiquetes
+troubleshooting: Resolució de problemes
+learnMore: Aprèn més
+misskeyUpdated: Calckey s'ha actualitzat!
+translate: Tradueix
+translatedFrom: Traduït per {x}
+aiChanMode: Ai-chan a la interfície d'usuari clàssica
+keepCw: Mantenir els avisos de contingut
+pubSub: Comptes Pub/Sub
+lastCommunication: Última comunicació
+breakFollowConfirm: Confirmes que vols eliminar un seguidor?
+itsOn: Activat
+itsOff: Desactivat
+emailRequiredForSignup: Requereix una adreça de correu electrònic per registrar-te
+unread: Sense llegir
+controlPanel: Tauler de control
+manageAccounts: Gestionar comptes
+makeReactionsPublic: Estableix l'historial de reaccions com a públic
+classic: Clàssic
+muteThread: Silenciar el fil
+ffVisibility: Visibilitat dels Seguiments/Seguidors
+incorrectPassword: Contrasenya incorrecta.
+clickToFinishEmailVerification: Feu clic a [{ok}] per completar la verificació del
+  correu electrònic.
+overridedDeviceKind: Tipus de dispositiu
+smartphone: Smartphone
+tablet: Tauleta
+auto: Automàtic
+recentNHours: Últimes {n} hores
+recentNDays: Últims {n} dies
+noEmailServerWarning: El servidor de correu electrònic no està configurat.
+check: Comprovar
+fast: Ràpida
+sensitiveMediaDetection: Detecció de mitjans NSFW
+remoteOnly: Només remotes
+failedToUpload: S'ha produït un error en la càrrega
+cannotUploadBecauseInappropriate: Aquest fitxer no s'ha pogut carregar perquè s'han
+  detectat parts d'aquest com a potencialment NSFW.
+cannotUploadBecauseNoFreeSpace: La pujada ha fallat a causa de la manca de capacitat
+  del Disc.
+enableAutoSensitive: Marcatge automàtic NSFW
+moveTo: Mou el compte actual al compte nou
+customKaTeXMacro: Macros KaTeX personalitzats
+_aboutMisskey:
+  contributors: Col·laboradors principals
+  allContributors: Tots els col·laboradors
+  donate: Fes una donació a Calckey
+  source: Codi font
+  translation: Tradueix Calckey
+  about: Calckey és una bifurcació de Misskey feta per ThatOneCalculator, que està
+    en desenvolupament des del 2022.
+  morePatrons: També agraïm el suport de molts altres ajudants que no figuren aquí.
+    Gràcies! 🥰
+  patrons: Mecenes de Calckey
+unknown: Desconegut
+pageLikesCount: Nombre de pàgines amb M'agrada
+youAreRunningUpToDateClient: Estás fent servir la versió del client més nova.
+unlikeConfirm: Vols treure el teu m'agrada?
+fullView: Vista complerta
+desktop: Escritori
+notesCount: Nombre de notes
+confirmToUnclipAlreadyClippedNote: Aquesta nota ja és al clip "{name}". Vols treure'l
+  d'aquest clip?
+driveFilesCount: Nombre de fitxers el Disk
+silencedInstances: Instàncies silenciades
+silenceThisInstance: Silencia la instància
+silencedInstancesDescription: Llista amb els noms de les instàncies que vols silenciar.
+  Les comptes en les instàncies silenciades seran tractades com "Silenciades", només
+  poden fer sol·licitud de seguiments, i no poden mencionar comptes locals si no les
+  segueixen. Això no afectarà les instàncies silenciades.
+objectStorageEndpointDesc: Deixa això buit si fas servir AWS, S3, d'una altre manera
+  específica un "endpoint" com a '<host>' o '<host>:<port>', depend del proveïdor
+  que facis servir.
+objectStorageRegionDesc: Especifica una regió com a 'xx-east-1'. Si el teu proveïdor
+  no distingeix entre regions, deixa això en buit o pots escriure 'us-east-1'.
+userPagePinTip: Pots mostrar notes aquí escollint "Pin al perfil" dintre del menú
+  de cada nota.
+userInfo: Informació d'usuari
+hideOnlineStatusDescription: Amagant el teu estat en línea redueix la comoditat d'ús
+  d'algunes característiques com ara la recerca.
+active: Actiu
+accounts: Comptes
+postToGallery: Crea una nova nota a la galeria
+secureMode: Mode segur (Recuperació Autoritzada)
+customCssWarn: Aquesta configuració només s'ha d'utilitzar si sabeu què fa. La introducció
+  de valors indeguts pot provocar que el client deixi de funcionar amb normalitat.
+squareAvatars: Mostra avatars quadrats
+secureModeInfo: Quan sol·liciteu des d'altres instàncies, no envieu de tornada sense
+  prova.
+privateModeInfo: Quan està activat, només les instàncies de la llista blanca es poden
+  federar amb les vostres instàncies. Totes les publicacions s'amagaran al públic.
+useBlurEffect: Utilitzeu efectes de desenfocament a la interfície d'usuari
+accountDeletionInProgress: La supressió del compte està en curs
+unmuteThread: Desfés el silenci al fil
+deleteAccountConfirm: Això suprimirà el vostre compte de manera irreversible. Procedir?
+requireAdminForView: Heu d'iniciar sessió amb un compte d'administrador per veure-ho.
+enableAutoSensitiveDescription: Permet la detecció i el marcatge automàtics dels mitjans
+  NSFW mitjançant Machine Learning sempre que sigui possible. Fins i tot si aquesta
+  opció està desactivada, és possible que estigui habilitada a tota la instància.
+localOnly: Només local
+customKaTeXMacroDescription: "Configura macros per escriure expressions matemàtiques\
+  \ fàcilment! La notació s'ajusta a les definicions de l'ordre LaTeX i s'escriu com\
+  \ a \\newcommand{\\name}{content} o \\newcommand{\\name}[nombre d'arguments]{contingut}.\
+  \ Per exemple, \\newcommand{\\add}[2]{#1 + #2} ampliarà \\add{3}{foo} a 3 + foo.\
+  \ Els claudàtors que envolten el nom de la macro es poden canviar per claudàtors\
+  \ rodons o quadrats. Això afecta els claudàtors utilitzats per als arguments. Es\
+  \ pot definir una (i només una) macro per línia, i no podeu trencar la línia al\
+  \ mig de la definició. Les línies no vàlides simplement s'ignoren. Només s'admeten\
+  \ funcions de substitució de cadenes senzilles; La sintaxi avançada, com ara la\
+  \ ramificació condicional, no es pot utilitzar aquí."
+objectStorageRegion: Regió
+objectStoragePrefix: Prefix
+objectStoragePrefixDesc: Els fitxers es guardaran dins de carpetes amb aquest prefix.
+objectStorageEndpoint: Endpoint
+newNoteRecived: Hi han notes noves
+sounds: Sons
+listen: Escoltar
+none: Res
+showInPage: Mostrar a la página
+popout: Apareixa
+volume: Volum
+objectStorageUseSSLDesc: Desactiva això si no fas servir HTTP per les connexions API
+objectStorageUseProxy: Conectarse mitjançant un Proxy
+objectStorageUseProxyDesc: Desactiva això si no faràs servir un servidor Proxy per
+  conexions API
+objectStorageSetPublicRead: Fixar com a "public-read" al pujar
+serverLogs: Registres del servidor
+deleteAll: Esborrar tot
+showFixedPostForm: Mostrar el formulari de publicació al principi de la línea de temps
+unableToProcess: Aquesta operació no es pot acabar
+recentUsed: Fet servir fa poc
+install: Instal·lar
+masterVolume: Volum principal
+uninstall: Desinstal·lar
+installedApps: Aplicacions autoritzades
+nothing: No hi a res per veure
+installedDate: Data d'autorització
+details: Detalls
+chooseEmoji: Selecciona un emoji
+removeAllFollowingDescription: Fent això deixes de seguir tots els comptes de {host}.
+  Si us plau fes servir això sí, per exemple, l'instància deixa d'existir.
+userSuspended: Aquest usuari ha sigut suspès.
+lastUsedDate: Data d'últim ús
+state: Estat
+sort: Ordenar
+ascendingOrder: Ascendent
+descendingOrder: Descendent
+scratchpad: Bloc de notes
+scratchpadDescription: El bloc de notes proporciona un entorn per experiments amb
+  AiScript. Pots escriure, executar i comprovar els resultats interactuant amb Calckey.
+output: Sortida
+script: Script
+disablePagesScript: Desactivar AiScript a les pàgines
+updateRemoteUser: Actualitzar la informació de l'usuari remot
+deleteAllFiles: Esborrar tots els fitxers
+deleteAllFilesConfirm: Segur que vols esborrar tots els fitxers?
+removeAllFollowing: Deixar de seguir a tots els usuaris
+accentColor: Color principal
+textColor: Color del text
+value: Valor
+sendErrorReportsDescription: "Quant està activat, es compartirà amb els desenvolupadors\
+  \ de Calckey quant aparegui un problema quan ajudarà a millorar la qualitat.\nAixò\
+  \ inclourà informació com la versió del teu sistema operatiu, el navegador que estiguis\
+  \ fent servir, la teva activitat a Calckey, etc."
+myTheme: El meu tema
+backgroundColor: Color de fons
+saveAs: Desa com...
+advanced: Avançat
+invalidValue: Valor invàlid.
+createdAt: Dada de creació
+updatedAt: Data d'actualització
+saveConfirm: Desa canvis?
+deleteConfirm: De veritat ho vols esborrar?
+receiveAnnouncementFromInstance: Rep notificacions d'aquesta instància
+emailNotification: Notificacions per correu electrònic
+publish: Publicar
+inChannelSearch: Buscar al canal
+useReactionPickerForContextMenu: Obrir el selector de reaccions al fer click esquerra
+typingUsers: L'{users} està escrivint
+oneDay: Un dia
+instanceDefaultLightTheme: Tema de llum predeterminat per a tota la instància
+instanceDefaultDarkTheme: Tema fosc predeterminat per a tota la instància
+instanceDefaultThemeDescription: Introduïu el codi del tema en format d'objecte.
+mutePeriod: Durada del silenci
+indefinitely: Permanentment
+tenMinutes: 10 minuts
+oneHour: Una hora
+oneWeek: Una setmana
+reflectMayTakeTime: Pot trigar una mica a reflectir-se.
+thereIsUnresolvedAbuseReportWarning: Hi ha informes sense resoldre.
+driveCapOverrideLabel: Canvieu la capacitat del disc per a aquest usuari
+isSystemAccount: Un compte creat i operat automàticament pel sistema.
+typeToConfirm: Introduïu {x} per confirmar
+deleteAccount: Suprimeix el compte
+document: Documentació
+sendPushNotificationReadMessage: Suprimeix les notificacions push un cop s'hagin llegit
+  les notificacions o missatges rellevants
+sendPushNotificationReadMessageCaption: Es mostrarà una notificació amb el text "{emptyPushNotificationMessage}"
+  durant un breu temps. Això pot augmentar l'ús de la bateria del vostre dispositiu,
+  si escau.
+showAds: Mostrar anuncis
+enterSendsMessage: Pren retorn al formulari del missatge per enviar (quant no s'activa
+  es Ctrl + Return)
+customMOTD: MOTD personalitzat (missatges de la pantalla d'inici)
+customMOTDDescription: Missatges personalitzats per al MOTD (pantalla de presentació)
+  separats per salts de línia es mostraran aleatòriament cada vegada que un usuari
+  carrega/recarrega la pàgina.
+customSplashIcons: Icones personalitzades de la pantalla d'inici (urls)
+customSplashIconsDescription: La URL de les icones de pantalla de presentació personalitzades
+  separades per salts de línia es mostraran aleatòriament cada vegada que un usuari
+  carrega/recarrega la pàgina. Si us plau, assegureu-vos que les imatges estiguin
+  en una URL estàtica, preferiblement totes a la mida de 192 x 192.
+moveFrom: Mou a aquest compte des d'un compte anterior
+moveFromLabel: 'Compte des del qual us moveu:'
+migrationConfirm: "Esteu absolutament segur que voleu migrar el vostre compte a {account}?\
+  \ Un cop ho feu, no podreu revertir-ho i no podreu tornar a utilitzar el vostre\
+  \ compte amb normalitat.\nA més, assegureu-vos d'haver configurat aquest compte\
+  \ actual com el compte del qual us moveu."
+defaultReaction: Reacció d'emoji predeterminada per a notes sortints i entrants
+enableCustomKaTeXMacro: Activa les macros KaTeX personalitzades
+noteId: ID de la nota
+_nsfw:
+  respect: Amaga els mitjans NSFW
+  ignore: No amagueu els mitjans NSFW
+  force: Amaga tots els mitjans
+inUse: Utilitzat
+ffVisibilityDescription: Et permet configurar qui pot veure a qui segueixes i qui
+  et segueix.
+continueThread: Continuar el fil
+reverse: Revés
+objectStorageBucket: Cubell
+objectStorageBucketDesc: Si us plau específica el nom del cubell que faràs servir
+  al teu proveïdor.
+clip: Clip
+createNew: Crear una nova
+optional: Opcional
+jumpToSpecifiedDate: Vés a una data concreta
+showingPastTimeline: Ara es mostra un línea de temps antiga
+clear: Tornar
+markAllAsRead: Marcar tot com a llegit
+recentPosts: Pàgines recents
+noMaintainerInformationWarning: La informació del responsable no està configurada.
+resolved: Resolt
+unresolved: Sense resoldre
+filter: Filtre
+slow: Lenta
+useDrawerReactionPickerForMobile: Mostra el selector de reaccions com a calaix al
+  mòbil
+welcomeBackWithName: Benvingut de nou, {name}
+showLocalPosts: 'Mostra les notes locals a:'
+homeTimeline: Línea de temps Local
+socialTimeline: Línea de temps Social
+themeColor: Color del Ticker de la instància
+size: Mida
+numberOfColumn: Nombre de columnes
+numberOfPageCache: Nombre de pàgines emmagatzemades a la memòria cau
+numberOfPageCacheDescription: L'augment d'aquest nombre millorarà la comoditat dels
+  usuaris, però provocarà més càrrega del servidor i més memòria per utilitzar-la.
+logoutConfirm: Vols tancar la sessió?
+lastActiveDate: Data d'últim ús
+statusbar: Barra d'estat
+pleaseSelect: Selecciona una opció
+colored: Color
+refreshInterval: "Interval d'actualització "
+speed: Velocitat
+cannotUploadBecauseExceedsFileSizeLimit: Aquest fitxer no s'ha pogut carregar perquè
+  supera la mida màxima permesa.
+activeEmailValidationDescription: Permet una validació més estricta de les adreces
+  de correu electrònic, que inclou la comprovació d'adreces d'un sol ús i si realment
+  es pot comunicar amb elles. Quan no està marcat, només es valida el format del correu
+  electrònic.
+shuffle: Barrejar
+account: Compte
+move: Moure
+pushNotification: Notificacions push
+subscribePushNotification: Activar les notificacions push
+unsubscribePushNotification: Desactivar les notificacions push
+pushNotificationAlreadySubscribed: Les notificacions push ja estan activades
+pushNotificationNotSupported: El vostre navegador o instància no admet notificacions
+  automàtiques
+license: Llicència
+indexPosts: Índex de notes
+indexFrom: Índex a partir de l'identificador de notes (deixeu en blanc per indexar
+  cada publicació)
+indexNotice: Ara indexant. Això probablement trigarà una estona, si us plau, no reinicieu
+  el servidor durant almenys una hora.