From 8c3974b97bfb778484aae060c58fbdbe1f137d36 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 17 Jul 2022 21:06:33 +0900
Subject: [PATCH 01/24] enhance(client): tweak ui

---
 packages/client/src/components/launch-pad.vue |  36 ----
 .../client/src/components/ui/child-menu.vue   |  63 ++++++
 packages/client/src/components/ui/menu.vue    | 187 +++++++++++++-----
 .../client/src/components/ui/popup-menu.vue   |   2 +-
 packages/client/src/components/ui/tooltip.vue | 158 ++-------------
 packages/client/src/scripts/popup-position.ts | 158 +++++++++++++++
 packages/client/src/types/menu.ts             |   7 +-
 packages/client/src/ui/_common_/common.vue    |  66 +++----
 .../src/ui/_common_/navbar-for-mobile.vue     |  30 +++
 packages/client/src/ui/_common_/navbar.vue    |  30 +++
 10 files changed, 462 insertions(+), 275 deletions(-)
 create mode 100644 packages/client/src/components/ui/child-menu.vue
 create mode 100644 packages/client/src/scripts/popup-position.ts

diff --git a/packages/client/src/components/launch-pad.vue b/packages/client/src/components/launch-pad.vue
index 4693df291..7891f61bf 100644
--- a/packages/client/src/components/launch-pad.vue
+++ b/packages/client/src/components/launch-pad.vue
@@ -15,20 +15,6 @@
 				</MkA>
 			</template>
 		</div>
-		<div class="sub">
-			<button v-click-anime class="_button" @click="help">
-				<i class="fas fa-question-circle icon"></i>
-				<div class="text">{{ $ts.help }}</div>
-			</button>
-			<MkA v-click-anime to="/about" @click.passive="close()">
-				<i class="fas fa-info-circle icon"></i>
-				<div class="text">{{ $ts.instanceInfo }}</div>
-			</MkA>
-			<MkA v-click-anime to="/about-misskey" @click.passive="close()">
-				<img src="/static-assets/favicon.png" class="icon"/>
-				<div class="text">{{ $ts.aboutMisskey }}</div>
-			</MkA>
-		</div>
 	</div>
 </MkModal>
 </template>
@@ -74,28 +60,6 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
 function close() {
 	modal.close();
 }
-
-function help(ev: MouseEvent) {
-	os.popupMenu([{
-		type: 'link',
-		to: '/mfm-cheat-sheet',
-		text: i18n.ts._mfm.cheatSheet,
-		icon: 'fas fa-code',
-	}, {
-		type: 'link',
-		to: '/scratchpad',
-		text: i18n.ts.scratchpad,
-		icon: 'fas fa-terminal',
-	}, null, {
-		text: i18n.ts.document,
-		icon: 'fas fa-question-circle',
-		action: () => {
-			window.open('https://misskey-hub.net/help.html', '_blank');
-		},
-	}], ev.currentTarget ?? ev.target);
-
-	close();
-}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/ui/child-menu.vue b/packages/client/src/components/ui/child-menu.vue
new file mode 100644
index 000000000..a0c26b50c
--- /dev/null
+++ b/packages/client/src/components/ui/child-menu.vue
@@ -0,0 +1,63 @@
+<template>
+<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>
+import { on } from 'events';
+import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue';
+import MkMenu from './menu.vue';
+import { MenuItem } from '@/types/menu';
+import * as os from '@/os';
+
+const props = defineProps<{
+	items: MenuItem[];
+	targetElement: HTMLElement;
+	width?: number;
+	viaKeyboard?: boolean;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+	(ev: 'actioned'): void;
+}>();
+
+const el = ref<HTMLElement>();
+const align = 'left';
+
+function setPosition() {
+	const rect = props.targetElement.getBoundingClientRect();
+	const left = rect.left + props.targetElement.offsetWidth;
+	const top = rect.top - 8;
+	el.value.style.left = left + 'px';
+	el.value.style.top = top + 'px';
+}
+
+function onChildClosed(actioned?: boolean) {
+	if (actioned) {
+		emit('actioned');
+	} else {
+		emit('closed');
+	}
+}
+
+onMounted(() => {
+	setPosition();
+	nextTick(() => {
+		setPosition();
+	});
+});
+
+defineExpose({
+	checkHit: (ev: MouseEvent) => {
+		return (ev.target === el.value || el.value.contains(ev.target));
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.sfhdhdhr {
+	position: fixed;
+}
+</style>
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index 6ad63c2ad..26283ffe5 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -1,55 +1,67 @@
 <template>
-<div
-	ref="itemsEl" v-hotkey="keymap"
-	class="rrevdjwt"
-	: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>
+<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>{{ 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="fa-fw" :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="fas fa-circle"></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="fa-fw" :class="item.icon"></i>
+				<span>{{ item.text }}</span>
+				<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></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)">
+				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
+				<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></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">{{ 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="fa-fw" :class="item.icon"></i>
+				<span>{{ item.text }}</span>
+				<span class="caret"><i class="fas fa-caret-right fa-fw"></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)">
+				<i v-if="item.icon" class="fa-fw" :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="fas fa-circle"></i></span>
+			</button>
+		</template>
+		<span v-if="items2.length === 0" class="none item">
+			<span>{{ $ts.none }}</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()">
-			<i v-if="item.icon" class="fa-fw" :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="fas fa-circle"></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()">
-			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
-			<span>{{ item.text }}</span>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></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)">
-			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</button>
-		<span v-else-if="item.type === 'switch'" :tabindex="i" class="item">
-			<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
-		</span>
-		<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
-			<i v-if="item.icon" class="fa-fw" :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="fas fa-circle"></i></span>
-		</button>
-	</template>
-	<span v-if="items2.length === 0" class="none item">
-		<span>{{ $ts.none }}</span>
-	</span>
+	</div>
+	<div v-if="childMenu" class="child">
+		<XChild ref="child" :items="childMenu" :target-element="childTarget" showing @actioned="childActioned"/>
+	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, watch } from 'vue';
+import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
 import { focusPrev, focusNext } from '@/scripts/focus';
 import FormSwitch from '@/components/form/switch.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
+import * as os from '@/os';
+const XChild = defineAsyncComponent(() => import('./child-menu.vue'));
 
 const props = defineProps<{
 	items: MenuItem[];
@@ -61,19 +73,23 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: 'close'): void;
+	(ev: 'close', actioned?: boolean): void;
 }>();
 
 let itemsEl = $ref<HTMLDivElement>();
 
 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, () => {
 	const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
 
@@ -93,21 +109,53 @@ watch(() => props.items, () => {
 	immediate: true,
 });
 
-onMounted(() => {
-	if (props.viaKeyboard) {
-		nextTick(() => {
-			focusNext(itemsEl.children[0], true, false);
-		});
+let childMenu = $ref<MenuItem[] | null>();
+let childTarget = $ref<HTMLElement | null>();
+
+function closeChild() {
+	childMenu = null;
+	childShowingItem = null;
+}
+
+function childActioned() {
+	closeChild();
+	close(true);
+}
+
+function onGlobalMousedown(event: MouseEvent) {
+	if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
+	if (child && child.checkHit(event)) return;
+	closeChild();
+}
+
+let childCloseTimer: null | number = null;
+function onItemMouseEnter(item) {
+	childCloseTimer = window.setTimeout(() => {
+		closeChild();
+	}, 300);
+}
+function onItemMouseLeave(item) {
+	if (childCloseTimer) window.clearTimeout(childCloseTimer);
+}
+
+async function showChildren(item: MenuItem, ev: MouseEvent) {
+	if (props.asDrawer) {
+		os.popupMenu(item.children, ev.currentTarget ?? ev.target);
+		close();
+	} else {
+		childTarget = ev.currentTarget ?? ev.target;
+		childMenu = item.children;
+		childShowingItem = item;
 	}
-});
+}
 
 function clicked(fn: MenuAction, ev: MouseEvent) {
 	fn(ev);
-	close();
+	close(true);
 }
 
-function close() {
-	emit('close');
+function close(actioned = false) {
+	emit('close', actioned);
 }
 
 function focusUp() {
@@ -117,6 +165,20 @@ function focusUp() {
 function focusDown() {
 	focusNext(document.activeElement);
 }
+
+onMounted(() => {
+	if (props.viaKeyboard) {
+		nextTick(() => {
+			focusNext(itemsEl.children[0], true, false);
+		});
+	}
+
+	document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
+});
+
+onBeforeUnmount(() => {
+	document.removeEventListener('mousedown', onGlobalMousedown);
+});
 </script>
 
 <style lang="scss" scoped>
@@ -225,6 +287,25 @@ function focusDown() {
 			opacity: 0.7;
 		}
 
+		&.parent {
+			display: flex;
+			align-items: center;
+			cursor: default;
+
+			> .caret {
+				margin-left: auto;
+			}
+
+			&.childShowing {
+				color: var(--accent);
+				text-decoration: none;
+
+				&:before {
+					background: var(--accentedBg);
+				}
+			}
+		}
+
 		> i {
 			margin-right: 5px;
 			width: 20px;
diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue
index 2bc7030d7..c29aff45e 100644
--- a/packages/client/src/components/ui/popup-menu.vue
+++ b/packages/client/src/components/ui/popup-menu.vue
@@ -1,6 +1,6 @@
 <template>
 <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
-	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
+	<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/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue
index f81bf2fc5..4c6258d24 100644
--- a/packages/client/src/components/ui/tooltip.vue
+++ b/packages/client/src/components/ui/tooltip.vue
@@ -12,6 +12,7 @@
 <script lang="ts" setup>
 import { nextTick, onMounted, onUnmounted, ref } from 'vue';
 import * as os from '@/os';
+import { calcPopupPosition } from '@/scripts/popup-position';
 
 const props = withDefaults(defineProps<{
 	showing: boolean;
@@ -36,151 +37,20 @@ const emit = defineEmits<{
 const el = ref<HTMLElement>();
 const zIndex = os.claimZIndex('high');
 
-const setPosition = () => {
-	if (el.value == null) return;
+function setPosition() {
+	const data = calcPopupPosition(el.value, {
+		anchorElement: props.targetElement,
+		direction: props.direction,
+		align: 'center',
+		innerMargin: props.innerMargin,
+		x: props.x,
+		y: props.y,
+	});
 
-	const contentWidth = el.value.offsetWidth;
-	const contentHeight = el.value.offsetHeight;
-
-	let rect: DOMRect;
-
-	if (props.targetElement) {
-		rect = props.targetElement.getBoundingClientRect();
-	}
-
-	const calcPosWhenTop = () => {
-		let left: number;
-		let top: number;
-
-		if (props.targetElement) {
-			left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
-			top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
-		} else {
-			left = props.x;
-			top = (props.y - contentHeight) - props.innerMargin;
-		}
-
-		left -= (el.value.offsetWidth / 2);
-
-		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
-			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenBottom = () => {
-		let left: number;
-		let top: number;
-
-		if (props.targetElement) {
-			left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
-			top = (rect.top + window.pageYOffset + props.targetElement.offsetHeight) + props.innerMargin;
-		} else {
-			left = props.x;
-			top = (props.y) + props.innerMargin;
-		}
-
-		left -= (el.value.offsetWidth / 2);
-
-		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
-			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenLeft = () => {
-		let left: number;
-		let top: number;
-
-		if (props.targetElement) {
-			left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
-			top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
-		} else {
-			left = (props.x - contentWidth) - props.innerMargin;
-			top = props.y;
-		}
-
-		top -= (el.value.offsetHeight / 2);
-
-		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
-			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenRight = () => {
-		let left: number;
-		let top: number;
-
-		if (props.targetElement) {
-			left = (rect.left + props.targetElement.offsetWidth + window.pageXOffset) + props.innerMargin;
-			top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
-		} else {
-			left = props.x + props.innerMargin;
-			top = props.y;
-		}
-
-		top -= (el.value.offsetHeight / 2);
-
-		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
-			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calc = (): {
-		left: number;
-		top: number;
-		transformOrigin: string;
-	} => {
-		switch (props.direction) {
-			case 'top': {
-				const [left, top] = calcPosWhenTop();
-
-				// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
-				if (top - window.pageYOffset < 0) {
-					const [left, top] = calcPosWhenBottom();
-					return { left, top, transformOrigin: 'center top' };
-				}
-
-				return { left, top, transformOrigin: 'center bottom' };
-			}
-
-			case 'bottom': {
-				const [left, top] = calcPosWhenBottom();
-				// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
-				return { left, top, transformOrigin: 'center top' };
-			}
-
-			case 'left': {
-				const [left, top] = calcPosWhenLeft();
-
-				// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
-				if (left - window.pageXOffset < 0) {
-					const [left, top] = calcPosWhenRight();
-					return { left, top, transformOrigin: 'left center' };
-				}
-
-				return { left, top, transformOrigin: 'right center' };
-			}
-
-			case 'right': {
-				const [left, top] = calcPosWhenRight();
-				// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
-				return { left, top, transformOrigin: 'left center' };
-			}
-		}
-	};
-
-	const { left, top, transformOrigin } = calc();
-	el.value.style.transformOrigin = transformOrigin;
-	el.value.style.left = left + 'px';
-	el.value.style.top = top + 'px';
-};
+	el.value.style.transformOrigin = data.transformOrigin;
+	el.value.style.left = data.left + 'px';
+	el.value.style.top = data.top + 'px';
+}
 
 let loopHandler;
 
diff --git a/packages/client/src/scripts/popup-position.ts b/packages/client/src/scripts/popup-position.ts
new file mode 100644
index 000000000..e84eebf10
--- /dev/null
+++ b/packages/client/src/scripts/popup-position.ts
@@ -0,0 +1,158 @@
+import { Ref } from 'vue';
+
+export function calcPopupPosition(el: HTMLElement, props: {
+	anchorElement: HTMLElement | null;
+	innerMargin: number;
+	direction: 'top' | 'bottom' | 'left' | 'right';
+	align: 'top' | 'bottom' | 'left' | 'right' | 'center';
+	alignOffset?: number;
+	x?: number;
+	y?: number;
+}): { top: number; left: number; transformOrigin: string; } {
+	const contentWidth = el.offsetWidth;
+	const contentHeight = el.offsetHeight;
+
+	let rect: DOMRect;
+
+	if (props.anchorElement) {
+		rect = props.anchorElement.getBoundingClientRect();
+	}
+
+	const calcPosWhenTop = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
+			top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
+		} else {
+			left = props.x;
+			top = (props.y - contentHeight) - props.innerMargin;
+		}
+
+		left -= (el.offsetWidth / 2);
+
+		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
+			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenBottom = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
+			top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin;
+		} else {
+			left = props.x;
+			top = (props.y) + props.innerMargin;
+		}
+
+		left -= (el.offsetWidth / 2);
+
+		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
+			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenLeft = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
+			top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
+		} else {
+			left = (props.x - contentWidth) - props.innerMargin;
+			top = props.y;
+		}
+
+		top -= (el.offsetHeight / 2);
+
+		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
+			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenRight = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin;
+
+			if (props.align === 'top') {
+				top = rect.top + window.pageYOffset;
+				if (props.alignOffset != null) top += props.alignOffset;
+			} else if (props.align === 'bottom') {
+				// TODO
+			} else { // center
+				top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
+				top -= (el.offsetHeight / 2);
+			}
+		} else {
+			left = props.x + props.innerMargin;
+			top = props.y;
+			top -= (el.offsetHeight / 2);
+		}
+
+		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
+			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calc = (): {
+		left: number;
+		top: number;
+		transformOrigin: string;
+	} => {
+		switch (props.direction) {
+			case 'top': {
+				const [left, top] = calcPosWhenTop();
+
+				// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
+				if (top - window.pageYOffset < 0) {
+					const [left, top] = calcPosWhenBottom();
+					return { left, top, transformOrigin: 'center top' };
+				}
+
+				return { left, top, transformOrigin: 'center bottom' };
+			}
+
+			case 'bottom': {
+				const [left, top] = calcPosWhenBottom();
+				// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
+				return { left, top, transformOrigin: 'center top' };
+			}
+
+			case 'left': {
+				const [left, top] = calcPosWhenLeft();
+
+				// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
+				if (left - window.pageXOffset < 0) {
+					const [left, top] = calcPosWhenRight();
+					return { left, top, transformOrigin: 'left center' };
+				}
+
+				return { left, top, transformOrigin: 'right center' };
+			}
+
+			case 'right': {
+				const [left, top] = calcPosWhenRight();
+				// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
+				return { left, top, transformOrigin: 'left center' };
+			}
+		}
+	};
+
+	return calc();
+}
diff --git a/packages/client/src/types/menu.ts b/packages/client/src/types/menu.ts
index ed67e6ab8..972f6db21 100644
--- a/packages/client/src/types/menu.ts
+++ b/packages/client/src/types/menu.ts
@@ -11,10 +11,11 @@ export type MenuA = { type: 'a', href: string, target?: string, download?: strin
 export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
 export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
 export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
+export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] };
 
 export type MenuPending = { type: 'pending' };
 
-type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
-type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>;
+type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent;
+type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent>;
 export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
-export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
+export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent;
diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue
index 9f7388db5..f32cd3fe0 100644
--- a/packages/client/src/ui/_common_/common.vue
+++ b/packages/client/src/ui/_common_/common.vue
@@ -1,5 +1,6 @@
 <template>
-<component :is="popup.component"
+<component
+	:is="popup.component"
 	v-for="popup in popups"
 	:key="popup.id"
 	v-bind="popup.props"
@@ -15,56 +16,45 @@
 <div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
+import { swInject } from './sw-inject';
 import { popup, popups, pendingApiRequestsCount } from '@/os';
 import { uploads } from '@/scripts/upload';
 import * as sound from '@/scripts/sound';
 import { $i } from '@/account';
-import { swInject } from './sw-inject';
 import { stream } from '@/stream';
 
-export default defineComponent({
-	components: {
-		XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
-		XUpload: defineAsyncComponent(() => import('./upload.vue')),
-	},
+const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
+const XUpload = defineAsyncComponent(() => import('./upload.vue'));
 
-	setup() {
-		const onNotification = notification => {
-			if ($i.mutingNotificationTypes.includes(notification.type)) return;
+const dev = _DEV_;
 
-			if (document.visibilityState === 'visible') {
-				stream.send('readNotification', {
-					id: notification.id
-				});
+const onNotification = notification => {
+	if ($i.mutingNotificationTypes.includes(notification.type)) return;
 
-				popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
-					notification
-				}, {}, 'closed');
-			}
+	if (document.visibilityState === 'visible') {
+		stream.send('readNotification', {
+			id: notification.id,
+		});
 
-			sound.play('notification');
-		};
+		popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
+			notification,
+		}, {}, 'closed');
+	}
 
-		if ($i) {
-			const connection = stream.useChannel('main', null, 'UI');
-			connection.on('notification', onNotification);
+	sound.play('notification');
+};
 
-			//#region Listen message from SW
-			if ('serviceWorker' in navigator) {
-				swInject();
-			}
-		}
+if ($i) {
+	const connection = stream.useChannel('main', null, 'UI');
+	connection.on('notification', onNotification);
 
-		return {
-			uploads,
-			popups,
-			pendingApiRequestsCount,
-			dev: _DEV_,
-		};
-	},
-});
+	//#region Listen message from SW
+	if ('serviceWorker' in navigator) {
+		swInject();
+	}
+}
 </script>
 
 <style lang="scss">
diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue
index d1b4c30b3..f2521cfc7 100644
--- a/packages/client/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue
@@ -87,6 +87,36 @@ function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.federation,
 		icon: 'fas fa-globe',
 		to: '/about#federation',
+	}, null, {
+		type: 'parent',
+		text: i18n.ts.help,
+		icon: 'fas fa-question-circle',
+		children: [{
+			type: 'link',
+			to: '/mfm-cheat-sheet',
+			text: i18n.ts._mfm.cheatSheet,
+			icon: 'fas fa-code',
+		}, {
+			type: 'link',
+			to: '/scratchpad',
+			text: i18n.ts.scratchpad,
+			icon: 'fas fa-terminal',
+		}, {
+			type: 'link',
+			to: '/api-console',
+			text: 'API Console',
+			icon: 'fas fa-terminal',
+		}, null, {
+			text: i18n.ts.document,
+			icon: 'fas fa-question-circle',
+			action: () => {
+				window.open('https://misskey-hub.net/help.html', '_blank');
+			},
+		}],
+	}, {
+		type: 'link',
+		text: i18n.ts.aboutMisskey,
+		to: '/about-misskey',
 	}], ev.currentTarget ?? ev.target, {
 		align: 'left',
 	});
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index e18f89113..7e6065c30 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -110,6 +110,36 @@ function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.federation,
 		icon: 'fas fa-globe',
 		to: '/about#federation',
+	}, null, {
+		type: 'parent',
+		text: i18n.ts.help,
+		icon: 'fas fa-question-circle',
+		children: [{
+			type: 'link',
+			to: '/mfm-cheat-sheet',
+			text: i18n.ts._mfm.cheatSheet,
+			icon: 'fas fa-code',
+		}, {
+			type: 'link',
+			to: '/scratchpad',
+			text: i18n.ts.scratchpad,
+			icon: 'fas fa-terminal',
+		}, {
+			type: 'link',
+			to: '/api-console',
+			text: 'API Console',
+			icon: 'fas fa-terminal',
+		}, null, {
+			text: i18n.ts.document,
+			icon: 'fas fa-question-circle',
+			action: () => {
+				window.open('https://misskey-hub.net/help.html', '_blank');
+			},
+		}],
+	}, {
+		type: 'link',
+		text: i18n.ts.aboutMisskey,
+		to: '/about-misskey',
 	}], ev.currentTarget ?? ev.target, {
 		align: 'left',
 	});

From 64377234147431500942fc3f5eba702f4ac4544e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 17 Jul 2022 23:18:05 +0900
Subject: [PATCH 02/24] enhance(client): tweak ui

---
 locales/ja-JP.yml                             |  1 +
 packages/client/src/account.ts                | 17 +++---
 packages/client/src/components/ui/button.vue  |  2 +-
 .../client/src/components/ui/context-menu.vue |  4 +-
 .../ui/{child-menu.vue => menu.child.vue}     |  8 +--
 packages/client/src/components/ui/menu.vue    |  4 +-
 .../client/src/pages/settings/profile.vue     |  4 +-
 packages/client/src/ui/deck/column.vue        | 53 ++++++++++---------
 8 files changed, 50 insertions(+), 43 deletions(-)
 rename packages/client/src/components/ui/{child-menu.vue => menu.child.vue} (85%)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 21615a093..e071b4bda 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -890,6 +890,7 @@ activeEmailValidationDescription: "ユーザーのメールアドレスのバリ
 navbar: "ナビゲーションバー"
 shuffle: "シャッフル"
 account: "アカウント"
+move: "移動"
 
 _sensitiveMediaDetection:
   description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
index 38f2ee4b3..243aea68c 100644
--- a/packages/client/src/account.ts
+++ b/packages/client/src/account.ts
@@ -206,17 +206,16 @@ export async function openAccountMenu(opts: {
 			to: `/@${ $i.username }`,
 			avatar: $i,
 		}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
+			type: 'parent',
 			icon: 'fas fa-plus',
 			text: i18n.ts.addAccount,
-			action: () => {
-				popupMenu([{
-					text: i18n.ts.existingAccount,
-					action: () => { showSigninDialog(); },
-				}, {
-					text: i18n.ts.createAccount,
-					action: () => { createAccount(); },
-				}], ev.currentTarget ?? ev.target);
-			},
+			children: [{
+				text: i18n.ts.existingAccount,
+				action: () => { showSigninDialog(); },
+			}, {
+				text: i18n.ts.createAccount,
+				action: () => { createAccount(); },
+			}],
 		}, {
 			type: 'link',
 			icon: 'fas fa-users',
diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue
index 5f5d6d42e..d3a4b5ea9 100644
--- a/packages/client/src/components/ui/button.vue
+++ b/packages/client/src/components/ui/button.vue
@@ -46,7 +46,7 @@ export default defineComponent({
 		rounded: {
 			type: Boolean,
 			required: false,
-			default: false,
+			default: true,
 		},
 		inline: {
 			type: Boolean,
diff --git a/packages/client/src/components/ui/context-menu.vue b/packages/client/src/components/ui/context-menu.vue
index e637d361c..165c3db46 100644
--- a/packages/client/src/components/ui/context-menu.vue
+++ b/packages/client/src/components/ui/context-menu.vue
@@ -1,16 +1,16 @@
 <template>
 <transition :name="$store.state.animation ? 'fade' : ''" appear>
 	<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
-		<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/>
+		<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
 	</div>
 </transition>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, onBeforeUnmount } from 'vue';
-import contains from '@/scripts/contains';
 import MkMenu from './menu.vue';
 import { MenuItem } from './types/menu.vue';
+import contains from '@/scripts/contains';
 import * as os from '@/os';
 
 const props = defineProps<{
diff --git a/packages/client/src/components/ui/child-menu.vue b/packages/client/src/components/ui/menu.child.vue
similarity index 85%
rename from packages/client/src/components/ui/child-menu.vue
rename to packages/client/src/components/ui/menu.child.vue
index a0c26b50c..b67224d3e 100644
--- a/packages/client/src/components/ui/child-menu.vue
+++ b/packages/client/src/components/ui/menu.child.vue
@@ -14,6 +14,7 @@ import * as os from '@/os';
 const props = defineProps<{
 	items: MenuItem[];
 	targetElement: HTMLElement;
+	rootElement: HTMLElement;
 	width?: number;
 	viaKeyboard?: boolean;
 }>();
@@ -27,9 +28,10 @@ const el = ref<HTMLElement>();
 const align = 'left';
 
 function setPosition() {
+	const rootRect = props.rootElement.getBoundingClientRect();
 	const rect = props.targetElement.getBoundingClientRect();
-	const left = rect.left + props.targetElement.offsetWidth;
-	const top = rect.top - 8;
+	const left = props.targetElement.offsetWidth;
+	const top = (rect.top - rootRect.top) - 8;
 	el.value.style.left = left + 'px';
 	el.value.style.top = top + 'px';
 }
@@ -58,6 +60,6 @@ defineExpose({
 
 <style lang="scss" scoped>
 .sfhdhdhr {
-	position: fixed;
+	position: absolute;
 }
 </style>
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index 26283ffe5..6d1a2cc77 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -50,7 +50,7 @@
 		</span>
 	</div>
 	<div v-if="childMenu" class="child">
-		<XChild ref="child" :items="childMenu" :target-element="childTarget" showing @actioned="childActioned"/>
+		<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
 	</div>
 </div>
 </template>
@@ -61,7 +61,7 @@ import { focusPrev, focusNext } from '@/scripts/focus';
 import FormSwitch from '@/components/form/switch.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
 import * as os from '@/os';
-const XChild = defineAsyncComponent(() => import('./child-menu.vue'));
+const XChild = defineAsyncComponent(() => import('./menu.child.vue'));
 
 const props = defineProps<{
 	items: MenuItem[];
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index f30b0ccbd..20c51c883 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -3,9 +3,9 @@
 	<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
 		<div class="avatar _acrylic">
 			<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
-			<MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
+			<MkButton primary rounded class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
 		</div>
-		<MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
+		<MkButton primary rounded class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
 	</div>
 
 	<FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue
index 1a1bd7d26..7b30ffad4 100644
--- a/packages/client/src/ui/deck/column.vue
+++ b/packages/client/src/ui/deck/column.vue
@@ -128,31 +128,36 @@ function getMenu() {
 			if (canceled) return;
 			updateColumn(props.column.id, result);
 		},
-	}, null, {
-		icon: 'fas fa-arrow-left',
-		text: i18n.ts._deck.swapLeft,
-		action: () => {
-			swapLeftColumn(props.column.id);
-		},
 	}, {
-		icon: 'fas fa-arrow-right',
-		text: i18n.ts._deck.swapRight,
-		action: () => {
-			swapRightColumn(props.column.id);
-		},
-	}, props.isStacked ? {
-		icon: 'fas fa-arrow-up',
-		text: i18n.ts._deck.swapUp,
-		action: () => {
-			swapUpColumn(props.column.id);
-		},
-	} : undefined, props.isStacked ? {
-		icon: 'fas fa-arrow-down',
-		text: i18n.ts._deck.swapDown,
-		action: () => {
-			swapDownColumn(props.column.id);
-		},
-	} : undefined, null, {
+		type: 'parent',
+		text: i18n.ts.move + '...',
+		icon: 'fas fa-arrows-up-down-left-right',
+		children: [{
+			icon: 'fas fa-arrow-left',
+			text: i18n.ts._deck.swapLeft,
+			action: () => {
+				swapLeftColumn(props.column.id);
+			},
+		}, {
+			icon: 'fas fa-arrow-right',
+			text: i18n.ts._deck.swapRight,
+			action: () => {
+				swapRightColumn(props.column.id);
+			},
+		}, props.isStacked ? {
+			icon: 'fas fa-arrow-up',
+			text: i18n.ts._deck.swapUp,
+			action: () => {
+				swapUpColumn(props.column.id);
+			},
+		} : undefined, props.isStacked ? {
+			icon: 'fas fa-arrow-down',
+			text: i18n.ts._deck.swapDown,
+			action: () => {
+				swapDownColumn(props.column.id);
+			},
+		} : undefined],
+	}, {
 		icon: 'fas fa-window-restore',
 		text: i18n.ts._deck.stackLeft,
 		action: () => {

From 3f25cb01fe2d9a89da57da1d7d3fcde2ac4ea390 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 00:18:56 +0900
Subject: [PATCH 03/24] :art:

---
 packages/client/src/store.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index 503333331..3971214af 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -270,7 +270,7 @@ type Plugin = {
  * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
  */
 import lightTheme from '@/themes/l-light.json5';
-import darkTheme from '@/themes/d-dark.json5';
+import darkTheme from '@/themes/d-green-lime.json5';
 
 export class ColdDeviceStorage {
 	public static default = {

From e48579070c9bbd9cdb3c7c1549dbf139dd170447 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 00:31:55 +0900
Subject: [PATCH 04/24] =?UTF-8?q?enhance(client):=20=E3=82=A6=E3=82=A3?=
 =?UTF-8?q?=E3=83=B3=E3=83=89=E3=82=A6=E3=82=92=E6=9C=80=E5=A4=A7=E5=8C=96?=
 =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                 |   9 +
 packages/client/src/components/ui/window.vue | 586 ++++++++++---------
 2 files changed, 316 insertions(+), 279 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfaf06ea8..aa253d11a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,15 @@
 You should also include the user name that made the change.
 -->
 
+## 12.x.x (unreleased)
+
+### Improvements
+- Client: ウィンドウを最大化できるように @syuilo
+- Client: UIのブラッシュアップ @syuilo
+
+### Bugfixes
+- 
+
 ## 12.116.1 (2022/07/17)
 
 ### Bugfixes
diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index d15503382..120861917 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -1,6 +1,6 @@
 <template>
 <transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
-	<div v-if="showing" class="ebkgocck">
+	<div v-if="showing" ref="rootEl" class="ebkgocck">
 		<div class="body _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
 			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
 				<span class="left">
@@ -11,6 +11,8 @@
 				</span>
 				<span class="right">
 					<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
+					<button v-if="canResize && maximized" class="button _button" @click="unMaximize()"><i class="fas fa-window-restore"></i></button>
+					<button v-else-if="canResize && !maximized" class="button _button" @click="maximize()"><i class="fas fa-window-maximize"></i></button>
 					<button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button>
 				</span>
 			</div>
@@ -32,15 +34,16 @@
 </transition>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onBeforeUnmount, onMounted, provide } from 'vue';
 import contains from '@/scripts/contains';
 import * as os from '@/os';
+import { MenuItem } from '@/types/menu';
 
 const minHeight = 50;
 const minWidth = 250;
 
-function dragListen(fn) {
+function dragListen(fn: (ev: MouseEvent) => void) {
 	window.addEventListener('mousemove', fn);
 	window.addEventListener('touchmove', fn);
 	window.addEventListener('mouseleave', dragClear.bind(null, fn));
@@ -56,315 +59,340 @@ function dragClear(fn) {
 	window.removeEventListener('touchend', dragClear);
 }
 
-export default defineComponent({
-	provide: {
-		inWindow: true,
-	},
+const props = withDefaults(defineProps<{
+	initialWidth?: number;
+	initialHeight?: number | null;
+	canResize?: boolean;
+	closeButton?: boolean;
+	mini?: boolean;
+	front?: boolean;
+	contextmenu?: MenuItem[] | null;
+	buttonsLeft?: any[];
+	buttonsRight?: any[];
+}>(), {
+	initialWidth: 400,
+	initialHeight: null,
+	canResize: false,
+	closeButton: true,
+	mini: false,
+	front: true,
+	contextmenu: null,
+	buttonsLeft: () => [],
+	buttonsRight: () => [],
+});
 
-	props: {
-		initialWidth: {
-			type: Number,
-			required: false,
-			default: 400,
-		},
-		initialHeight: {
-			type: Number,
-			required: false,
-			default: null,
-		},
-		canResize: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		closeButton: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		mini: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		front: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		contextmenu: {
-			type: Array,
-			required: false,
-		},
-		buttonsLeft: {
-			type: Array,
-			required: false,
-			default: () => [],
-		},
-		buttonsRight: {
-			type: Array,
-			required: false,
-			default: () => [],
-		},
-	},
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+}>();
 
-	emits: ['closed'],
+provide('inWindow', true);
 
-	data() {
-		return {
-			showing: true,
-			id: Math.random().toString(), // TODO: UUIDとかにする
-		};
-	},
+let rootEl = $ref<HTMLElement>();
+let showing = $ref(true);
+let beforeClickedAt = 0;
+let maximized = $ref(false);
+let unMaximizedTop = '';
+let unMaximizedLeft = '';
+let unMaximizedWidth = '';
+let unMaximizedHeight = '';
 
-	mounted() {
-		if (this.initialWidth) this.applyTransformWidth(this.initialWidth);
-		if (this.initialHeight) this.applyTransformHeight(this.initialHeight);
+function close() {
+	showing = false;
+}
 
-		this.applyTransformTop((window.innerHeight / 2) - (this.$el.offsetHeight / 2));
-		this.applyTransformLeft((window.innerWidth / 2) - (this.$el.offsetWidth / 2));
+function onKeydown(evt) {
+	if (evt.which === 27) { // Esc
+		evt.preventDefault();
+		evt.stopPropagation();
+		close();
+	}
+}
 
-		// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
-		this.top();
+function onContextmenu(ev: MouseEvent) {
+	if (props.contextmenu) {
+		os.contextMenu(props.contextmenu, ev);
+	}
+}
 
-		window.addEventListener('resize', this.onBrowserResize);
-	},
+// 最前面へ移動
+function top() {
+	rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
+}
 
-	unmounted() {
-		window.removeEventListener('resize', this.onBrowserResize);
-	},
+function maximize() {
+	maximized = true;
+	unMaximizedTop = rootEl.style.top;
+	unMaximizedLeft = rootEl.style.left;
+	unMaximizedWidth = rootEl.style.width;
+	unMaximizedHeight = rootEl.style.height;
+	rootEl.style.top = '0';
+	rootEl.style.left = '0';
+	rootEl.style.width = '100%';
+	rootEl.style.height = '100%';
+}
 
-	methods: {
-		close() {
-			this.showing = false;
-		},
+function unMaximize() {
+	maximized = false;
+	rootEl.style.top = unMaximizedTop;
+	rootEl.style.left = unMaximizedLeft;
+	rootEl.style.width = unMaximizedWidth;
+	rootEl.style.height = unMaximizedHeight;
+}
 
-		onKeydown(evt) {
-			if (evt.which === 27) { // Esc
-				evt.preventDefault();
-				evt.stopPropagation();
-				this.close();
+function onBodyMousedown() {
+	top();
+}
+
+function onDblClick() {
+	maximize();
+}
+
+function onHeaderMousedown(evt: MouseEvent) {
+	// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
+	if (evt.button === 2) return;
+
+	let beforeMaximized = false;
+
+	if (maximized) {
+		beforeMaximized = true;
+		unMaximize();
+	}
+
+	// ダブルクリック判定
+	if (Date.now() - beforeClickedAt < 300) {
+		beforeClickedAt = Date.now();
+		onDblClick();
+		return;
+	}
+
+	beforeClickedAt = Date.now();
+
+	const main = rootEl;
+
+	if (!contains(main, document.activeElement)) main.focus();
+
+	const position = main.getBoundingClientRect();
+
+	const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
+	const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
+	const moveBaseX = beforeMaximized ? parseInt(unMaximizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる
+	const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
+	const browserWidth = window.innerWidth;
+	const browserHeight = window.innerHeight;
+	const windowWidth = main.offsetWidth;
+	const windowHeight = main.offsetHeight;
+
+	function move(x: number, y: number) {
+		let moveLeft = x - moveBaseX;
+		let moveTop = y - moveBaseY;
+
+		// 下はみ出し
+		if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
+
+		// 左はみ出し
+		if (moveLeft < 0) moveLeft = 0;
+
+		// 上はみ出し
+		if (moveTop < 0) moveTop = 0;
+
+		// 右はみ出し
+		if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
+
+		rootEl.style.left = moveLeft + 'px';
+		rootEl.style.top = moveTop + 'px';
+	}
+
+	if (beforeMaximized) {
+		move(clickX, clickY);
+	}
+
+	// 動かした時
+	dragListen(me => {
+		const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
+		const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
+
+		move(x, y);
+	});
+}
+
+// 上ハンドル掴み時
+function onTopHandleMousedown(evt) {
+	const main = rootEl;
+
+	const base = evt.clientY;
+	const height = parseInt(getComputedStyle(main, '').height, 10);
+	const top = parseInt(getComputedStyle(main, '').top, 10);
+
+	// 動かした時
+	dragListen(me => {
+		const move = me.clientY - base;
+		if (top + move > 0) {
+			if (height + -move > minHeight) {
+				applyTransformHeight(height + -move);
+				applyTransformTop(top + move);
+			} else { // 最小の高さより小さくなろうとした時
+				applyTransformHeight(minHeight);
+				applyTransformTop(top + (height - minHeight));
 			}
-		},
+		} else { // 上のはみ出し時
+			applyTransformHeight(top + height);
+			applyTransformTop(0);
+		}
+	});
+}
 
-		onContextmenu(ev: MouseEvent) {
-			if (this.contextmenu) {
-				os.contextMenu(this.contextmenu, ev);
+// 右ハンドル掴み時
+function onRightHandleMousedown(evt) {
+	const main = rootEl;
+
+	const base = evt.clientX;
+	const width = parseInt(getComputedStyle(main, '').width, 10);
+	const left = parseInt(getComputedStyle(main, '').left, 10);
+	const browserWidth = window.innerWidth;
+
+	// 動かした時
+	dragListen(me => {
+		const move = me.clientX - base;
+		if (left + width + move < browserWidth) {
+			if (width + move > minWidth) {
+				applyTransformWidth(width + move);
+			} else { // 最小の幅より小さくなろうとした時
+				applyTransformWidth(minWidth);
 			}
-		},
+		} else { // 右のはみ出し時
+			applyTransformWidth(browserWidth - left);
+		}
+	});
+}
 
-		// 最前面へ移動
-		top() {
-			(this.$el as any).style.zIndex = os.claimZIndex(this.front ? 'middle' : 'low');
-		},
+// 下ハンドル掴み時
+function onBottomHandleMousedown(evt) {
+	const main = rootEl;
 
-		onBodyMousedown() {
-			this.top();
-		},
+	const base = evt.clientY;
+	const height = parseInt(getComputedStyle(main, '').height, 10);
+	const top = parseInt(getComputedStyle(main, '').top, 10);
+	const browserHeight = window.innerHeight;
 
-		onHeaderMousedown(evt: MouseEvent) {
-			// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
-			if (evt.button === 2) return;
+	// 動かした時
+	dragListen(me => {
+		const move = me.clientY - base;
+		if (top + height + move < browserHeight) {
+			if (height + move > minHeight) {
+				applyTransformHeight(height + move);
+			} else { // 最小の高さより小さくなろうとした時
+				applyTransformHeight(minHeight);
+			}
+		} else { // 下のはみ出し時
+			applyTransformHeight(browserHeight - top);
+		}
+	});
+}
 
-			const main = this.$el as any;
+// 左ハンドル掴み時
+function onLeftHandleMousedown(evt) {
+	const main = rootEl;
 
-			if (!contains(main, document.activeElement)) main.focus();
+	const base = evt.clientX;
+	const width = parseInt(getComputedStyle(main, '').width, 10);
+	const left = parseInt(getComputedStyle(main, '').left, 10);
 
-			const position = main.getBoundingClientRect();
+	// 動かした時
+	dragListen(me => {
+		const move = me.clientX - base;
+		if (left + move > 0) {
+			if (width + -move > minWidth) {
+				applyTransformWidth(width + -move);
+				applyTransformLeft(left + move);
+			} else { // 最小の幅より小さくなろうとした時
+				applyTransformWidth(minWidth);
+				applyTransformLeft(left + (width - minWidth));
+			}
+		} else { // 左のはみ出し時
+			applyTransformWidth(left + width);
+			applyTransformLeft(0);
+		}
+	});
+}
 
-			const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
-			const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
-			const moveBaseX = clickX - position.left;
-			const moveBaseY = clickY - position.top;
-			const browserWidth = window.innerWidth;
-			const browserHeight = window.innerHeight;
-			const windowWidth = main.offsetWidth;
-			const windowHeight = main.offsetHeight;
+// 左上ハンドル掴み時
+function onTopLeftHandleMousedown(evt) {
+	onTopHandleMousedown(evt);
+	onLeftHandleMousedown(evt);
+}
 
-			// 動かした時
-			dragListen(me => {
-				const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
-				const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
+// 右上ハンドル掴み時
+function onTopRightHandleMousedown(evt) {
+	onTopHandleMousedown(evt);
+	onRightHandleMousedown(evt);
+}
 
-				let moveLeft = x - moveBaseX;
-				let moveTop = y - moveBaseY;
+// 右下ハンドル掴み時
+function onBottomRightHandleMousedown(evt) {
+	onBottomHandleMousedown(evt);
+	onRightHandleMousedown(evt);
+}
 
-				// 下はみ出し
-				if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
+// 左下ハンドル掴み時
+function onBottomLeftHandleMousedown(evt) {
+	onBottomHandleMousedown(evt);
+	onLeftHandleMousedown(evt);
+}
 
-				// 左はみ出し
-				if (moveLeft < 0) moveLeft = 0;
+// 高さを適用
+function applyTransformHeight(height) {
+	if (height > window.innerHeight) height = window.innerHeight;
+	rootEl.style.height = height + 'px';
+}
 
-				// 上はみ出し
-				if (moveTop < 0) moveTop = 0;
+// 幅を適用
+function applyTransformWidth(width) {
+	if (width > window.innerWidth) width = window.innerWidth;
+	rootEl.style.width = width + 'px';
+}
 
-				// 右はみ出し
-				if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
+// Y座標を適用
+function applyTransformTop(top) {
+	rootEl.style.top = top + 'px';
+}
 
-				this.$el.style.left = moveLeft + 'px';
-				this.$el.style.top = moveTop + 'px';
-			});
-		},
+// X座標を適用
+function applyTransformLeft(left) {
+	rootEl.style.left = left + 'px';
+}
 
-		// 上ハンドル掴み時
-		onTopHandleMousedown(evt) {
-			const main = this.$el as any;
+function onBrowserResize() {
+	const main = rootEl;
+	const position = main.getBoundingClientRect();
+	const browserWidth = window.innerWidth;
+	const browserHeight = window.innerHeight;
+	const windowWidth = main.offsetWidth;
+	const windowHeight = main.offsetHeight;
+	if (position.left < 0) main.style.left = '0'; // 左はみ出し
+	if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
+	if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
+	if (position.top < 0) main.style.top = '0'; // 上はみ出し
+}
 
-			const base = evt.clientY;
-			const height = parseInt(getComputedStyle(main, '').height, 10);
-			const top = parseInt(getComputedStyle(main, '').top, 10);
+onMounted(() => {
+	if (props.initialWidth) applyTransformWidth(props.initialWidth);
+	if (props.initialHeight) applyTransformHeight(props.initialHeight);
 
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientY - base;
-				if (top + move > 0) {
-					if (height + -move > minHeight) {
-						this.applyTransformHeight(height + -move);
-						this.applyTransformTop(top + move);
-					} else { // 最小の高さより小さくなろうとした時
-						this.applyTransformHeight(minHeight);
-						this.applyTransformTop(top + (height - minHeight));
-					}
-				} else { // 上のはみ出し時
-					this.applyTransformHeight(top + height);
-					this.applyTransformTop(0);
-				}
-			});
-		},
+	applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2));
+	applyTransformLeft((window.innerWidth / 2) - (rootEl.offsetWidth / 2));
 
-		// 右ハンドル掴み時
-		onRightHandleMousedown(evt) {
-			const main = this.$el as any;
+	// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
+	top();
 
-			const base = evt.clientX;
-			const width = parseInt(getComputedStyle(main, '').width, 10);
-			const left = parseInt(getComputedStyle(main, '').left, 10);
-			const browserWidth = window.innerWidth;
+	window.addEventListener('resize', onBrowserResize);
+});
 
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientX - base;
-				if (left + width + move < browserWidth) {
-					if (width + move > minWidth) {
-						this.applyTransformWidth(width + move);
-					} else { // 最小の幅より小さくなろうとした時
-						this.applyTransformWidth(minWidth);
-					}
-				} else { // 右のはみ出し時
-					this.applyTransformWidth(browserWidth - left);
-				}
-			});
-		},
+onBeforeUnmount(() => {
+	window.removeEventListener('resize', onBrowserResize);
+});
 
-		// 下ハンドル掴み時
-		onBottomHandleMousedown(evt) {
-			const main = this.$el as any;
-
-			const base = evt.clientY;
-			const height = parseInt(getComputedStyle(main, '').height, 10);
-			const top = parseInt(getComputedStyle(main, '').top, 10);
-			const browserHeight = window.innerHeight;
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientY - base;
-				if (top + height + move < browserHeight) {
-					if (height + move > minHeight) {
-						this.applyTransformHeight(height + move);
-					} else { // 最小の高さより小さくなろうとした時
-						this.applyTransformHeight(minHeight);
-					}
-				} else { // 下のはみ出し時
-					this.applyTransformHeight(browserHeight - top);
-				}
-			});
-		},
-
-		// 左ハンドル掴み時
-		onLeftHandleMousedown(evt) {
-			const main = this.$el as any;
-
-			const base = evt.clientX;
-			const width = parseInt(getComputedStyle(main, '').width, 10);
-			const left = parseInt(getComputedStyle(main, '').left, 10);
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientX - base;
-				if (left + move > 0) {
-					if (width + -move > minWidth) {
-						this.applyTransformWidth(width + -move);
-						this.applyTransformLeft(left + move);
-					} else { // 最小の幅より小さくなろうとした時
-						this.applyTransformWidth(minWidth);
-						this.applyTransformLeft(left + (width - minWidth));
-					}
-				} else { // 左のはみ出し時
-					this.applyTransformWidth(left + width);
-					this.applyTransformLeft(0);
-				}
-			});
-		},
-
-		// 左上ハンドル掴み時
-		onTopLeftHandleMousedown(evt) {
-			this.onTopHandleMousedown(evt);
-			this.onLeftHandleMousedown(evt);
-		},
-
-		// 右上ハンドル掴み時
-		onTopRightHandleMousedown(evt) {
-			this.onTopHandleMousedown(evt);
-			this.onRightHandleMousedown(evt);
-		},
-
-		// 右下ハンドル掴み時
-		onBottomRightHandleMousedown(evt) {
-			this.onBottomHandleMousedown(evt);
-			this.onRightHandleMousedown(evt);
-		},
-
-		// 左下ハンドル掴み時
-		onBottomLeftHandleMousedown(evt) {
-			this.onBottomHandleMousedown(evt);
-			this.onLeftHandleMousedown(evt);
-		},
-
-		// 高さを適用
-		applyTransformHeight(height) {
-			if (height > window.innerHeight) height = window.innerHeight;
-			(this.$el as any).style.height = height + 'px';
-		},
-
-		// 幅を適用
-		applyTransformWidth(width) {
-			if (width > window.innerWidth) width = window.innerWidth;
-			(this.$el as any).style.width = width + 'px';
-		},
-
-		// Y座標を適用
-		applyTransformTop(top) {
-			(this.$el as any).style.top = top + 'px';
-		},
-
-		// X座標を適用
-		applyTransformLeft(left) {
-			(this.$el as any).style.left = left + 'px';
-		},
-
-		onBrowserResize() {
-			const main = this.$el as any;
-			const position = main.getBoundingClientRect();
-			const browserWidth = window.innerWidth;
-			const browserHeight = window.innerHeight;
-			const windowWidth = main.offsetWidth;
-			const windowHeight = main.offsetHeight;
-			if (position.left < 0) main.style.left = 0; // 左はみ出し
-			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
-			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
-			if (position.top < 0) main.style.top = 0; // 上はみ出し
-		},
-	},
+defineExpose({
+	close,
 });
 </script>
 

From 6a5f4349afe14f608c2deb696382e16fde2f7623 Mon Sep 17 00:00:00 2001
From: Ryu jongheon <lptprjh@gmail.com>
Date: Mon, 18 Jul 2022 00:33:12 +0900
Subject: [PATCH 05/24] fix(client): use icon for local if available (#9012)

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

diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue
index c32409ecf..d9f196f88 100644
--- a/packages/client/src/components/instance-ticker.vue
+++ b/packages/client/src/components/instance-ticker.vue
@@ -8,6 +8,7 @@
 <script lang="ts" setup>
 import { } from 'vue';
 import { instanceName } from '@/config';
+import { instance as Instance } from '@/instance';
 
 const props = defineProps<{
 	instance?: {
@@ -19,7 +20,7 @@ const props = defineProps<{
 
 // if no instance data is given, this is for the local instance
 const instance = props.instance ?? {
-	faviconUrl: '/favicon.ico',
+	faviconUrl: Instance.iconUrl || Instance.faviconUrl || '/favicon.ico',
 	name: instanceName,
 	themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
 };

From d4e0d160140e97ba1c8454c86a14b9c5053cc104 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 01:29:29 +0900
Subject: [PATCH 06/24] chore(client): tweak style

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

diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue
index eb889c488..eaebc3541 100644
--- a/packages/client/src/components/reactions-viewer.details.vue
+++ b/packages/client/src/components/reactions-viewer.details.vue
@@ -50,14 +50,14 @@ const emit = defineEmits<{
 		}
 
 		> .name {
-			font-size: 0.9em;
+			font-size: 1em;
 		}
 	}
 
 	> .users {
 		flex: 1;
 		min-width: 0;
-		font-size: 0.9em;
+		font-size: 0.95em;
 		border-left: solid 0.5px var(--divider);
 		padding-left: 10px;
 		margin-left: 10px;

From 85f7100d4106d7eace654d62aec7c6fec637b9fc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 01:37:26 +0900
Subject: [PATCH 07/24] 12.117.0-beta.1

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

diff --git a/package.json b/package.json
index bce4386f3..f91a3aa63 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.116.1",
+	"version": "12.117.0-beta.1",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From b76d76b8f991f0ae5482ae54cf9d8218cbbd6f1f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 05:03:39 +0900
Subject: [PATCH 08/24] enhance(client): tweak ui

---
 CHANGELOG.md                                   |  2 ++
 packages/client/src/components/global/a.vue    | 10 +++++++---
 packages/client/src/components/ui/window.vue   | 16 ++++++++++++----
 packages/client/src/pages/settings/index.vue   |  2 --
 packages/client/src/pages/settings/profile.vue |  3 ++-
 5 files changed, 23 insertions(+), 10 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa253d11a..ca486e640 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ You should also include the user name that made the change.
 
 ### Improvements
 - Client: ウィンドウを最大化できるように @syuilo
+- Client: Shiftキーを押した状態でリンクをクリックするとアプリ内ウィンドウで開くように @syuilo
+- Client: デッキを使用している際、Ctrlキーを押した状態でリンクをクリックするとページ遷移を強制できるように @syuilo
 - Client: UIのブラッシュアップ @syuilo
 
 ### Bugfixes
diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue
index c7cf12e8c..67bf54def 100644
--- a/packages/client/src/components/global/a.vue
+++ b/packages/client/src/components/global/a.vue
@@ -50,7 +50,7 @@ function onContextmenu(ev) {
 		icon: 'fas fa-expand-alt',
 		text: i18n.ts.showInPage,
 		action: () => {
-			router.push(props.to);
+			router.push(props.to, 'forcePage');
 		},
 	}, null, {
 		icon: 'fas fa-external-link-alt',
@@ -79,7 +79,7 @@ function popout() {
 	popout_(props.to);
 }
 
-function nav() {
+function nav(ev: MouseEvent) {
 	if (props.behavior === 'browser') {
 		location.href = props.to;
 		return;
@@ -93,6 +93,10 @@ function nav() {
 		}
 	}
 
-	router.push(props.to);
+	if (ev.shiftKey) {
+		return openWindow();
+	}
+
+	router.push(props.to, ev.ctrlKey ? 'forcePage' : null);
 }
 </script>
diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index 120861917..e259ecdab 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -1,6 +1,6 @@
 <template>
 <transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
-	<div v-if="showing" ref="rootEl" class="ebkgocck">
+	<div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }">
 		<div class="body _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
 			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
 				<span class="left">
@@ -87,7 +87,7 @@ const emit = defineEmits<{
 
 provide('inWindow', true);
 
-let rootEl = $ref<HTMLElement>();
+let rootEl = $ref<HTMLElement | null>();
 let showing = $ref(true);
 let beforeClickedAt = 0;
 let maximized = $ref(false);
@@ -116,7 +116,9 @@ function onContextmenu(ev: MouseEvent) {
 
 // 最前面へ移動
 function top() {
-	rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
+	if (rootEl) {
+		rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
+	}
 }
 
 function maximize() {
@@ -412,7 +414,7 @@ defineExpose({
 	left: 0;
 
 	> .body {
-		overflow: hidden;
+		overflow: clip;
 		display: flex;
 		flex-direction: column;
 		contain: content;
@@ -550,5 +552,11 @@ defineExpose({
 			cursor: nesw-resize;
 		}
 	}
+
+	&.maximized {
+		> .body {
+			border-radius: 0;
+		}
+	}
 }
 </style>
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index f970660a4..8b1cc6c12 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -330,13 +330,11 @@ definePageMetadata(INFO);
 				width: 34%;
 				padding-right: 32px;
 				box-sizing: border-box;
-				overflow: auto;
 			}
 
 			> .main {
 				flex: 1;
 				min-width: 0;
-				overflow: auto;
 			}
 		}
 	}
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index 20c51c883..5bb3273b3 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="_formRoot">
 	<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
-		<div class="avatar _acrylic">
+		<div class="avatar">
 			<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
 			<MkButton primary rounded class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
 		</div>
@@ -187,6 +187,7 @@ definePageMetadata({
 	position: relative;
 	background-size: cover;
 	background-position: center;
+	border: solid 1px var(--divider);
 	border-radius: 10px;
 	overflow: clip;
 

From 916e2b7cbe74ae0d58aec30835574de0a7ec5d72 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 05:04:28 +0900
Subject: [PATCH 09/24] New Crowdin updates (#9009)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)
---
 locales/bn-BD.yml | 1 +
 locales/de-DE.yml | 2 ++
 locales/en-US.yml | 2 ++
 locales/ko-KR.yml | 5 +++++
 locales/ru-RU.yml | 1 +
 locales/vi-VN.yml | 1 +
 locales/zh-CN.yml | 7 +++++--
 locales/zh-TW.yml | 3 ++-
 8 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index f6547646c..f48893507 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -1645,6 +1645,7 @@ _deck:
   alwaysShowMainColumn: "সর্বদা মেইন কলাম দেখান"
   columnAlign: "কলাম সাজান"
   addColumn: "কলাম যুক্ত করুন"
+  configureColumn: "কলাম সেটিংস"
   swapLeft: "বামে সরান"
   swapRight: "ডানে সরান"
   swapUp: "উপরে উঠান"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 1ae93f6c4..1ae3e77b6 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -890,6 +890,7 @@ activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-A
 navbar: "Navigationsleiste"
 shuffle: "Mischen"
 account: "Benutzerkonto"
+move: "Verschieben"
 _sensitiveMediaDetection:
   description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
   sensitivity: "Erkennungssensitivität"
@@ -1696,6 +1697,7 @@ _deck:
   alwaysShowMainColumn: "Hauptspalte immer zeigen"
   columnAlign: "Spaltenausrichtung"
   addColumn: "Spalte hinzufügen"
+  configureColumn: "Spalteneinstellungen"
   swapLeft: "Mit linker Spalte tauschen"
   swapRight: "Mit rechter Spalte tauschen"
   swapUp: "Mit oberer Spalte tauschen"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index a2ded5a45..92c85507d 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -890,6 +890,7 @@ activeEmailValidationDescription: "Enables stricter validation of email addresse
 navbar: "Navigation bar"
 shuffle: "Shuffle"
 account: "Account"
+move: "Move"
 _sensitiveMediaDetection:
   description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
   sensitivity: "Detection sensitivity"
@@ -1696,6 +1697,7 @@ _deck:
   alwaysShowMainColumn: "Always show main column"
   columnAlign: "Align columns"
   addColumn: "Add column"
+  configureColumn: "Column settings"
   swapLeft: "Swap with the left column"
   swapRight: "Swap with the right column"
   swapUp: "Swap with the above column"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 2ba05505c..7959a3dc3 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -886,6 +886,8 @@ beta: "베타"
 enableAutoSensitive: "자동 NSFW 탐지"
 enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 인스턴스 정책에 따라 자동으로 설정될 수 있습니다."
 activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다."
+navbar: "네비게이션 바"
+shuffle: "셔플"
 account: "계정"
 _sensitiveMediaDetection:
   description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
@@ -1692,6 +1694,7 @@ _deck:
   alwaysShowMainColumn: "메인 칼럼 항상 표시"
   columnAlign: "칼럼 정렬"
   addColumn: "칼럼 추가"
+  configureColumn: "칼럼 설정"
   swapLeft: "왼쪽으로 이동"
   swapRight: "오른쪽으로 이동"
   swapUp: "위로 이동"
@@ -1699,6 +1702,8 @@ _deck:
   stackLeft: "왼쪽에 쌓기"
   popRight: "오른쪽으로 빼기"
   profile: "프로파일"
+  newProfile: "새 프로파일"
+  deleteProfile: "프로파일 삭제"
   introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!"
   introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다."
   widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index b0be6b4ff..a22cacca3 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -1637,6 +1637,7 @@ _deck:
   alwaysShowMainColumn: "Всегда показывать главную колонку"
   columnAlign: "Выравнивание колонок"
   addColumn: "Добавить колонку"
+  configureColumn: "Настройки колонок"
   swapLeft: "Переставить левее"
   swapRight: "Переставить правее"
   swapUp: "Переставить выше"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index 90aa1da30..4cc1c37f3 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -1696,6 +1696,7 @@ _deck:
   alwaysShowMainColumn: "Luôn hiện cột chính"
   columnAlign: "Căn cột"
   addColumn: "Thêm cột"
+  configureColumn: "Cài đặt cột"
   swapLeft: "Hoán đổi với cột bên trái"
   swapRight: "Hoán đổi với cột bên phải"
   swapUp: "Hoán đổi với cột trên"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index e27b02081..8c6c4b358 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -862,7 +862,7 @@ typeToConfirm: "输入 {x} 以确认操作。"
 deleteAccount: "删除账户"
 document: "文档"
 numberOfPageCache: "缓存页数"
-numberOfPageCacheDescription: "设置较高的值会更方便用户,但服务器负载和内存使用量会增加。"
+numberOfPageCacheDescription: "设置较高的值会更方便用户,但设备的负载和内存使用量会增加。"
 logoutConfirm: "是否确认登出?"
 lastActiveDate: "最后活跃时间"
 statusbar: "状态栏"
@@ -1691,13 +1691,16 @@ _deck:
   alwaysShowMainColumn: "总是显示主列"
   columnAlign: "列对齐"
   addColumn: "添加列"
+  configureColumn: "列设置"
   swapLeft: "向左移动"
   swapRight: "向右移动"
   swapUp: "向上移动"
   swapDown: "向下移动"
   stackLeft: "向左折叠"
   popRight: "向右弹出"
-  profile: "个人资料"
+  profile: "配置文件"
+  newProfile: "新建配置文件"
+  deleteProfile: "删除配置文件"
   _columns:
     main: "主列"
     widgets: "小工具"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 4ee440c07..a79ec2fed 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -155,7 +155,7 @@ searchWith: "搜尋: {q}"
 youHaveNoLists: "你沒有任何清單"
 followConfirm: "你真的要追隨{name}嗎?"
 proxyAccount: "代理帳戶"
-proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者關注該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶關注。"
+proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者追蹤該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶追蹤。"
 host: "主機"
 selectUser: "選取使用者"
 recipient: "收件人"
@@ -1695,6 +1695,7 @@ _deck:
   alwaysShowMainColumn: "總是顯示主欄"
   columnAlign: "對齊欄位"
   addColumn: "新增欄位"
+  configureColumn: "欄位的設定"
   swapLeft: "向左移動"
   swapRight: "向右移動"
   swapUp: "往上移動"

From aa2d520708ca1631104bff362d7de637951a631b Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 05:06:50 +0900
Subject: [PATCH 10/24] 12.117.0

---
 CHANGELOG.md | 5 +----
 package.json | 2 +-
 2 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca486e640..69bb5ba89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@
 You should also include the user name that made the change.
 -->
 
-## 12.x.x (unreleased)
+## 12.117.0 (2022/07/18)
 
 ### Improvements
 - Client: ウィンドウを最大化できるように @syuilo
@@ -17,9 +17,6 @@ You should also include the user name that made the change.
 - Client: デッキを使用している際、Ctrlキーを押した状態でリンクをクリックするとページ遷移を強制できるように @syuilo
 - Client: UIのブラッシュアップ @syuilo
 
-### Bugfixes
-- 
-
 ## 12.116.1 (2022/07/17)
 
 ### Bugfixes
diff --git a/package.json b/package.json
index f91a3aa63..2699b7dc9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.117.0-beta.1",
+	"version": "12.117.0",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 321b6f3977bf7e33f80f6032d905034cbf7ca7d6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 05:08:13 +0900
Subject: [PATCH 11/24] lint fix

---
 packages/client/src/pages/registry.value.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/pages/registry.value.vue b/packages/client/src/pages/registry.value.vue
index 34253cc93..9deb31e4a 100644
--- a/packages/client/src/pages/registry.value.vue
+++ b/packages/client/src/pages/registry.value.vue
@@ -74,7 +74,7 @@ function fetchValue() {
 async function save() {
 	try {
 		JSON5.parse(valueForEditor);
-	} catch (e) {
+	} catch (err) {
 		os.alert({
 			type: 'error',
 			text: i18n.ts.invalidValue,

From d0a1018b48c7c2290a3b838532a5c4a15618b7f5 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 18:41:07 +0900
Subject: [PATCH 12/24] update vite

---
 packages/client/package.json |  4 ++--
 packages/client/yarn.lock    | 16 ++++++++--------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/packages/client/package.json b/packages/client/package.json
index 94f6688dd..31c760489 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -15,7 +15,7 @@
 		"@rollup/plugin-alias": "3.1.9",
 		"@rollup/plugin-json": "4.1.0",
 		"@syuilo/aiscript": "0.11.1",
-		"@vitejs/plugin-vue": "3.0.0",
+		"@vitejs/plugin-vue": "3.0.1",
 		"@vue/compiler-sfc": "3.2.37",
 		"abort-controller": "3.0.0",
 		"autobind-decorator": "2.4.0",
@@ -73,7 +73,7 @@
 		"uuid": "8.3.2",
 		"v-debounce": "0.1.2",
 		"vanilla-tilt": "1.7.2",
-		"vite": "3.0.0",
+		"vite": "3.0.1",
 		"vue": "3.2.37",
 		"vue-prism-editor": "2.0.0-alpha.2",
 		"vuedraggable": "4.0.1",
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index 3cef5b87c..0be2fa310 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -600,10 +600,10 @@
   resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
   integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
 
-"@vitejs/plugin-vue@3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.0.tgz#7081e2b3fbe04e291bb85107b9fb57a1fa5e6aeb"
-  integrity sha512-yWP34ArFh/jAeNUDkkLz/kVRLjf5ppJiq4L36f64Cp6dIrMQeYZGDP9xxdemlXfZR9ylN9JgHUl3GzfqOtgYDg==
+"@vitejs/plugin-vue@3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.1.tgz#b6af8f782485374bbb5fe09edf067a845bf4caae"
+  integrity sha512-Ll9JgxG7ONIz/XZv3dssfoMUDu9qAnlJ+km+pBA0teYSXzwPCIzS/e1bmwNYl5dcQGs677D21amgfYAnzMl17A==
 
 "@vue/compiler-core@3.2.37":
   version "3.2.37"
@@ -4222,10 +4222,10 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
-vite@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.0.tgz#b4675cb9ab83ec0803b9c952ffa6519bcce033a7"
-  integrity sha512-M7phQhY3+fRZa0H+1WzI6N+/onruwPTBTMvaj7TzgZ0v2TE+N2sdLKxJOfOv9CckDWt5C4HmyQP81xB4dwRKzA==
+vite@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.1.tgz#9c30c9662ec24455b8834d0b831c9f3c5a9139ca"
+  integrity sha512-nefKSglkoEsDpYUkBuT2++L04ktcP8fz8dxLtmZdDdMyhubFSOLFw6BTh/46Fc6tIX/cibs/NVYWNrsqn0k6pQ==
   dependencies:
     esbuild "^0.14.47"
     postcss "^8.4.14"

From a04e735d5d9cb9e93f3b9ee015d524a68a875e3a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Jul 2022 18:41:17 +0900
Subject: [PATCH 13/24] chore(client): tweak style

---
 packages/client/src/ui/_common_/navbar.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 7e6065c30..30837ed28 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -156,7 +156,7 @@ function more(ev: MouseEvent) {
 <style lang="scss" scoped>
 .mvcprjjd {
 	$nav-width: 250px;
-	$nav-icon-only-width: 86px;
+	$nav-icon-only-width: 80px;
 
 	flex: 0 0 $nav-width;
 	width: $nav-width;

From 18486552620ccfc4d1bf83a0c11eb54c1065dcaf Mon Sep 17 00:00:00 2001
From: xianon <xianon@hotmail.co.jp>
Date: Mon, 18 Jul 2022 21:43:31 +0900
Subject: [PATCH 14/24] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?=
 =?UTF-8?q?=E6=83=85=E5=A0=B1=E3=81=AE=E5=8F=96=E5=BE=97=E3=81=AE=E5=86=8D?=
 =?UTF-8?q?=E8=A9=A6=E8=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=99=E3=82=8B?=
 =?UTF-8?q?=20(#9017)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/client/src/pages/user/followers.vue | 2 +-
 packages/client/src/pages/user/following.vue | 2 +-
 packages/client/src/pages/user/index.vue     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/pages/user/followers.vue b/packages/client/src/pages/user/followers.vue
index 296a4b7b4..b61b48329 100644
--- a/packages/client/src/pages/user/followers.vue
+++ b/packages/client/src/pages/user/followers.vue
@@ -6,7 +6,7 @@
 			<div v-if="user">
 				<XFollowList :user="user" type="followers"/>
 			</div>
-			<MkError v-else-if="error" @retry="fetch()"/>
+			<MkError v-else-if="error" @retry="fetchUser()"/>
 			<MkLoading v-else/>
 		</transition>
 	</MkSpacer>
diff --git a/packages/client/src/pages/user/following.vue b/packages/client/src/pages/user/following.vue
index d1753fe7d..a23977b42 100644
--- a/packages/client/src/pages/user/following.vue
+++ b/packages/client/src/pages/user/following.vue
@@ -6,7 +6,7 @@
 			<div v-if="user">
 				<XFollowList :user="user" type="following"/>
 			</div>
-			<MkError v-else-if="error" @retry="fetch()"/>
+			<MkError v-else-if="error" @retry="fetchUser()"/>
 			<MkLoading v-else/>
 		</transition>
 	</MkSpacer>
diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
index 4b07beeb8..7e635f8b2 100644
--- a/packages/client/src/pages/user/index.vue
+++ b/packages/client/src/pages/user/index.vue
@@ -10,7 +10,7 @@
 				<XPages v-else-if="tab === 'pages'" :user="user"/>
 				<XGallery v-else-if="tab === 'gallery'" :user="user"/>
 			</div>
-			<MkError v-else-if="error" @retry="fetch()"/>
+			<MkError v-else-if="error" @retry="fetchUser()"/>
 			<MkLoading v-else/>
 		</transition>
 	</div>

From 3632994024418f4d1c4eb1bcf149b813967f8a7a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 01:20:36 +0900
Subject: [PATCH 15/24] fix(client): fix window default prop

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

diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index e259ecdab..460cf7d59 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -75,7 +75,7 @@ const props = withDefaults(defineProps<{
 	canResize: false,
 	closeButton: true,
 	mini: false,
-	front: true,
+	front: false,
 	contextmenu: null,
 	buttonsLeft: () => [],
 	buttonsRight: () => [],

From 07f390cf004d09fb28f3cc286b8d5ca49ad4cb74 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 13:23:36 +0900
Subject: [PATCH 16/24] Update CHANGELOG.md

---
 CHANGELOG.md | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69bb5ba89..114481357 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,15 @@
 You should also include the user name that made the change.
 -->
 
+## 12.x.x (unreleased)
+
+### Improvements
+- Client: UIのブラッシュアップ @syuilo
+
+### Bugfixes
+- Client: リアクションピッカーがアプリ内ウィンドウの後ろに表示されてしまう問題を修正 @syuilo
+- Client: ユーザー情報の取得の再試行を修正する @xianonn
+
 ## 12.117.0 (2022/07/18)
 
 ### Improvements

From 64d108cb86513e5a5931745ef1524092bbf34da5 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 13:25:23 +0900
Subject: [PATCH 17/24] update vite

---
 packages/client/package.json | 2 +-
 packages/client/yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/client/package.json b/packages/client/package.json
index 31c760489..eaebf69ed 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -73,7 +73,7 @@
 		"uuid": "8.3.2",
 		"v-debounce": "0.1.2",
 		"vanilla-tilt": "1.7.2",
-		"vite": "3.0.1",
+		"vite": "3.0.2",
 		"vue": "3.2.37",
 		"vue-prism-editor": "2.0.0-alpha.2",
 		"vuedraggable": "4.0.1",
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index 0be2fa310..af3af22fc 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -4222,10 +4222,10 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
-vite@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.1.tgz#9c30c9662ec24455b8834d0b831c9f3c5a9139ca"
-  integrity sha512-nefKSglkoEsDpYUkBuT2++L04ktcP8fz8dxLtmZdDdMyhubFSOLFw6BTh/46Fc6tIX/cibs/NVYWNrsqn0k6pQ==
+vite@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.2.tgz#2a7b4642c53ae066cf724e7e581d6c1fd24e2c32"
+  integrity sha512-TAqydxW/w0U5AoL5AsD9DApTvGb2iNbGs3sN4u2VdT1GFkQVUfgUldt+t08TZgi23uIauh1TUOQJALduo9GXqw==
   dependencies:
     esbuild "^0.14.47"
     postcss "^8.4.14"

From bfce689abcb7c4f3fd9401cb7761f4a6c8e6b93d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Tue, 19 Jul 2022 17:09:21 +0900
Subject: [PATCH 18/24] fix(server): make sure `getFileInfo` doesn't fail if
 `detectSensitivity` ever fails (#9020)

---
 packages/backend/src/misc/get-file-info.ts | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts
index 42061fcf8..1c988b248 100644
--- a/packages/backend/src/misc/get-file-info.ts
+++ b/packages/backend/src/misc/get-file-info.ts
@@ -101,13 +101,17 @@ export async function getFileInfo(path: string, opts: {
 	let porn = false;
 
 	if (!opts.skipSensitiveDetection) {
-		[sensitive, porn] = await detectSensitivity(
+		await detectSensitivity(
 			path,
 			type.mime,
 			opts.sensitiveThreshold ?? 0.5,
 			opts.sensitiveThresholdForPorn ?? 0.75,
 			opts.enableSensitiveMediaDetectionForVideos ?? false,
-		);
+		).then(value => {
+			[sensitive, porn] = value;
+		}, error => {
+			warnings.push(`detectSensitivity failed: ${error}`);
+		});
 	}
 
 	return {

From f025e49146803e49a5957e089103075ddbd8eedc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 17:10:21 +0900
Subject: [PATCH 19/24] Update CHANGELOG.md

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 114481357..c8926cb6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ You should also include the user name that made the change.
 - Client: UIのブラッシュアップ @syuilo
 
 ### Bugfixes
+- Server: ファイルのアップロードに失敗することがある問題を修正 @acid-chicken
 - Client: リアクションピッカーがアプリ内ウィンドウの後ろに表示されてしまう問題を修正 @syuilo
 - Client: ユーザー情報の取得の再試行を修正する @xianonn
 

From b1282e4f338f1e2ccf475f6c1907c99236a08f06 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 17:13:24 +0900
Subject: [PATCH 20/24] fix(client): tweak mfm-cheat-sheet

---
 CHANGELOG.md                                  |   3 +-
 locales/ja-JP.yml                             |   2 +
 packages/client/src/pages/mfm-cheat-sheet.vue | 643 +++++++++---------
 3 files changed, 332 insertions(+), 316 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8926cb6d..0c6423149 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,7 +17,8 @@ You should also include the user name that made the change.
 ### Bugfixes
 - Server: ファイルのアップロードに失敗することがある問題を修正 @acid-chicken
 - Client: リアクションピッカーがアプリ内ウィンドウの後ろに表示されてしまう問題を修正 @syuilo
-- Client: ユーザー情報の取得の再試行を修正する @xianonn
+- Client: ユーザー情報の取得の再試行を修正 @xianonn
+- Client: MFMチートシートの挙動を修正 @syuilo
 
 ## 12.117.0 (2022/07/18)
 
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index e071b4bda..0c4a7c723 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1038,6 +1038,8 @@ _mfm:
   sparkleDescription: "キラキラしたパーティクルのエフェクトを追加します。"
   rotate: "回転"
   rotateDescription: "指定した角度で回転させます。"
+  plain: "プレーン"
+  plainDescription: "内側の構文を全て無効にします。"
 
 _instanceTicker:
   none: "表示しない"
diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index 3315479ab..0b5dae996 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -1,301 +1,313 @@
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader/></template>
-	<div class="mwysmxbg">
-		<div>{{ $ts._mfm.intro }}</div>
+	<MkSpacer :content-max="800">
+		<div class="mwysmxbg">
+			<div>{{ $ts._mfm.intro }}</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.mention }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.mentionDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_mention"/>
+						<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.hashtag }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.hashtagDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_hashtag"/>
+						<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.url }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.urlDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_url"/>
+						<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.link }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.linkDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_link"/>
+						<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.emoji }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.emojiDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_emoji"/>
+						<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.bold }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.boldDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_bold"/>
+						<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.small }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.smallDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_small"/>
+						<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.quote }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.quoteDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_quote"/>
+						<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.center }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.centerDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_center"/>
+						<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.inlineCode }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.inlineCodeDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_inlineCode"/>
+						<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.blockCode }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.blockCodeDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_blockCode"/>
+						<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.inlineMath }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.inlineMathDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_inlineMath"/>
+						<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<!-- deprecated
 		<div class="section _block">
-			<div class="title">{{ $ts._mfm.mention }}</div>
+			<div class="title">{{ $ts._mfm.search }}</div>
 			<div class="content">
-				<p>{{ $ts._mfm.mentionDescription }}</p>
+				<p>{{ $ts._mfm.searchDescription }}</p>
 				<div class="preview">
-					<Mfm :text="preview_mention"/>
-					<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
+					<Mfm :text="preview_search"/>
+					<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
 				</div>
 			</div>
 		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.hashtag }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.hashtagDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_hashtag"/>
-					<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
+		-->
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.flip }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.flipDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_flip"/>
+						<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.font }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.fontDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_font"/>
+						<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.x2 }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.x2Description }}</p>
+					<div class="preview">
+						<Mfm :text="preview_x2"/>
+						<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.x3 }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.x3Description }}</p>
+					<div class="preview">
+						<Mfm :text="preview_x3"/>
+						<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.x4 }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.x4Description }}</p>
+					<div class="preview">
+						<Mfm :text="preview_x4"/>
+						<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.blur }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.blurDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_blur"/>
+						<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.jelly }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.jellyDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_jelly"/>
+						<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.tada }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.tadaDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_tada"/>
+						<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.jump }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.jumpDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_jump"/>
+						<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.bounce }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.bounceDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_bounce"/>
+						<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.spin }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.spinDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_spin"/>
+						<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.shake }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.shakeDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_shake"/>
+						<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.twitch }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.twitchDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_twitch"/>
+						<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.rainbow }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.rainbowDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_rainbow"/>
+						<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.sparkle }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.sparkleDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_sparkle"/>
+						<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.rotate }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.rotateDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_rotate"/>
+						<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
+					</div>
+				</div>
+			</div>
+			<div class="section _block">
+				<div class="title">{{ $ts._mfm.plain }}</div>
+				<div class="content">
+					<p>{{ $ts._mfm.plainDescription }}</p>
+					<div class="preview">
+						<Mfm :text="preview_plain"/>
+						<MkTextarea v-model="preview_plain"><span>MFM</span></MkTextarea>
+					</div>
 				</div>
 			</div>
 		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.url }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.urlDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_url"/>
-					<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.link }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.linkDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_link"/>
-					<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.emoji }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.emojiDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_emoji"/>
-					<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.bold }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.boldDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_bold"/>
-					<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.small }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.smallDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_small"/>
-					<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.quote }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.quoteDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_quote"/>
-					<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.center }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.centerDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_center"/>
-					<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.inlineCode }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.inlineCodeDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_inlineCode"/>
-					<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.blockCode }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.blockCodeDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_blockCode"/>
-					<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.inlineMath }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.inlineMathDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_inlineMath"/>
-					<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<!-- deprecated
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.search }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.searchDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_search"/>
-				<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	-->
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.flip }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.flipDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_flip"/>
-					<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.font }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.fontDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_font"/>
-					<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.x2 }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.x2Description }}</p>
-				<div class="preview">
-					<Mfm :text="preview_x2"/>
-					<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.x3 }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.x3Description }}</p>
-				<div class="preview">
-					<Mfm :text="preview_x3"/>
-					<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.x4 }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.x4Description }}</p>
-				<div class="preview">
-					<Mfm :text="preview_x4"/>
-					<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.blur }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.blurDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_blur"/>
-					<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.jelly }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.jellyDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_jelly"/>
-					<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.tada }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.tadaDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_tada"/>
-					<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.jump }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.jumpDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_jump"/>
-					<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.bounce }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.bounceDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_bounce"/>
-					<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.spin }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.spinDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_spin"/>
-					<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.shake }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.shakeDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_shake"/>
-					<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.twitch }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.twitchDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_twitch"/>
-					<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.rainbow }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.rainbowDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_rainbow"/>
-					<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.sparkle }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.sparkleDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_sparkle"/>
-					<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
-				</div>
-			</div>
-		</div>
-		<div class="section _block">
-			<div class="title">{{ $ts._mfm.rotate }}</div>
-			<div class="content">
-				<p>{{ $ts._mfm.rotateDescription }}</p>
-				<div class="preview">
-					<Mfm :text="preview_rotate"/>
-					<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
-				</div>
-			</div>
-		</div>
-	</div>
+	</MkSpacer>
 </MkStickyContainer>
 </template>
 
@@ -306,35 +318,36 @@ import { definePageMetadata } from '@/scripts/page-metadata';
 import { i18n } from '@/i18n';
 import { instance } from '@/instance';
 
-const preview_mention = '@example';
-const preview_hashtag = '#test';
-const preview_url = 'https://example.com';
-const preview_link = `[${i18n.ts._mfm.dummy}](https://example.com)`;
-const preview_emoji = instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:';
-const preview_bold = `**${i18n.ts._mfm.dummy}**`;
-const preview_small = `<small>${i18n.ts._mfm.dummy}</small>`;
-const preview_center = `<center>${i18n.ts._mfm.dummy}</center>`;
-const preview_inlineCode = '`<: "Hello, world!"`';
-const preview_blockCode = '```\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```';
-const preview_inlineMath = '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)';
-const preview_quote = `> ${i18n.ts._mfm.dummy}`;
-const preview_search = `${i18n.ts._mfm.dummy} 検索`;
-const preview_jelly = '$[jelly 🍮] $[jelly.speed=5s 🍮]';
-const preview_tada = '$[tada 🍮] $[tada.speed=5s 🍮]';
-const preview_jump = '$[jump 🍮] $[jump.speed=5s 🍮]';
-const preview_bounce = '$[bounce 🍮] $[bounce.speed=5s 🍮]';
-const preview_shake = '$[shake 🍮] $[shake.speed=5s 🍮]';
-const preview_twitch = '$[twitch 🍮] $[twitch.speed=5s 🍮]';
-const preview_spin = '$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]';
-const preview_flip = `$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`;
-const preview_font = `$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`;
-const preview_x2 = '$[x2 🍮]';
-const preview_x3 = '$[x3 🍮]';
-const preview_x4 = '$[x4 🍮]';
-const preview_blur = `$[blur ${i18n.ts._mfm.dummy}]`;
-const preview_rainbow = '$[rainbow 🍮] $[rainbow.speed=5s 🍮]';
-const preview_sparkle = '$[sparkle 🍮]';
-const preview_rotate = '$[rotate 🍮]';
+let preview_mention = $ref('@example');
+let preview_hashtag = $ref('#test');
+let preview_url = $ref('https://example.com');
+let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`);
+let preview_emoji = $ref(instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:');
+let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`);
+let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`);
+let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`);
+let preview_inlineCode = $ref('`<: "Hello, world!"`');
+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_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
+let preview_search = $ref(`${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 🍮]');
+let preview_bounce = $ref('$[bounce 🍮] $[bounce.speed=5s 🍮]');
+let preview_shake = $ref('$[shake 🍮] $[shake.speed=5s 🍮]');
+let preview_twitch = $ref('$[twitch 🍮] $[twitch.speed=5s 🍮]');
+let preview_spin = $ref('$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]');
+let preview_flip = $ref(`$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`);
+let preview_font = $ref(`$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`);
+let preview_x2 = $ref('$[x2 🍮]');
+let preview_x3 = $ref('$[x3 🍮]');
+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_plain = $ref('<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>');
 
 definePageMetadata({
 	title: i18n.ts._mfm.cheatSheet,

From 566d42e0653ab0d9e100e1066fb40a9b19c7b17e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 17:21:03 +0900
Subject: [PATCH 21/24] =?UTF-8?q?fix(client):=20=E3=80=8C=E3=82=A4?=
 =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=82=BF=E3=83=B3=E3=82=B9=E3=81=8B=E3=82=89?=
 =?UTF-8?q?=E3=81=AE=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B=E3=82=92=E5=8F=97?=
 =?UTF-8?q?=E3=81=91=E5=8F=96=E3=82=8B=E3=80=8D=E3=81=AE=E8=A8=AD=E5=AE=9A?=
 =?UTF-8?q?=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84?=
 =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix #8474
---
 CHANGELOG.md                                 | 1 +
 packages/client/src/pages/settings/email.vue | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c6423149..c337e13df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ You should also include the user name that made the change.
 - Client: リアクションピッカーがアプリ内ウィンドウの後ろに表示されてしまう問題を修正 @syuilo
 - Client: ユーザー情報の取得の再試行を修正 @xianonn
 - Client: MFMチートシートの挙動を修正 @syuilo
+- Client: 「インスタンスからのお知らせを受け取る」の設定を変更できない問題を修正 @syuilo
 
 ## 12.117.0 (2022/07/18)
 
diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue
index e575af6d6..9d2afd6a6 100644
--- a/packages/client/src/pages/settings/email.vue
+++ b/packages/client/src/pages/settings/email.vue
@@ -10,7 +10,7 @@
 	</FormSection>
 
 	<FormSection>
-		<FormSwitch :value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
+		<FormSwitch :model-value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
 			{{ $ts.receiveAnnouncementFromInstance }}
 		</FormSwitch>
 	</FormSection>

From 6feb15d0c77572c1a28d7bb59548bdbae7503008 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 17:21:30 +0900
Subject: [PATCH 22/24] New translations ja-JP.yml (Slovak) (#9014)

---
 locales/sk-SK.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 2ac0a8566..4fbdc1cb1 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -885,6 +885,7 @@ enableAutoSensitiveDescription: "Ak je zapnuté, príznak NSFW sa na médiách a
 activeEmailValidationDescription: "Dôkladnejšie overí e-mailovú adresu používateľa tým, že zistí, či ide o vyradenú e-mailovú adresu a či sa s ňou dá skutočne komunikovať. Ak nie je začiarknuté, e-mailová adresa sa kontroluje len ako text."
 navbar: "Navigačný panel"
 account: "Účty"
+move: "Pohyb"
 _sensitiveMediaDetection:
   description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
   sensitivity: "Citlivosť detekcie"
@@ -1691,6 +1692,7 @@ _deck:
   alwaysShowMainColumn: "Vždy zobraziť v hlavnom stĺpci"
   columnAlign: "Zarovnať stĺpce"
   addColumn: "Pridať stĺpec"
+  configureColumn: "Nastavenie stĺpcov"
   swapLeft: "Vymeniť vľavo"
   swapRight: "Vymeniť vpravo"
   swapUp: "Vymeniť hore"

From 707f7a3bb44f8be8bf41e116c68ee58227f371ea Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 17:22:24 +0900
Subject: [PATCH 23/24] 12.117.1

---
 CHANGELOG.md | 2 +-
 package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c337e13df..59c748fb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@
 You should also include the user name that made the change.
 -->
 
-## 12.x.x (unreleased)
+## 12.117.1 (2022/07/19)
 
 ### Improvements
 - Client: UIのブラッシュアップ @syuilo
diff --git a/package.json b/package.json
index 2699b7dc9..cabb9f8f9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.117.0",
+	"version": "12.117.1",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 5edecc19141bf09f3714a9e795cf913c2f6f067c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Jul 2022 21:36:33 +0900
Subject: [PATCH 24/24] enhance(client): suspense

Fix #8817
---
 CHANGELOG.md                                          | 7 +++++++
 packages/client/src/components/global/router-view.vue | 8 +++++++-
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59c748fb6..4fd6ff231 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,13 @@
 You should also include the user name that made the change.
 -->
 
+## 12.x.x (unreleased)
+
+### Improvements
+
+### Bugfixes
+- Client: 一度作ったwebhookの設定画面を開こうとするとページがフリーズする @syuilo
+
 ## 12.117.1 (2022/07/19)
 
 ### Improvements
diff --git a/packages/client/src/components/global/router-view.vue b/packages/client/src/components/global/router-view.vue
index fca2371f0..cd1e78019 100644
--- a/packages/client/src/components/global/router-view.vue
+++ b/packages/client/src/components/global/router-view.vue
@@ -1,6 +1,12 @@
 <template>
 <KeepAlive :max="defaultStore.state.numberOfPageCache">
-	<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
+	<Suspense>
+		<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
+
+		<template #fallback>
+			Loading...
+		</template>
+	</Suspense>
 </KeepAlive>
 </template>