From b1442b71079f17567615a8c35b6669d70fff18e2 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 00:36:06 +0900
Subject: [PATCH 001/100] refactor(client): remove invalid computed

---
 packages/client/src/components/captcha.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue
index 183658471..736073491 100644
--- a/packages/client/src/components/captcha.vue
+++ b/packages/client/src/components/captcha.vue
@@ -51,7 +51,7 @@ const variable = computed(() => {
 	}
 });
 
-const loaded = computed(() => !!window[variable.value]);
+const loaded = !!window[variable.value];
 
 const src = computed(() => {
 	switch (props.provider) {
@@ -62,7 +62,7 @@ const src = computed(() => {
 
 const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
 
-if (loaded.value) {
+if (loaded) {
 	available.value = true;
 } else {
 	(document.getElementById(props.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
@@ -74,7 +74,7 @@ if (loaded.value) {
 }
 
 function reset() {
-	if (captcha.value?.reset) captcha.value.reset();
+	if (captcha.value.reset) captcha.value.reset();
 }
 
 function requestRender() {

From b9c7b9be04d322ad18ace261015526d494187564 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 11:13:32 +0900
Subject: [PATCH 002/100] =?UTF-8?q?enhance(client):=20=E3=83=A1=E3=83=8B?=
 =?UTF-8?q?=E3=83=A5=E3=83=BC=E6=95=B4=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Resolve #6389
Fix #8035
---
 locales/ja-JP.yml                             |   1 +
 packages/client/src/components/launch-pad.vue |  31 ++++-
 packages/client/src/menu.ts                   |  29 -----
 .../{emojis.category.vue => about.emojis.vue} |   0
 .../client/src/pages/about.federation.vue     | 106 +++++++++++++++
 packages/client/src/pages/about.vue           |  18 ++-
 packages/client/src/pages/admin/index.vue     |   2 +-
 packages/client/src/pages/emojis.vue          |  60 ---------
 packages/client/src/pages/federation.vue      | 122 ------------------
 packages/client/src/pages/mentions.vue        |  27 ----
 packages/client/src/pages/messages.vue        |  30 -----
 packages/client/src/pages/notifications.vue   |  44 +++++--
 .../client/src/pages/user/index.activity.vue  |   6 +-
 packages/client/src/router.ts                 |  12 --
 packages/client/src/widgets/activity.vue      |   2 +-
 15 files changed, 190 insertions(+), 300 deletions(-)
 rename packages/client/src/pages/{emojis.category.vue => about.emojis.vue} (100%)
 create mode 100644 packages/client/src/pages/about.federation.vue
 delete mode 100644 packages/client/src/pages/emojis.vue
 delete mode 100644 packages/client/src/pages/federation.vue
 delete mode 100644 packages/client/src/pages/mentions.vue
 delete mode 100644 packages/client/src/pages/messages.vue

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index f81338922..139643f72 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -857,6 +857,7 @@ check: "チェック"
 isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
 typeToConfirm: "この操作を行うには {x} と入力してください"
 deleteAccount: "アカウント削除"
+document: "ドキュメント"
 
 _emailUnavailable:
   used: "既に使用されています"
diff --git a/packages/client/src/components/launch-pad.vue b/packages/client/src/components/launch-pad.vue
index ffefc1b08..a6025f8b2 100644
--- a/packages/client/src/components/launch-pad.vue
+++ b/packages/client/src/components/launch-pad.vue
@@ -16,13 +16,13 @@
 			</template>
 		</div>
 		<div class="sub">
-			<a v-click-anime href="https://misskey-hub.net/help.html" target="_blank" @click.passive="close()">
+			<button v-click-anime class="_button" @click="help">
 				<i class="fas fa-question-circle icon"></i>
 				<div class="text">{{ $ts.help }}</div>
-			</a>
+			</button>
 			<MkA v-click-anime to="/about" @click.passive="close()">
 				<i class="fas fa-info-circle icon"></i>
-				<div class="text">{{ $t('aboutX', { x: instanceName }) }}</div>
+				<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"/>
@@ -34,13 +34,14 @@
 </template>
 
 <script lang="ts" setup>
-import {  } from 'vue';
+import { } from 'vue';
 import MkModal from '@/components/ui/modal.vue';
 import { menuDef } from '@/menu';
 import { instanceName } from '@/config';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
 import { deviceKind } from '@/scripts/device-kind';
+import * as os from '@/os';
 
 const props = withDefaults(defineProps<{
 	src?: HTMLElement;
@@ -73,6 +74,28 @@ const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuD
 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/menu.ts b/packages/client/src/menu.ts
index 5e281f4ea..2c0126eb8 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -112,20 +112,6 @@ export const menuDef = reactive({
 			os.popupMenu(items, ev.currentTarget ?? ev.target);
 		},
 	},
-	mentions: {
-		title: 'mentions',
-		icon: 'fas fa-at',
-		show: computed(() => $i != null),
-		indicated: computed(() => $i != null && $i.hasUnreadMentions),
-		to: '/my/mentions',
-	},
-	messages: {
-		title: 'directNotes',
-		icon: 'fas fa-envelope',
-		show: computed(() => $i != null),
-		indicated: computed(() => $i != null && $i.hasUnreadSpecifiedNotes),
-		to: '/my/messages',
-	},
 	favorites: {
 		title: 'favorites',
 		icon: 'fas fa-star',
@@ -153,21 +139,6 @@ export const menuDef = reactive({
 		icon: 'fas fa-satellite-dish',
 		to: '/channels',
 	},
-	federation: {
-		title: 'federation',
-		icon: 'fas fa-globe',
-		to: '/federation',
-	},
-	emojis: {
-		title: 'emojis',
-		icon: 'fas fa-laugh',
-		to: '/emojis',
-	},
-	scratchpad: {
-		title: 'scratchpad',
-		icon: 'fas fa-terminal',
-		to: '/scratchpad',
-	},
 	ui: {
 		title: 'switchUi',
 		icon: 'fas fa-columns',
diff --git a/packages/client/src/pages/emojis.category.vue b/packages/client/src/pages/about.emojis.vue
similarity index 100%
rename from packages/client/src/pages/emojis.category.vue
rename to packages/client/src/pages/about.emojis.vue
diff --git a/packages/client/src/pages/about.federation.vue b/packages/client/src/pages/about.federation.vue
new file mode 100644
index 000000000..00ca44eec
--- /dev/null
+++ b/packages/client/src/pages/about.federation.vue
@@ -0,0 +1,106 @@
+<template>
+<div class="taeiyria">
+	<div class="query">
+		<MkInput v-model="host" :debounce="true" class="">
+			<template #prefix><i class="fas fa-search"></i></template>
+			<template #label>{{ $ts.host }}</template>
+		</MkInput>
+		<FormSplit style="margin-top: var(--margin);">
+			<MkSelect v-model="state">
+				<template #label>{{ $ts.state }}</template>
+				<option value="all">{{ $ts.all }}</option>
+				<option value="federating">{{ $ts.federating }}</option>
+				<option value="subscribing">{{ $ts.subscribing }}</option>
+				<option value="publishing">{{ $ts.publishing }}</option>
+				<option value="suspended">{{ $ts.suspended }}</option>
+				<option value="blocked">{{ $ts.blocked }}</option>
+				<option value="notResponding">{{ $ts.notResponding }}</option>
+			</MkSelect>
+			<MkSelect v-model="sort">
+				<template #label>{{ $ts.sort }}</template>
+				<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
+				<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
+				<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
+				<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
+				<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
+				<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
+				<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
+				<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
+			</MkSelect>
+		</FormSplit>
+	</div>
+
+	<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
+		<div class="dqokceoi">
+			<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
+				<MkInstanceCardMini :instance="instance"/>
+			</MkA>
+		</div>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkInstanceCardMini from '@/components/instance-card-mini.vue';
+import FormSplit from '@/components/form/split.vue';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+
+let host = $ref('');
+let state = $ref('federating');
+let sort = $ref('+pubSub');
+const pagination = {
+	endpoint: 'federation/instances' as const,
+	limit: 10,
+	offsetMode: true,
+	params: computed(() => ({
+		sort: sort,
+		host: host !== '' ? host : null,
+		...(
+			state === 'federating' ? { federating: true } :
+			state === 'subscribing' ? { subscribing: true } :
+			state === 'publishing' ? { publishing: true } :
+			state === 'suspended' ? { suspended: true } :
+			state === 'blocked' ? { blocked: true } :
+			state === 'notResponding' ? { notResponding: true } :
+			{}),
+	})),
+};
+
+function getStatus(instance) {
+	if (instance.isSuspended) return 'Suspended';
+	if (instance.isBlocked) return 'Blocked';
+	if (instance.isNotResponding) return 'Error';
+	return 'Alive';
+}
+</script>
+
+<style lang="scss" scoped>
+.taeiyria {
+	> .query {
+		background: var(--bg);
+		margin-bottom: 16px;
+	}
+}
+
+.dqokceoi {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
+	grid-gap: 12px;
+
+	> .instance:hover {
+		text-decoration: none;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index c0226bdb6..bacfab771 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -67,6 +67,12 @@
 			</FormSection>
 		</div>
 	</MkSpacer>
+	<MkSpacer v-else-if="tab === 'emojis'" :content-max="1000" :margin-min="20">
+		<XEmojis/>
+	</MkSpacer>
+	<MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20">
+		<XFederation/>
+	</MkSpacer>
 	<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
 		<MkInstanceStats :chart-limit="500" :detailed="true"/>
 	</MkSpacer>
@@ -75,6 +81,8 @@
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
+import XEmojis from './about.emojis.vue';
+import XFederation from './about.federation.vue';
 import { version, instanceName , host } from '@/config';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
@@ -100,10 +108,18 @@ const headerActions = $computed(() => []);
 const headerTabs = $computed(() => [{
 	key: 'overview',
 	title: i18n.ts.overview,
+}, {
+	key: 'emojis',
+	title: i18n.ts.customEmojis,
+	icon: 'fas fa-laugh',
+}, {
+	key: 'federation',
+	title: i18n.ts.federation,
+	icon: 'fas fa-globe',
 }, {
 	key: 'charts',
 	title: i18n.ts.charts,
-	icon: 'fas fa-chart-bar',
+	icon: 'fas fa-chart-simple',
 }]);
 
 definePageMetadata(computed(() => ({
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index b91330e1b..5ffbe3495 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -201,7 +201,7 @@ const component = $computed(() => {
 		case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
 		case 'users': return defineAsyncComponent(() => import('./users.vue'));
 		case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
-		case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
+		//case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
 		case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
 		case 'files': return defineAsyncComponent(() => import('./files.vue'));
 		case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue
deleted file mode 100644
index 159299584..000000000
--- a/packages/client/src/pages/emojis.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<div :class="$style.root">
-		<XCategory v-if="tab === 'category'"/>
-	</div>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { ref, computed } from 'vue';
-import XCategory from './emojis.category.vue';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-const tab = ref('category');
-
-function menu(ev) {
-	os.popupMenu([{
-		icon: 'fas fa-download',
-		text: i18n.ts.export,
-		action: async () => {
-			os.api('export-custom-emojis', {
-			})
-			.then(() => {
-				os.alert({
-					type: 'info',
-					text: i18n.ts.exportRequested,
-				});
-			}).catch((err) => {
-				os.alert({
-					type: 'error',
-					text: err.message,
-				});
-			});
-		},
-	}], ev.currentTarget ?? ev.target);
-}
-
-const headerActions = $computed(() => [{
-	icon: 'fas fa-ellipsis-h',
-	handler: menu,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
-	title: i18n.ts.customEmojis,
-	icon: 'fas fa-laugh',
-	bg: 'var(--bg)',
-});
-</script>
-
-<style lang="scss" module>
-.root {
-	max-width: 1000px;
-	margin: 0 auto;
-}
-</style>
diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue
deleted file mode 100644
index 07c5a32bd..000000000
--- a/packages/client/src/pages/federation.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-<MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<MkSpacer :content-max="1000">
-		<div class="taeiyria">
-			<div class="query">
-				<MkInput v-model="host" :debounce="true" class="">
-					<template #prefix><i class="fas fa-search"></i></template>
-					<template #label>{{ $ts.host }}</template>
-				</MkInput>
-				<FormSplit style="margin-top: var(--margin);">
-					<MkSelect v-model="state">
-						<template #label>{{ $ts.state }}</template>
-						<option value="all">{{ $ts.all }}</option>
-						<option value="federating">{{ $ts.federating }}</option>
-						<option value="subscribing">{{ $ts.subscribing }}</option>
-						<option value="publishing">{{ $ts.publishing }}</option>
-						<option value="suspended">{{ $ts.suspended }}</option>
-						<option value="blocked">{{ $ts.blocked }}</option>
-						<option value="notResponding">{{ $ts.notResponding }}</option>
-					</MkSelect>
-					<MkSelect v-model="sort">
-						<template #label>{{ $ts.sort }}</template>
-						<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
-						<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
-						<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
-						<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
-						<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
-						<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
-						<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
-						<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
-						<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
-						<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
-						<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
-						<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
-						<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
-						<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
-					</MkSelect>
-				</FormSplit>
-			</div>
-
-			<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
-				<div class="dqokceoi">
-					<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
-						<MkInstanceCardMini :instance="instance"/>
-					</MkA>
-				</div>
-			</MkPagination>
-		</div>
-	</MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { computed } from 'vue';
-import MkButton from '@/components/ui/button.vue';
-import MkInput from '@/components/form/input.vue';
-import MkSelect from '@/components/form/select.vue';
-import MkPagination from '@/components/ui/pagination.vue';
-import MkInstanceCardMini from '@/components/instance-card-mini.vue';
-import FormSplit from '@/components/form/split.vue';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let host = $ref('');
-let state = $ref('federating');
-let sort = $ref('+pubSub');
-const pagination = {
-	endpoint: 'federation/instances' as const,
-	limit: 10,
-	offsetMode: true,
-	params: computed(() => ({
-		sort: sort,
-		host: host !== '' ? host : null,
-		...(
-			state === 'federating' ? { federating: true } :
-			state === 'subscribing' ? { subscribing: true } :
-			state === 'publishing' ? { publishing: true } :
-			state === 'suspended' ? { suspended: true } :
-			state === 'blocked' ? { blocked: true } :
-			state === 'notResponding' ? { notResponding: true } :
-			{}),
-	})),
-};
-
-function getStatus(instance) {
-	if (instance.isSuspended) return 'Suspended';
-	if (instance.isBlocked) return 'Blocked';
-	if (instance.isNotResponding) return 'Error';
-	return 'Alive';
-}
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
-	title: i18n.ts.federation,
-	icon: 'fas fa-globe',
-	bg: 'var(--bg)',
-});
-</script>
-
-<style lang="scss" scoped>
-.taeiyria {
-	> .query {
-		background: var(--bg);
-		margin-bottom: 16px;
-	}
-}
-
-.dqokceoi {
-	display: grid;
-	grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
-	grid-gap: 12px;
-
-	> .instance:hover {
-		text-decoration: none;
-	}
-}
-</style>
diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue
deleted file mode 100644
index 0835f1f01..000000000
--- a/packages/client/src/pages/mentions.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<template><MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-		<MkSpacer :content-max="800">
-	<XNotes :pagination="pagination"/>
-</MkSpacer></MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import XNotes from '@/components/notes.vue';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-const pagination = {
-	endpoint: 'notes/mentions' as const,
-	limit: 10,
-};
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
-	title: i18n.ts.mentions,
-	icon: 'fas fa-at',
-	bg: 'var(--bg)',
-});
-</script>
diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue
deleted file mode 100644
index e443b5c46..000000000
--- a/packages/client/src/pages/messages.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template><MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-		<MkSpacer :content-max="800">
-	<XNotes :pagination="pagination"/>
-</MkSpacer></MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import XNotes from '@/components/notes.vue';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-const pagination = {
-	endpoint: 'notes/mentions' as const,
-	limit: 10,
-	params: {
-		visibility: 'specified',
-	},
-};
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
-	title: i18n.ts.directNotes,
-	icon: 'fas fa-envelope',
-	bg: 'var(--bg)',
-});
-</script>
diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue
index 52cb298fa..3df1a3f17 100644
--- a/packages/client/src/pages/notifications.vue
+++ b/packages/client/src/pages/notifications.vue
@@ -2,8 +2,14 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="800">
-		<div class="clupoqwt">
-			<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
+		<div v-if="tab === 'all' || tab === 'unread'">
+			<XNotifications class="notifications" :include-types="includeTypes" :unread-only="unreadOnly"/>
+		</div>
+		<div v-else-if="tab === 'mentions'">
+			<XNotes :pagination="mentionsPagination"/>
+		</div>
+		<div v-else-if="tab === 'directNotes'">
+			<XNotes :pagination="directNotesPagination"/>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
@@ -13,12 +19,27 @@
 import { computed } from 'vue';
 import { notificationTypes } from 'misskey-js';
 import XNotifications from '@/components/notifications.vue';
+import XNotes from '@/components/notes.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 
 let tab = $ref('all');
 let includeTypes = $ref<string[] | null>(null);
+let unreadOnly = $computed(() => tab === 'unread');
+
+const mentionsPagination = {
+	endpoint: 'notes/mentions' as const,
+	limit: 10,
+};
+
+const directNotesPagination = {
+	endpoint: 'notes/mentions' as const,
+	limit: 10,
+	params: {
+		visibility: 'specified',
+	},
+};
 
 function setFilter(ev) {
 	const typeItems = notificationTypes.map(t => ({
@@ -38,18 +59,18 @@ function setFilter(ev) {
 	os.popupMenu(items, ev.currentTarget ?? ev.target);
 }
 
-const headerActions = $computed(() => [{
+const headerActions = $computed(() => [tab === 'all' ? {
 	text: i18n.ts.filter,
 	icon: 'fas fa-filter',
 	highlighted: includeTypes != null,
 	handler: setFilter,
-}, {
+} : undefined, tab === 'all' ? {
 	text: i18n.ts.markAllAsRead,
 	icon: 'fas fa-check',
 	handler: () => {
 		os.apiWithDialog('notifications/mark-all-as-read');
 	},
-}]);
+} : undefined].filter(x => x !== undefined));
 
 const headerTabs = $computed(() => [{
 	key: 'all',
@@ -57,6 +78,14 @@ const headerTabs = $computed(() => [{
 }, {
 	key: 'unread',
 	title: i18n.ts.unread,
+}, {
+	key: 'mentions',
+	title: i18n.ts.mentions,
+	icon: 'fas fa-at',
+}, {
+	key: 'directNotes',
+	title: i18n.ts.directNotes,
+	icon: 'fas fa-envelope',
 }]);
 
 definePageMetadata(computed(() => ({
@@ -65,8 +94,3 @@ definePageMetadata(computed(() => ({
 	bg: 'var(--bg)',
 })));
 </script>
-
-<style lang="scss" scoped>
-.clupoqwt {
-}
-</style>
diff --git a/packages/client/src/pages/user/index.activity.vue b/packages/client/src/pages/user/index.activity.vue
index aecd25d6b..8a7a86e0f 100644
--- a/packages/client/src/pages/user/index.activity.vue
+++ b/packages/client/src/pages/user/index.activity.vue
@@ -1,6 +1,6 @@
 <template>
 <MkContainer>
-	<template #header><i class="fas fa-chart-bar" style="margin-right: 0.5em;"></i>{{ $ts.activity }}</template>
+	<template #header><i class="fas fa-chart-simple" style="margin-right: 0.5em;"></i>{{ $ts.activity }}</template>
 	<template #func>
 		<button class="_button" @click="showMenu">
 			<i class="fas fa-ellipsis-h"></i>
@@ -36,8 +36,8 @@ function showMenu(ev: MouseEvent) {
 		active: true,
 		action: () => {
 			chartSrc = 'per-user-notes';
-		}
-	}/*, {
+		},
+	},/*, {
 		text: i18n.ts.following,
 		action: () => {
 			chartSrc = 'per-user-following';
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index 769d9cb2a..a452a8bb3 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -65,12 +65,6 @@ export const routes = [{
 }, {
 	path: '/explore',
 	component: page(() => import('./pages/explore.vue')),
-}, {
-	path: '/federation',
-	component: page(() => import('./pages/federation.vue')),
-}, {
-	path: '/emojis',
-	component: page(() => import('./pages/emojis.vue')),
 }, {
 	path: '/search',
 	component: page(() => import('./pages/search.vue')),
@@ -156,12 +150,6 @@ export const routes = [{
 }, {
 	path: '/my/favorites',
 	component: page(() => import('./pages/favorites.vue')),
-}, {
-	path: '/my/messages',
-	component: page(() => import('./pages/messages.vue')),
-}, {
-	path: '/my/mentions',
-	component: page(() => import('./pages/mentions.vue')),
 }, {
 	name: 'messaging',
 	path: '/my/messaging',
diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue
index 265bde4a3..7252d6540 100644
--- a/packages/client/src/widgets/activity.vue
+++ b/packages/client/src/widgets/activity.vue
@@ -1,6 +1,6 @@
 <template>
 <MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" class="mkw-activity">
-	<template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template>
+	<template #header><i class="fas fa-chart-simple"></i>{{ $ts._widgets.activity }}</template>
 	<template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template>
 
 	<div>

From d2790bfa6093e1ff1cfbd8de2882101da06a64c0 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 14:14:27 +0900
Subject: [PATCH 003/100] enhance(client): add users tab to instance-info

---
 packages/client/src/pages/instance-info.vue | 25 +++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
index 6d52f2a87..4eaa60112 100644
--- a/packages/client/src/pages/instance-info.vue
+++ b/packages/client/src/pages/instance-info.vue
@@ -95,6 +95,13 @@
 				</div>
 			</div>
 		</div>
+		<div v-else-if="tab === 'users'" class="_formRoot">
+			<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
+				<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${new Date(user.updatedAt).toLocaleString()}`" class="user" :to="`/user-info/${user.id}`">
+					<MkUserCardMini :user="user"/>
+				</MkA>
+			</MkPagination>
+		</div>
 		<div v-else-if="tab === 'raw'" class="_formRoot">
 			<MkObjectView tall :value="instance">
 			</MkObjectView>
@@ -121,6 +128,8 @@ import bytes from '@/filters/bytes';
 import { iAmModerator } from '@/account';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { i18n } from '@/i18n';
+import MkUserCardMini from '@/components/user-card-mini.vue';
+import MkPagination from '@/components/ui/pagination.vue';
 
 const props = defineProps<{
 	host: string;
@@ -133,6 +142,18 @@ let instance = $ref<misskey.entities.Instance | null>(null);
 let suspended = $ref(false);
 let isBlocked = $ref(false);
 
+const usersPagination = {
+	endpoint: 'admin/show-users' as const,
+	limit: 10,
+	params: {
+		sort: '+updatedAt',
+		state: 'all',
+		origin: 'remote',
+		hostname: props.host,
+	},
+	offsetMode: true,
+};
+
 async function fetch() {
 	instance = await os.api('federation/show-instance', {
 		host: props.host,
@@ -182,6 +203,10 @@ const headerTabs = $computed(() => [{
 	key: 'chart',
 	title: i18n.ts.charts,
 	icon: 'fas fa-chart-simple',
+}, {
+	key: 'users',
+	title: i18n.ts.users,
+	icon: 'fas fa-users',
 }, {
 	key: 'raw',
 	title: 'Raw data',

From ee71296eb76d780d254ad9d4d7a4b0fb36a76811 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 14:19:40 +0900
Subject: [PATCH 004/100] chore(client): tweak style

---
 packages/client/src/components/key-value.vue | 4 ++--
 packages/client/src/components/ui/window.vue | 1 +
 packages/client/src/pages/instance-info.vue  | 2 +-
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/key-value.vue b/packages/client/src/components/key-value.vue
index 592c7be59..3d665e159 100644
--- a/packages/client/src/components/key-value.vue
+++ b/packages/client/src/components/key-value.vue
@@ -16,8 +16,8 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
 import * as os from '@/os';
 
 const props = withDefaults(defineProps<{
-	copy: string | null;
-	oneline: boolean;
+	copy?: string | null;
+	oneline?: boolean;
 }>(), {
 	copy: null,
 	oneline: false,
diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index 8305cd1b2..547543770 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -410,6 +410,7 @@ export default defineComponent({
 			backdrop-filter: var(--blur, blur(15px));
 			//border-bottom: solid 1px var(--divider);
 			font-size: 95%;
+			font-weight: bold;
 
 			> .left, > .right {
 				> .button {
diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
index 4eaa60112..b72fcb152 100644
--- a/packages/client/src/pages/instance-info.vue
+++ b/packages/client/src/pages/instance-info.vue
@@ -209,7 +209,7 @@ const headerTabs = $computed(() => [{
 	icon: 'fas fa-users',
 }, {
 	key: 'raw',
-	title: 'Raw data',
+	title: 'Raw',
 	icon: 'fas fa-code',
 }]);
 

From a40862106c236aa1bc57de5ad178bc7543ffea20 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 15:41:06 +0900
Subject: [PATCH 005/100] chore(client): tweak client

---
 .../client/src/components/instance-stats.vue  | 221 +++++++++++++-----
 .../src/components/object-view.value.vue      | 106 ++++++---
 .../client/src/components/object-view.vue     |  23 +-
 packages/client/src/pages/about.vue           |   2 +-
 packages/client/src/pages/user-info.vue       |   2 +-
 5 files changed, 250 insertions(+), 104 deletions(-)

diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue
index f386a8de9..9a1769a3a 100644
--- a/packages/client/src/components/instance-stats.vue
+++ b/packages/client/src/components/instance-stats.vue
@@ -1,81 +1,188 @@
 <template>
 <div class="zbcjwnqg">
-	<div class="selects" style="display: flex;">
-		<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
-			<optgroup :label="$ts.federation">
-				<option value="federation">{{ $ts._charts.federation }}</option>
-				<option value="ap-request">{{ $ts._charts.apRequest }}</option>
-			</optgroup>
-			<optgroup :label="$ts.users">
-				<option value="users">{{ $ts._charts.usersIncDec }}</option>
-				<option value="users-total">{{ $ts._charts.usersTotal }}</option>
-				<option value="active-users">{{ $ts._charts.activeUsers }}</option>
-			</optgroup>
-			<optgroup :label="$ts.notes">
-				<option value="notes">{{ $ts._charts.notesIncDec }}</option>
-				<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
-				<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
-				<option value="notes-total">{{ $ts._charts.notesTotal }}</option>
-			</optgroup>
-			<optgroup :label="$ts.drive">
-				<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
-				<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
-			</optgroup>
-		</MkSelect>
-		<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
-			<option value="hour">{{ $ts.perHour }}</option>
-			<option value="day">{{ $ts.perDay }}</option>
-		</MkSelect>
+	<div class="main">
+		<div class="body">
+			<div class="selects" style="display: flex;">
+				<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
+					<optgroup :label="$ts.federation">
+						<option value="federation">{{ $ts._charts.federation }}</option>
+						<option value="ap-request">{{ $ts._charts.apRequest }}</option>
+					</optgroup>
+					<optgroup :label="$ts.users">
+						<option value="users">{{ $ts._charts.usersIncDec }}</option>
+						<option value="users-total">{{ $ts._charts.usersTotal }}</option>
+						<option value="active-users">{{ $ts._charts.activeUsers }}</option>
+					</optgroup>
+					<optgroup :label="$ts.notes">
+						<option value="notes">{{ $ts._charts.notesIncDec }}</option>
+						<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
+						<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
+						<option value="notes-total">{{ $ts._charts.notesTotal }}</option>
+					</optgroup>
+					<optgroup :label="$ts.drive">
+						<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
+						<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
+					</optgroup>
+				</MkSelect>
+				<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
+					<option value="hour">{{ $ts.perHour }}</option>
+					<option value="day">{{ $ts.perDay }}</option>
+				</MkSelect>
+			</div>
+			<div class="chart">
+				<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
+			</div>
+		</div>
 	</div>
-	<div class="chart">
-		<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
+	<div class="subpub">
+		<div class="sub">
+			<div class="title">Sub</div>
+			<canvas ref="subDoughnutEl"></canvas>
+		</div>
+		<div class="pub">
+			<div class="title">Pub</div>
+			<canvas ref="pubDoughnutEl"></canvas>
+		</div>
 	</div>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, ref } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+import {
+	Chart,
+	ArcElement,
+	LineElement,
+	BarElement,
+	PointElement,
+	BarController,
+	LineController,
+	CategoryScale,
+	LinearScale,
+	TimeScale,
+	Legend,
+	Title,
+	Tooltip,
+	SubTitle,
+	Filler,
+	DoughnutController,
+} from 'chart.js';
 import MkSelect from '@/components/form/select.vue';
 import MkChart from '@/components/chart.vue';
+import { useChartTooltip } from '@/scripts/use-chart-tooltip';
+import * as os from '@/os';
 
-export default defineComponent({
-	components: {
-		MkSelect,
-		MkChart,
-	},
+Chart.register(
+	ArcElement,
+	LineElement,
+	BarElement,
+	PointElement,
+	BarController,
+	LineController,
+	DoughnutController,
+	CategoryScale,
+	LinearScale,
+	TimeScale,
+	Legend,
+	Title,
+	Tooltip,
+	SubTitle,
+	Filler,
+);
 
-	props: {
-		chartLimit: {
-			type: Number,
-			required: false,
-			default: 90
+const props = withDefaults(defineProps<{
+	chartLimit?: number;
+	detailed?: boolean;
+}>(), {
+	chartLimit: 90,
+});
+
+const chartSpan = $ref<'hour' | 'day'>('hour');
+const chartSrc = $ref('active-users');
+let subDoughnutEl = $ref<HTMLCanvasElement>();
+let pubDoughnutEl = $ref<HTMLCanvasElement>();
+
+const { handler: externalTooltipHandler1 } = useChartTooltip();
+const { handler: externalTooltipHandler2 } = useChartTooltip();
+
+function createDoughnut(chartEl, tooltip, data) {
+	return new Chart(chartEl, {
+		type: 'doughnut',
+		data: {
+			labels: data.map(x => x.name),
+			datasets: [{
+				backgroundColor: data.map(x => x.color),
+				data: data.map(x => x.value),
+			}],
 		},
-		detailed: {
-			type: Boolean,
-			required: false,
-			default: false
+		options: {
+			layout: {
+				padding: {
+					left: 8,
+					right: 8,
+					top: 8,
+					bottom: 8,
+				},
+			},
+			interaction: {
+				intersect: false,
+			},
+			plugins: {
+				legend: {
+					display: false,
+				},
+				tooltip: {
+					enabled: false,
+					mode: 'index',
+					animation: {
+						duration: 0,
+					},
+					external: tooltip,
+				},
+			},
 		},
-	},
+	});
+}
 
-	setup() {
-		const chartSpan = ref<'hour' | 'day'>('hour');
-		const chartSrc = ref('active-users');
-
-		return {
-			chartSrc,
-			chartSpan,
-		};
-	},
+onMounted(() => {
+	os.apiGet('federation/stats').then(fedStats => {
+		createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowersCount }]));
+		createDoughnut(pubDoughnutEl, externalTooltipHandler1, fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowingCount }]));
+	});
 });
 </script>
 
 <style lang="scss" scoped>
 .zbcjwnqg {
-	> .selects {
+	> .main {
+		background: var(--panel);
+		border-radius: var(--radius);
+		padding: 24px;
+		margin-bottom: 16px;
+
+		> .body {
+			> .chart {
+				padding: 8px 0 0 0;
+			}
+		}
 	}
 
-	> .chart {
-		padding: 8px 0 0 0;
+	> .subpub {
+		display: flex;
+		gap: 16px;
+
+		> .sub, > .pub {
+			position: relative;
+			background: var(--panel);
+			border-radius: var(--radius);
+			padding: 24px;
+
+			> .title {
+				position: absolute;
+				top: 24px;
+				left: 24px;
+			}
+		}
 	}
 }
 </style>
diff --git a/packages/client/src/components/object-view.value.vue b/packages/client/src/components/object-view.value.vue
index 6f388636d..0c7230d78 100644
--- a/packages/client/src/components/object-view.value.vue
+++ b/packages/client/src/components/object-view.value.vue
@@ -1,31 +1,35 @@
 <template>
 <div class="igpposuu _monospace">
 	<div v-if="value === null" class="null">null</div>
-	<div v-else-if="typeof value === 'boolean'" class="boolean">{{ value ? 'true' : 'false' }}</div>
+	<div v-else-if="typeof value === 'boolean'" class="boolean" :class="{ true: value, false: !value }">{{ value ? 'true' : 'false' }}</div>
 	<div v-else-if="typeof value === 'string'" class="string">"{{ value }}"</div>
 	<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
-	<div v-else-if="Array.isArray(value)" class="array">
-		<button @click="collapsed_ = !collapsed_">[ {{ collapsed_ ? '+' : '-' }} ]</button>
-		<template v-if="!collapsed_">
-			<div v-for="i in value.length" class="element">
-				{{ i }}: <XValue :value="value[i - 1]" collapsed/>
-			</div>
-		</template>
+	<div v-else-if="isArray(value) && isEmpty(value)" class="array empty">[]</div>
+	<div v-else-if="isArray(value)" class="array">
+		<div v-for="i in value.length" class="element">
+			{{ i }}: <XValue :value="value[i - 1]" collapsed/>
+		</div>
 	</div>
-	<div v-else-if="typeof value === 'object'" class="object">
-		<button @click="collapsed_ = !collapsed_">{ {{ collapsed_ ? '+' : '-' }} }</button>
-		<template v-if="!collapsed_">
-			<div v-for="k in Object.keys(value)" class="kv">
-				<div class="k">{{ k }}:</div>
-				<div class="v"><XValue :value="value[k]" collapsed/></div>
+	<div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div>
+	<div v-else-if="isObject(value)" class="object">
+		<div v-for="k in Object.keys(value)" class="kv">
+			<button class="toggle _button" :class="{ visible: collapsable(value[k]) }" @click="collapsed[k] = !collapsed[k]">{{ collapsed[k] ? '+' : '-' }}</button>
+			<div class="k">{{ k }}:</div>
+			<div v-if="collapsed[k]" class="v">
+				<button class="_button" @click="collapsed[k] = !collapsed[k]">
+					<template v-if="typeof value[k] === 'string'">"..."</template>
+					<template v-else-if="isArray(value[k])">[...]</template>
+					<template v-else-if="isObject(value[k])">{...}</template>
+				</button>
 			</div>
-		</template>
+			<div v-else class="v"><XValue :value="value[k]"/></div>
+		</div>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, ref } from 'vue';
+import { computed, defineComponent, reactive, ref } from 'vue';
 import number from '@/filters/number';
 
 export default defineComponent({
@@ -33,24 +37,44 @@ export default defineComponent({
 
 	props: {
 		value: {
-			type: Object,
 			required: true,
 		},
-		collapsed: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
 	},
 
 	setup(props) {
-		const collapsed_ = ref(props.collapsed);
+		const collapsed = reactive({});
+
+		if (isObject(props.value)) {
+			for (const key in props.value) {
+				collapsed[key] = collapsable(props.value[key]);
+			}
+		}
+
+		function isObject(v): boolean {
+			return typeof v === 'object' && !Array.isArray(v) && v !== null;
+		}
+
+		function isArray(v): boolean {
+			return Array.isArray(v);
+		}
+
+		function isEmpty(v): boolean {
+			return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
+		}
+
+		function collapsable(v): boolean {
+			return (isObject(v) || isArray(v)) && !isEmpty(v);
+		}
 
 		return {
 			number,
-			collapsed_,
+			collapsed,
+			isObject,
+			isArray,
+			isEmpty,
+			collapsable,
 		};
-	}
+	},
 });
 </script>
 
@@ -66,6 +90,14 @@ export default defineComponent({
 	> .boolean {
 		display: inline;
 		color: var(--codeBoolean);
+
+		&.true {
+			font-weight: bold;
+		}
+
+		&.false {
+			opacity: 0.7;
+		}
 	}
 
 	> .string {
@@ -78,7 +110,12 @@ export default defineComponent({
 		color: var(--codeNumber);
 	}
 
-	> .array {
+	> .array.empty {
+		display: inline;
+		opacity: 0.7;
+	}
+
+	> .array:not(.empty) {
 		display: inline;
 
 		> .element {
@@ -87,13 +124,28 @@ export default defineComponent({
 		}
 	}
 
-	> .object {
+	> .object.empty {
+		display: inline;
+		opacity: 0.7;
+	}
+
+	> .object:not(.empty) {
 		display: inline;
 
 		> .kv {
 			display: block;
 			padding-left: 16px;
 
+			> .toggle {
+				width: 16px;
+				color: var(--accent);
+				visibility: hidden;
+
+				&.visible {
+					visibility: visible;
+				}
+			}
+
 			> .k {
 				display: inline;
 				margin-right: 8px;
diff --git a/packages/client/src/components/object-view.vue b/packages/client/src/components/object-view.vue
index e9db96de8..db66049fc 100644
--- a/packages/client/src/components/object-view.vue
+++ b/packages/client/src/components/object-view.vue
@@ -4,26 +4,13 @@
 </div>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XValue from './object-view.value.vue';
 
-export default defineComponent({
-	components: {
-		XValue
-	},
-
-	props: {
-		value: {
-			type: Object,
-			required: true,
-		},
-	},
-
-	setup(props) {
-
-	}
-});
+const props = defineProps<{
+	value: Record<string, unknown>;
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index bacfab771..de89e3593 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -73,7 +73,7 @@
 	<MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20">
 		<XFederation/>
 	</MkSpacer>
-	<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
+	<MkSpacer v-else-if="tab === 'charts'" :content-max="1000" :margin-min="20">
 		<MkInstanceStats :chart-limit="500" :detailed="true"/>
 	</MkSpacer>
 </MkStickyContainer>
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index 9dfb2d87a..76b772ece 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -294,7 +294,7 @@ const headerTabs = $computed(() => [{
 	icon: 'fas fa-share-alt',
 }, {
 	key: 'raw',
-	title: 'Raw data',
+	title: 'Raw',
 	icon: 'fas fa-code',
 }].filter(x => x != null));
 

From c02f155c704967b67b63b28bc782f1bdb11d10de Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 16:00:00 +0900
Subject: [PATCH 006/100] enhance(client): improve router

Fix #8902
---
 packages/client/src/nirax.ts              | 11 +++++++++++
 packages/client/src/pages/about.vue       |  8 +++++++-
 packages/client/src/pages/admin/index.vue |  2 +-
 packages/client/src/router.ts             |  5 +++--
 4 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts
index 6db633566..9dc787de4 100644
--- a/packages/client/src/nirax.ts
+++ b/packages/client/src/nirax.ts
@@ -8,6 +8,7 @@ type RouteDef = {
 	component: Component;
 	query?: Record<string, string>;
 	name?: string;
+	hash?: string;
 	globalCacheKey?: string;
 };
 
@@ -78,7 +79,12 @@ export class Router extends EventEmitter<{
 
 	public resolve(path: string): { route: RouteDef; props: Map<string, string>; } | null {
 		let queryString: string | null = null;
+		let hash: string | null = null;
 		if (path[0] === '/') path = path.substring(1);
+		if (path.includes('#')) {
+			hash = path.substring(path.indexOf('#') + 1);
+			path = path.substring(0, path.indexOf('#'));
+		}
 		if (path.includes('?')) {
 			queryString = path.substring(path.indexOf('?') + 1);
 			path = path.substring(0, path.indexOf('?'));
@@ -127,6 +133,10 @@ export class Router extends EventEmitter<{
 
 			if (parts.length !== 0) continue forEachRouteLoop;
 
+			if (route.hash != null && hash != null) {
+				props.set(route.hash, hash);
+			}
+
 			if (route.query != null && queryString != null) {
 				const queryObject = [...new URLSearchParams(queryString).entries()]
 					.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
@@ -138,6 +148,7 @@ export class Router extends EventEmitter<{
 					}
 				}
 			}
+
 			return {
 				route,
 				props,
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index de89e3593..683f7446b 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -95,8 +95,14 @@ import number from '@/filters/number';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 
+const props = withDefaults(defineProps<{
+	initialTab?: string;
+}>(), {
+	initialTab: 'overview',
+});
+
 let stats = $ref(null);
-let tab = $ref('overview');
+let tab = $ref(props.initialTab);
 
 const initStats = () => os.api('stats', {
 }).then((res) => {
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index 5ffbe3495..d967a081b 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -109,7 +109,7 @@ const menuDef = $computed(() => [{
 	}, {
 		icon: 'fas fa-globe',
 		text: i18n.ts.federation,
-		to: '/admin/federation',
+		to: '/about#federation',
 		active: props.initialPage === 'federation',
 	}, {
 		icon: 'fas fa-clipboard-list',
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index a452a8bb3..d257612f3 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -50,6 +50,7 @@ export const routes = [{
 }, {
 	path: '/about',
 	component: page(() => import('./pages/about.vue')),
+	hash: 'initialTab',
 }, {
 	path: '/about-misskey',
 	component: page(() => import('./pages/about-misskey.vue')),
@@ -203,7 +204,7 @@ export const routes = [{
 	component: page(() => import('./pages/not-found.vue')),
 }];
 
-export const mainRouter = new Router(routes, location.pathname + location.search);
+export const mainRouter = new Router(routes, location.pathname + location.search + location.hash);
 
 window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
 
@@ -228,7 +229,7 @@ mainRouter.addListener('push', ctx => {
 });
 
 window.addEventListener('popstate', (event) => {
-	mainRouter.change(location.pathname + location.search, event.state?.key);
+	mainRouter.change(location.pathname + location.search + location.hash, event.state?.key);
 	const scrollPos = scrollPosStore.get(event.state?.key) ?? 0;
 	window.scroll({ top: scrollPos, behavior: 'instant' });
 	window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール

From 7bc77f6137628f6851e549f6f0eaac2d2b83a0cf Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 16:06:13 +0900
Subject: [PATCH 007/100] perf(client): remove needless reactivity

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

diff --git a/packages/client/src/components/global/router-view.vue b/packages/client/src/components/global/router-view.vue
index 393ba30c3..56b53e012 100644
--- a/packages/client/src/components/global/router-view.vue
+++ b/packages/client/src/components/global/router-view.vue
@@ -21,7 +21,7 @@ if (router == null) {
 	throw new Error('no router provided');
 }
 
-let currentPageComponent = $ref(router.getCurrentComponent());
+let currentPageComponent = $shallowRef(router.getCurrentComponent());
 let currentPageProps = $ref(router.getCurrentProps());
 let key = $ref(router.getCurrentKey());
 

From 65f39917734e8b473bb46a9dccb53bc5073430a1 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 16:07:38 +0900
Subject: [PATCH 008/100] chore(client): fix type def

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

diff --git a/packages/client/src/components/form/split.vue b/packages/client/src/components/form/split.vue
index 676b29396..301a8a84e 100644
--- a/packages/client/src/components/form/split.vue
+++ b/packages/client/src/components/form/split.vue
@@ -6,9 +6,9 @@
 
 <script lang="ts" setup>
 const props = withDefaults(defineProps<{
-	minWidth: number;
+	minWidth?: number;
 }>(), {
-  minWidth: 210,
+	minWidth: 210,
 });
 
 const minWidth = props.minWidth + 'px';

From ef9c59430b64ddd242f21c837d19d3c10488912a Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Wed, 29 Jun 2022 11:26:06 +0200
Subject: [PATCH 009/100]  Prevent access to user pages when not logged in [v2]
 (#8904)

* do not throw error when navigating

* enhance: add loginRequired to router

This allows client pages to require logging in before displaying the
page, useful for example for user settings pages.

* add login requirements

Co-authored-by: Andreas Nedbal <git@pixelde.su>
---
 packages/client/src/nirax.ts                |  6 +++++
 packages/client/src/router.ts               | 27 +++++++++++++++++++++
 packages/client/src/scripts/please-login.ts |  2 +-
 3 files changed, 34 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts
index 9dc787de4..19c4464ea 100644
--- a/packages/client/src/nirax.ts
+++ b/packages/client/src/nirax.ts
@@ -2,11 +2,13 @@
 
 import { EventEmitter } from 'eventemitter3';
 import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue';
+import { pleaseLogin } from '@/scripts/please-login';
 
 type RouteDef = {
 	path: string;
 	component: Component;
 	query?: Record<string, string>;
+	loginRequired?: boolean;
 	name?: string;
 	hash?: string;
 	globalCacheKey?: string;
@@ -169,6 +171,10 @@ export class Router extends EventEmitter<{
 			throw new Error('no route found for: ' + path);
 		}
 
+		if (res.route.loginRequired) {
+			pleaseLogin('/');
+		}
+
 		const isSamePath = beforePath === path;
 		if (isSamePath && key == null) key = this.currentKey;
 		this.currentComponent = res.route.component;
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index d257612f3..54cd7231d 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -38,6 +38,7 @@ export const routes = [{
 	name: 'settings',
 	path: '/settings/:initialPage(*)?',
 	component: page(() => import('./pages/settings/index.vue')),
+	loginRequired: true,
 }, {
 	path: '/reset-password/:token?',
 	component: page(() => import('./pages/reset-password.vue')),
@@ -60,6 +61,7 @@ export const routes = [{
 }, {
 	path: '/theme-editor',
 	component: page(() => import('./pages/theme-editor.vue')),
+	loginRequired: true,
 }, {
 	path: '/explore/tags/:tag',
 	component: page(() => import('./pages/explore.vue')),
@@ -76,12 +78,15 @@ export const routes = [{
 }, {
 	path: '/authorize-follow',
 	component: page(() => import('./pages/follow.vue')),
+	loginRequired: true,
 }, {
 	path: '/share',
 	component: page(() => import('./pages/share.vue')),
+	loginRequired: true,
 }, {
 	path: '/api-console',
 	component: page(() => import('./pages/api-console.vue')),
+	loginRequired: true,
 }, {
 	path: '/mfm-cheat-sheet',
 	component: page(() => import('./pages/mfm-cheat-sheet.vue')),
@@ -109,18 +114,22 @@ export const routes = [{
 }, {
 	path: '/pages/new',
 	component: page(() => import('./pages/page-editor/page-editor.vue')),
+	loginRequired: true,
 }, {
 	path: '/pages/edit/:initPageId',
 	component: page(() => import('./pages/page-editor/page-editor.vue')),
+	loginRequired: true,
 }, {
 	path: '/pages',
 	component: page(() => import('./pages/pages.vue')),
 }, {
 	path: '/gallery/:postId/edit',
 	component: page(() => import('./pages/gallery/edit.vue')),
+	loginRequired: true,
 }, {
 	path: '/gallery/new',
 	component: page(() => import('./pages/gallery/edit.vue')),
+	loginRequired: true,
 }, {
 	path: '/gallery/:postId',
 	component: page(() => import('./pages/gallery/post.vue')),
@@ -130,9 +139,11 @@ export const routes = [{
 }, {
 	path: '/channels/:channelId/edit',
 	component: page(() => import('./pages/channel-editor.vue')),
+	loginRequired: true,
 }, {
 	path: '/channels/new',
 	component: page(() => import('./pages/channel-editor.vue')),
+	loginRequired: true,
 }, {
 	path: '/channels/:channelId',
 	component: page(() => import('./pages/channel.vue')),
@@ -148,52 +159,68 @@ export const routes = [{
 }, {
 	path: '/my/notifications',
 	component: page(() => import('./pages/notifications.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/favorites',
 	component: page(() => import('./pages/favorites.vue')),
+	loginRequired: true,
 }, {
 	name: 'messaging',
 	path: '/my/messaging',
 	component: page(() => import('./pages/messaging/index.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/messaging/:userAcct',
 	component: page(() => import('./pages/messaging/messaging-room.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/messaging/group/:groupId',
 	component: page(() => import('./pages/messaging/messaging-room.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/drive/folder/:folder',
 	component: page(() => import('./pages/drive.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/drive',
 	component: page(() => import('./pages/drive.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/follow-requests',
 	component: page(() => import('./pages/follow-requests.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/lists/:listId',
 	component: page(() => import('./pages/my-lists/list.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/lists',
 	component: page(() => import('./pages/my-lists/index.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/clips',
 	component: page(() => import('./pages/my-clips/index.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/antennas/create',
 	component: page(() => import('./pages/my-antennas/create.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/antennas/:antennaId',
 	component: page(() => import('./pages/my-antennas/edit.vue')),
+	loginRequired: true,
 }, {
 	path: '/my/antennas',
 	component: page(() => import('./pages/my-antennas/index.vue')),
+	loginRequired: true,
 }, {
 	path: '/timeline/list/:listId',
 	component: page(() => import('./pages/user-list-timeline.vue')),
+	loginRequired: true,
 }, {
 	path: '/timeline/antenna/:antennaId',
 	component: page(() => import('./pages/antenna-timeline.vue')),
+	loginRequired: true,
 }, {
 	name: 'index',
 	path: '/',
diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts
index e21a6d2ed..1f3806184 100644
--- a/packages/client/src/scripts/please-login.ts
+++ b/packages/client/src/scripts/please-login.ts
@@ -17,5 +17,5 @@ export function pleaseLogin(path?: string) {
 		},
 	}, 'closed');
 
-	throw new Error('signin required');
+	if (!path) throw new Error('signin required');
 }

From 6d51e4aa4fef428a6e8c008134fcf660c6638d5e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 21:22:15 +0900
Subject: [PATCH 010/100] feat(client): add tag cloud component

---
 packages/client/assets/tagcanvas.min.js      | 21 +++++
 packages/client/src/components/tag-cloud.vue | 80 ++++++++++++++++++++
 packages/client/src/pages/admin/overview.vue | 33 +++++++-
 3 files changed, 133 insertions(+), 1 deletion(-)
 create mode 100644 packages/client/assets/tagcanvas.min.js
 create mode 100644 packages/client/src/components/tag-cloud.vue

diff --git a/packages/client/assets/tagcanvas.min.js b/packages/client/assets/tagcanvas.min.js
new file mode 100644
index 000000000..bcee46e68
--- /dev/null
+++ b/packages/client/assets/tagcanvas.min.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (C) 2010-2021 Graham Breach
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/**
+ * TagCanvas 2.11
+ * For more information, please contact <graham@goat1000.com>
+ */
+ (function(){"use strict";var r,C,p=Math.abs,o=Math.sin,l=Math.cos,g=Math.max,h=Math.min,af=Math.ceil,E=Math.sqrt,w=Math.pow,I={},D={},R={0:"0,",1:"17,",2:"34,",3:"51,",4:"68,",5:"85,",6:"102,",7:"119,",8:"136,",9:"153,",a:"170,",A:"170,",b:"187,",B:"187,",c:"204,",C:"204,",d:"221,",D:"221,",e:"238,",E:"238,",f:"255,",F:"255,"},f,d,b,T,z,F,M,c=document,v,e,P,j={};for(r=0;r<256;++r)C=r.toString(16),r<16&&(C='0'+C),D[C]=D[C.toUpperCase()]=r.toString()+',';function n(a){return typeof a!='undefined'}function B(a){return typeof a=='object'&&a!=null}function G(a,c,b){return isNaN(a)?b:h(b,g(c,a))}function x(){return!1}function q(){return(new Date).valueOf()}function ak(c,d){var b=[],e=c.length,a;for(a=0;a<e;++a)b.push(c[a]);return b.sort(d),b}function ai(a){for(var b=a.length-1,d,c;b;)c=~~(Math.random()*b),d=a[b],a[b]=a[c],a[c]=d,--b}function ag(){var a=window.AudioContext||window.webkitAudioContext;if(e=new a,!e){e='off';return}return e}function $(u,a,t,s,r,q,p){var j=s,h=r,i=t*.01,n=80*i,m=100*i,o=40*i,l=30*i,c=l/2,b=j+n,f=b-o,k=h+m,d=k-l,g=h+l,e=h+m/2;if(a.setTransform(1,0,0,1,0,0),a.setLineDash([]),a.globalAlpha=1,a.strokeStyle=p,a.lineWidth=q,a.lineJoin='round',a.beginPath(),a.moveTo(f,g),a.lineTo(f,d),a.moveTo(b,k),a.lineTo(f,d),a.lineTo(j,d),a.lineTo(j,g),a.lineTo(f,g),a.lineTo(b,h),u){a.lineTo(b,g),a.moveTo(b,d),a.lineTo(b,k),a.moveTo(b-c,e-c),a.lineTo(b+c,e+c),a.moveTo(b+c,e-c),a.lineTo(b-c,e+c),a.stroke();return}a.closePath(),a.stroke()}function s(a,b,c){this.x=a,this.y=b,this.z=c}z=s.prototype,z.length=function(){return E(this.x*this.x+this.y*this.y+this.z*this.z)},z.dot=function(a){return this.x*a.x+this.y*a.y+this.z*a.z},z.cross=function(a){var b=this.y*a.z-this.z*a.y,c=this.z*a.x-this.x*a.z,d=this.x*a.y-this.y*a.x;return new s(b,c,d)},z.angle=function(b){var c=this.dot(b),a;return c==0?Math.PI/2:(a=c/(this.length()*b.length()),a>=1)?0:a<=-1?Math.PI:Math.acos(a)},z.unit=function(){var a=this.length();return new s(this.x/a,this.y/a,this.z/a)};function ay(b,a){a=a*Math.PI/180,b=b*Math.PI/180;var c=o(b)*l(a),d=-o(a),e=-l(b)*l(a);return new s(c,d,e)}function m(a){this[1]={1:a[0],2:a[1],3:a[2]},this[2]={1:a[3],2:a[4],3:a[5]},this[3]={1:a[6],2:a[7],3:a[8]}}T=m.prototype,m.Identity=function(){return new m([1,0,0,0,1,0,0,0,1])},m.Rotation=function(e,a){var c=o(e),d=l(e),b=1-d;return new m([d+w(a.x,2)*b,a.x*a.y*b-a.z*c,a.x*a.z*b+a.y*c,a.y*a.x*b+a.z*c,d+w(a.y,2)*b,a.y*a.z*b-a.x*c,a.z*a.x*b-a.y*c,a.z*a.y*b+a.x*c,d+w(a.z,2)*b])},T.mul=function(c){var d=[],a,b,e=c.xform?1:0;for(a=1;a<=3;++a)for(b=1;b<=3;++b)e?d.push(this[a][1]*c[1][b]+this[a][2]*c[2][b]+this[a][3]*c[3][b]):d.push(this[a][b]*c);return new m(d)},T.xform=function(b){var a={},c=b.x,d=b.y,e=b.z;return a.x=c*this[1][1]+d*this[2][1]+e*this[3][1],a.y=c*this[1][2]+d*this[2][2]+e*this[3][2],a.z=c*this[1][3]+d*this[2][3]+e*this[3][3],a};function aB(g,j,k,m,f){var a,b,c,d,e=[],h=2/g,i;i=Math.PI*(3-E(5)+(parseFloat(f)?parseFloat(f):0));for(a=0;a<g;++a)b=a*h-1+h/2,c=E(1-b*b),d=a*i,e.push([l(d)*c*j,b*k,o(d)*c*m]);return e}function U(n,p,m,k,h,g){var b,f=[],i=2/n,j,a,d,c,e;j=Math.PI*(3-E(5)+(parseFloat(g)?parseFloat(g):0));for(a=0;a<n;++a)d=a*i-1+i/2,b=a*j,c=l(b),e=o(b),f.push(p?[d*m,c*k,e*h]:[c*m,d*k,e*h]);return f}function aa(k,e,f,h,i,j){var b,g=[],m=Math.PI*2/e,a,c,d;for(a=0;a<e;++a)b=a*m,c=l(b),d=o(b),g.push(k?[j*f,c*h,d*i]:[c*f,j*h,d*i]);return g}function ax(a,b,c,d,e){return U(a,0,b,c,d,e)}function aH(a,b,c,d,e){return U(a,1,b,c,d,e)}function aG(b,c,d,e,a){return a=isNaN(a)?0:a*1,aa(0,b,c,d,e,a)}function aF(b,c,d,e,a){return a=isNaN(a)?0:a*1,aa(1,b,c,d,e,a)}function av(b){var a=new Image;a.onload=function(){var c=a.width/2,d=a.height/2;b.centreFunc=function(b,g,h,e,f){b.setTransform(1,0,0,1,0,0),b.globalAlpha=1,b.drawImage(a,e-c,f-d)}},a.src=b.centreImage}function aE(a,c){var b=a,d,e,f=(c*1).toPrecision(3)+')';return a[0]==='#'?(I[a]||(a.length===4?I[a]='rgba('+R[a[1]]+R[a[2]]+R[a[3]]:I[a]='rgba('+D[a.substr(1,2)]+D[a.substr(3,2)]+D[a.substr(5,2)]),b=I[a]+f):a.substr(0,4)==='rgb('||a.substr(0,4)==='hsl('?b=a.replace('(','a(').replace(')',','+f):(a.substr(0,5)==='rgba('||a.substr(0,5)==='hsla(')&&(d=a.lastIndexOf(',')+1,e=a.indexOf(')'),c*=parseFloat(a.substring(d,e)),b=a.substr(0,d)+c.toPrecision(3)+')'),b}function k(b,d){if(window.G_vmlCanvasManager)return null;var a=c.createElement('canvas');return a.width=b,a.height=d,a}function aD(){var b=k(3,3),a,c;return!!b&&(a=b.getContext('2d'),a.strokeStyle='#000',a.shadowColor='#fff',a.shadowBlur=3,a.globalAlpha=0,a.strokeRect(2,2,2,2),a.globalAlpha=1,c=a.getImageData(2,2,1,1),b=null,c.data[0]>0)}function aC(a,c,f,d){var e=a.createLinearGradient(0,0,c,0),b;for(b in d)e.addColorStop(1-b,d[b]);a.fillStyle=e,a.fillRect(0,f,c,1)}function L(a,m,j){var l=1024,d=1,e=a.weightGradient,i,f,b,c;if(a.gCanvas)f=a.gCanvas.getContext('2d'),d=a.gCanvas.height;else{if(B(e[0])?d=e.length:e=[e],a.gCanvas=i=k(l,d),!i)return null;f=i.getContext('2d');for(b=0;b<d;++b)aC(f,l,b,e[b])}return j=g(h(j||0,d-1),0),c=f.getImageData(~~((l-1)*m),j,1,1).data,'rgba('+c[0]+','+c[1]+','+c[2]+','+c[3]/255+')'}function Y(b,i,q,k,o,n,h,d,a,g,f,l){var m=o+(d||0)+(a.length&&a[0]<0?p(a[0]):0),j=n+(d||0)+(a.length&&a[1]<0?p(a[1]):0),c,e;b.font=i,b.textBaseline='top',b.fillStyle=q,h&&(b.shadowColor=h),d&&(b.shadowBlur=d),a.length&&(b.shadowOffsetX=a[0],b.shadowOffsetY=a[1]);for(c=0;c<k.length;++c)e=0,f&&('right'==l?e=g-f[c]:'centre'==l&&(e=(g-f[c])/2)),b.fillText(k[c],m+e,j),j+=parseInt(i)}function y(d,a,b,f,e,c,g){c?(d.beginPath(),d.moveTo(a,b+e-c),d.arcTo(a,b,a+c,b,c),d.arcTo(a+f,b,a+f,b+c,c),d.arcTo(a+f,b+e,a+f-c,b+e,c),d.arcTo(a,b+e,a,b+e-c,c),d.closePath(),d[g?'stroke':'fill']()):d[g?'strokeRect':'fillRect'](a,b,f,e)}function O(a,b,c,d,e,f,g,h,i){this.strings=a,this.font=b,this.width=c,this.height=d,this.maxWidth=e,this.stringWidths=f,this.align=g,this.valign=h,this.scale=i}M=O.prototype,M.SetImage=function(a,b,c,d,e,f,g,h){this.image=a,this.iwidth=b*this.scale,this.iheight=c*this.scale,this.ipos=d,this.ipad=e*this.scale,this.iscale=h,this.ialign=f,this.ivalign=g},M.Align=function(c,d,a){var b=0;return a=='right'||a=='bottom'?b=d-c:a!='left'&&a!='top'&&(b=(d-c)/2),b},M.Create=function(G,D,F,b,A,m,q,j,E){var o,e,f,a,l,s,i,u,v,r,w,n,c,d,x,B=p(q[0]),C=p(q[1]),t,z;return j=g(j,B+m,C+m),l=2*(j+b),i=2*(j+b),e=this.width+l,f=this.height+i,v=r=j+b,this.image&&(w=n=j+b,c=this.iwidth,d=this.iheight,this.ipos=='top'||this.ipos=='bottom'?(c<this.width?w+=this.Align(c,this.width,this.ialign):v+=this.Align(this.width,c,this.align),this.ipos=='top'?r+=d+this.ipad:n+=this.height+this.ipad,e=g(e,c+l),f+=d+this.ipad):(d<this.height?n+=this.Align(d,this.height,this.ivalign):r+=this.Align(this.height,d,this.valign),this.ipos=='right'?w+=this.width+this.ipad:v+=c+this.ipad,e+=c+this.ipad,f=g(f,d+i))),o=k(e,f),!o?null:(l=i=b/2,s=e-b,u=f-b,x=h(E,s/2,u/2),a=o.getContext('2d'),D&&(a.fillStyle=D,y(a,l,i,s,u,x)),b&&(a.strokeStyle=F,a.lineWidth=b,y(a,l,i,s,u,x,!0)),(m||B||C)&&(t=k(e,f),t&&(z=a,a=t.getContext('2d'))),Y(a,this.font,G,this.strings,v,r,0,0,[],this.maxWidth,this.stringWidths,this.align),this.image&&a.drawImage(this.image,w,n,c,d),z&&(a=z,A&&(a.shadowColor=A),m&&(a.shadowBlur=m),a.shadowOffsetX=q[0],a.shadowOffsetY=q[1],a.drawImage(t,0,0)),o)};function H(a,c,d){var b=k(c,d),e;return b?(e=b.getContext('2d'),e.drawImage(a,(c-a.width)/2,(d-a.height)/2),b):null}function S(e,b,c){var a=k(b,c),d;return a?(d=a.getContext('2d'),d.drawImage(e,0,0,b,c),a):null}function W(n,u,t,e,s,c,v,d,r,w){var g=u+(2*d+c)*e,f=t+(2*d+c)*e,l=k(g,f),b,i,q,m,j,o,a,p;return l?(c*=e,r*=e,i=q=c/2,m=g-c,j=f-c,d=d*e+i,b=l.getContext('2d'),p=h(r,m/2,j/2),s&&(b.fillStyle=s,y(b,i,q,m,j,p)),c&&(b.strokeStyle=v,b.lineWidth=c,y(b,i,q,m,j,p,!0)),w?(o=k(g,f),a=o.getContext('2d'),a.drawImage(n,d,d,u,t),a.globalCompositeOperation='source-in',a.fillStyle=v,a.fillRect(0,0,g,f),a.globalCompositeOperation='destination-over',a.drawImage(l,0,0),a.globalCompositeOperation='source-over',b.drawImage(o,0,0)):b.drawImage(n,d,d,n.width,n.height),{image:l,width:g/e,height:f/e}):null}function at(l,f,c,d,j){var e,a,b=parseFloat(f),i=g(c,d);return e=k(c,d),!e?null:(f.indexOf('%')>0?b=i*b/100:b=b*j,a=e.getContext('2d'),a.globalCompositeOperation='source-over',a.fillStyle='#fff',b>=i/2?(b=h(c,d)/2,a.beginPath(),a.moveTo(c/2,d/2),a.arc(c/2,d/2,b,0,2*Math.PI,!1),a.fill(),a.closePath()):(b=h(c/2,d/2,b),y(a,0,0,c,d,b,!0),a.fill()),a.globalCompositeOperation='source-in',a.drawImage(l,0,0,c,d),e)}function ao(q,m,i,b,h,a,c){var g=p(c[0]),f=p(c[1]),j=m+(g>a?g+a:a*2)*b,l=i+(f>a?f+a:a*2)*b,n=b*((a||0)+(c[0]<0?g:0)),o=b*((a||0)+(c[1]<0?f:0)),e,d;return e=k(j,l),!e?null:(d=e.getContext('2d'),h&&(d.shadowColor=h),a&&(d.shadowBlur=a*b),c&&(d.shadowOffsetX=c[0]*b,d.shadowOffsetY=c[1]*b),d.drawImage(q,n,o,m,i),{image:e,width:j/b,height:l/b})}function ae(m,o,l){var c=parseInt(m.toString().length*l),h=parseInt(l*2*m.length),j=k(c,h),g,i,e,f,b,d,n,a;if(!j)return null;g=j.getContext('2d'),g.fillStyle='#000',g.fillRect(0,0,c,h),Y(g,l+'px '+o,'#fff',m,0,0,0,0,[],'centre'),i=g.getImageData(0,0,c,h),e=i.width,f=i.height,a={min:{x:e,y:f},max:{x:-1,y:-1}};for(d=0;d<f;++d)for(b=0;b<e;++b)n=(d*e+b)*4,i.data[n+1]>0&&(b<a.min.x&&(a.min.x=b),b>a.max.x&&(a.max.x=b),d<a.min.y&&(a.min.y=d),d>a.max.y&&(a.max.y=d));return e!=c&&(a.min.x*=c/e,a.max.x*=c/e),f!=h&&(a.min.y*=c/f,a.max.y*=c/f),j=null,a}function Q(a){return"'"+a.replace(/(\'|\")/g,'').replace(/\s*,\s*/g,"', '")+"'"}function t(b,d,a){a=a||c,a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent('on'+b,d)}function am(b,d,a){a=a||c,a.removeEventListener?a.removeEventListener(b,d):a.detachEvent('on'+b,d)}function A(g,e,j,a,b){var l=b.imageScale,h,c,k,m,f,d;if(!e.complete)return t('load',function(){A(g,e,j,a,b)},e);if(!g.complete)return t('load',function(){A(g,e,j,a,b)},g);if(j&&!j.complete)return t('load',function(){A(g,e,j,a,b)},j);e.width=e.width,e.height=e.height,l&&(g.width=e.width*l,g.height=e.height*l),a.iw=g.width,a.ih=g.height,b.txtOpt&&(c=g,h=b.zoomMax*b.txtScale,f=a.iw*h,d=a.ih*h,f<e.naturalWidth||d<e.naturalHeight?(c=S(g,f,d),c&&(a.fimage=c)):(f=a.iw,d=a.ih,h=1),parseFloat(b.imageRadius)&&(a.image=a.fimage=g=at(a.image,b.imageRadius,f,d,h)),a.HasText()||(b.shadow&&(c=ao(a.image,f,d,h,b.shadow,b.shadowBlur,b.shadowOffset),c&&(a.fimage=c.image,a.w=c.width,a.h=c.height)),(b.bgColour||b.bgOutlineThickness)&&(k=b.bgColour=='tag'?i(a.a,'background-color'):b.bgColour,m=b.bgOutline=='tag'?i(a.a,'color'):b.bgOutline||b.textColour,f=a.fimage.width,d=a.fimage.height,b.outlineMethod=='colour'&&(c=W(a.fimage,f,d,h,k,b.bgOutlineThickness,a.outline.colour,b.padding,b.bgRadius,1),c&&(a.oimage=c.image)),c=W(a.fimage,f,d,h,k,b.bgOutlineThickness,m,b.padding,b.bgRadius),c&&(a.fimage=c.image,a.w=c.width,a.h=c.height)),b.outlineMethod=='size'&&(b.outlineIncrease>0?(a.iw+=2*b.outlineIncrease,a.ih+=2*b.outlineIncrease,f=h*a.iw,d=h*a.ih,c=S(a.fimage,f,d),a.oimage=c,a.fimage=H(a.fimage,a.oimage.width,a.oimage.height)):(f=h*(a.iw+2*b.outlineIncrease),d=h*(a.ih+2*b.outlineIncrease),c=S(a.fimage,f,d),a.oimage=H(c,a.fimage.width,a.fimage.height))))),a.alt=j,a.Init()}function i(a,d){var b=c.defaultView,e=d.replace(/\-([a-z])/g,function(a){return a.charAt(1).toUpperCase()});return b&&b.getComputedStyle&&b.getComputedStyle(a,null).getPropertyValue(d)||a.currentStyle&&a.currentStyle[e]}function aj(c,d,e){var b=1,a;return d?b=1*(c.getAttribute(d)||e):(a=i(c,'font-size'))&&(b=a.indexOf('px')>-1&&a.replace('px','')*1||a.indexOf('pt')>-1&&a.replace('pt','')*1.25||a*3.3),b}function u(a){return a.target&&n(a.target.id)?a.target.id:a.srcElement.parentNode.id}function K(a,c){var b,d,e=parseInt(i(c,'width'))/c.width,f=parseInt(i(c,'height'))/c.height;return n(a.offsetX)?b={x:a.offsetX,y:a.offsetY}:(d=X(c.id),n(a.changedTouches)&&(a=a.changedTouches[0]),a.pageX&&(b={x:a.pageX-d.x,y:a.pageY-d.y})),b&&e&&f&&(b.x/=e,b.y/=f),b}function an(c){var d=c.target||c.fromElement.parentNode,b=a.tc[d.id];b&&(b.mx=b.my=-1,b.UnFreeze(),b.EndDrag())}function ad(e){var g,c=a,b,d,f=u(e);for(g in c.tc)b=c.tc[g],b.tttimer&&(clearTimeout(b.tttimer),b.tttimer=null);f&&c.tc[f]&&(b=c.tc[f],(d=K(e,b.canvas))&&(b.mx=d.x,b.my=d.y,b.Drag(e,d)),b.drawn=0)}function ap(b){var e=a,f=c.addEventListener?0:1,d=u(b);d&&b.button==f&&e.tc[d]&&e.tc[d].BeginDrag(b)}function aq(b){var f=a,g=c.addEventListener?0:1,e=u(b),d;e&&b.button==g&&f.tc[e]&&(d=f.tc[e],ad(b),!d.EndDrag()&&!d.touchState&&d.Clicked(b))}function ar(c){var e=u(c),b=e&&a.tc[e],d;b&&c.changedTouches&&(c.touches.length==1&&b.touchState==0?(b.touchState=1,b.BeginDrag(c),(d=K(c,b.canvas))&&(b.mx=d.x,b.my=d.y,b.drawn=0)):c.targetTouches.length==2&&b.pinchZoom?(b.touchState=3,b.EndDrag(),b.BeginPinch(c)):(b.EndDrag(),b.EndPinch(),b.touchState=0))}function ac(c){var d=u(c),b=d&&a.tc[d];if(b&&c.changedTouches){switch(b.touchState){case 1:b.Draw(),b.Clicked();break;break;case 2:b.EndDrag();break;case 3:b.EndPinch()}b.touchState=0}}function au(c){var f,e=a,b,d,g=u(c);for(f in e.tc)b=e.tc[f],b.tttimer&&(clearTimeout(b.tttimer),b.tttimer=null);if(b=g&&e.tc[g],b&&c.changedTouches&&b.touchState){switch(b.touchState){case 1:case 2:(d=K(c,b.canvas))&&(b.mx=d.x,b.my=d.y,b.Drag(c,d)&&(b.touchState=2));break;case 3:b.Pinch(c)}b.drawn=0}}function ab(b){var d=a,c=u(b);c&&d.tc[c]&&(b.cancelBubble=!0,b.returnValue=!1,b.preventDefault&&b.preventDefault(),d.tc[c].Wheel((b.wheelDelta||b.detail)>0))}function aw(d){var c,b=a;clearTimeout(b.scrollTimer);for(c in b.tc)b.tc[c].Pause();b.scrollTimer=setTimeout(function(){var b,c=a;for(b in c.tc)c.tc[b].Resume()},b.scrollPause)}function al(){Z(q())}function Z(b){var c=a.tc,d;a.NextFrame(a.interval),b=b||q();for(d in c)c[d].Draw(b)}function az(){requestAnimationFrame(Z)}function aA(a){setTimeout(al,a)}function X(f){var g=c.getElementById(f),b=g.getBoundingClientRect(),a=c.documentElement,d=c.body,e=window,h=e.pageXOffset||a.scrollLeft,i=e.pageYOffset||a.scrollTop,j=a.clientLeft||d.clientLeft,k=a.clientTop||d.clientTop;return{x:b.left+h-j,y:b.top+i-k}}function aI(a,b,d,e){var c=a.radius*a.z1/(a.z1+a.z2+b.z);return{x:b.x*c*d,y:b.y*c*e,z:b.z,w:(a.z1-b.z)/a.z2}}function V(a){this.e=a,this.br=0,this.line=[],this.text=[],this.original=a.innerText||a.textContent}F=V.prototype,F.Empty=function(){for(var a=0;a<this.text.length;++a)if(this.text[a].length)return!1;return!0},F.Lines=function(c){var e=c?1:0,b,d,a;c=c||this.e,b=c.childNodes,d=b.length;for(a=0;a<d;++a)b[a].nodeName=='BR'?(this.text.push(this.line.join(' ')),this.br=1):b[a].nodeType==3?this.br?(this.line=[b[a].nodeValue],this.br=0):this.line.push(b[a].nodeValue):this.Lines(b[a]);return e||this.br||this.text.push(this.line.join(' ')),this.text},F.SplitWidth=function(h,e,f,g){var c,b,a,d=[];e.font=g+'px '+f;for(c=0;c<this.text.length;++c){a=this.text[c].split(/\s+/),this.line=[a[0]];for(b=1;b<a.length;++b)e.measureText(this.line.join(' ')+' '+a[b]).width>h?(d.push(this.line.join(' ')),this.line=[a[b]]):this.line.push(a[b]);d.push(this.line.join(' '))}return this.text=d};function _(a,b){this.ts=null,this.tc=a,this.tag=b,this.x=this.y=this.w=this.h=this.sc=1,this.z=0,this.pulse=1,this.pulsate=a.pulsateTo<1,this.colour=a.outlineColour,this.adash=~~a.outlineDash,this.agap=~~a.outlineDashSpace||this.adash,this.aspeed=a.outlineDashSpeed*1,this.colour=='tag'?this.colour=i(b.a,'color'):this.colour=='tagbg'&&(this.colour=i(b.a,'background-color')),this.Draw=this.pulsate?this.DrawPulsate:this.DrawSimple,this.radius=a.outlineRadius|0,this.SetMethod(a.outlineMethod,a.altImage)}f=_.prototype,f.SetMethod=function(a,d){var b={block:['PreDraw','DrawBlock'],colour:['PreDraw','DrawColour'],outline:['PostDraw','DrawOutline'],classic:['LastDraw','DrawOutline'],size:['PreDraw','DrawSize'],none:['LastDraw']},c=b[a]||b.outline;a=='none'?this.Draw=function(){return 1}:this.drawFunc=this[c[1]],this[c[0]]=this.Draw,d&&(this.RealPreDraw=this.PreDraw,this.PreDraw=this.DrawAlt)},f.Update=function(d,e,i,j,a,f,g,h){var b=this.tc.outlineOffset,c=2*b;this.x=a*d+g-b,this.y=a*e+h-b,this.w=a*i+c,this.h=a*j+c,this.sc=a,this.z=f},f.Ants=function(k){if(!this.adash)return;var b=this.adash,c=this.agap,a=this.aspeed,j=b+c,h=0,g=b,f=c,i=0,d=0,e;a&&(d=p(a)*(q()-this.ts)/50,a<0&&(d=864e4-d),a=~~d%j),a?(b>=a?(h=b-a,g=a):(f=j-a,i=c-f),e=[h,f,g,i]):e=[b,c],k.setLineDash(e)},f.DrawOutline=function(a,d,e,b,c,f){var g=h(this.radius,c/2,b/2);a.strokeStyle=f,this.Ants(a),y(a,d,e,b,c,g,!0)},f.DrawSize=function(i,n,m,l,k,j,a,h,g){var f=a.w,e=a.h,c,b,d;return this.pulsate?(a.image?d=(a.image.height+this.tc.outlineIncrease)/a.image.height:d=a.oscale,b=a.fimage||a.image,c=1+(d-1)*(1-this.pulse),a.h*=c,a.w*=c):b=a.oimage,a.alpha=1,a.Draw(i,h,g,b),a.h=e,a.w=f,1},f.DrawColour=function(d,h,i,e,f,g,a,b,c){return a.oimage?(this.pulse<1?(a.alpha=1-w(this.pulse,2),a.Draw(d,b,c,a.fimage),a.alpha=this.pulse):a.alpha=1,a.Draw(d,b,c,a.oimage),1):this[a.image?'DrawColourImage':'DrawColourText'](d,h,i,e,f,g,a,b,c)},f.DrawColourText=function(f,h,i,j,g,e,a,b,c){var d=a.colour;return a.colour=e,a.alpha=1,a.Draw(f,b,c),a.colour=d,1},f.DrawColourImage=function(a,q,p,o,n,m,i,r,l){var f=a.canvas,e=~~g(q,0),d=~~g(p,0),c=h(f.width-e,o)+.5|0,b=h(f.height-d,n)+.5|0,j;return v?(v.width=c,v.height=b):v=k(c,b),!v?this.SetMethod('outline'):(j=v.getContext('2d'),j.drawImage(f,e,d,c,b,0,0,c,b),a.clearRect(e,d,c,b),this.pulsate?i.alpha=1-w(this.pulse,2):i.alpha=1,i.Draw(a,r,l),a.setTransform(1,0,0,1,0,0),a.save(),a.beginPath(),a.rect(e,d,c,b),a.clip(),a.globalCompositeOperation='source-in',a.fillStyle=m,a.fillRect(e,d,c,b),a.restore(),a.globalAlpha=1,a.globalCompositeOperation='destination-over',a.drawImage(v,0,0,c,b,e,d,c,b),a.globalCompositeOperation='source-over',1)},f.DrawAlt=function(b,a,c,d,f,g){var e=this.RealPreDraw(b,a,c,d,f,g);return a.alt&&(a.DrawImage(b,c,d,a.alt),e=1),e},f.DrawBlock=function(a,d,e,b,c,f){var g=h(this.radius,c/2,b/2);a.fillStyle=f,y(a,d,e,b,c,g)},f.DrawSimple=function(a,b,c,d,e,f){var g=this.tc;return a.setTransform(1,0,0,1,0,0),a.strokeStyle=this.colour,a.lineWidth=g.outlineThickness,a.shadowBlur=a.shadowOffsetX=a.shadowOffsetY=0,a.globalAlpha=f?e:1,this.drawFunc(a,this.x,this.y,this.w,this.h,this.colour,b,c,d)},f.DrawPulsate=function(h,d,e,f){var g=q()-this.ts,c=this.tc,b=c.pulsateTo+(1-c.pulsateTo)*(.5+l(2*Math.PI*g/(1e3*c.pulsateTime))/2);return this.pulse=b=a.Smooth(1,b),this.DrawSimple(h,d,e,f,b,1)},f.Active=function(d,a,b){var c=a>=this.x&&b>=this.y&&a<=this.x+this.w&&b<=this.y+this.h;return c?this.ts=this.ts||q():this.ts=null,c},f.PreDraw=f.PostDraw=f.LastDraw=x;function J(a,h,c,b,e,f,g,d,i,j,k,l,m,n){this.tc=a,this.image=null,this.text=h,this.text_original=n,this.line_widths=[],this.title=c.title||null,this.a=c,this.position=new s(b[0],b[1],b[2]),this.x=this.y=this.z=0,this.w=e,this.h=f,this.colour=g||a.textColour,this.bgColour=d||a.bgColour,this.bgRadius=i|0,this.bgOutline=j||this.colour,this.bgOutlineThickness=k|0,this.textFont=l||a.textFont,this.padding=m|0,this.sc=this.alpha=1,this.weighted=!a.weight,this.outline=new _(a,this),this.audio=null}d=J.prototype,d.Init=function(b){var a=this.tc;this.textHeight=a.textHeight,this.HasText()?this.Measure(a.ctxt,a):(this.w=this.iw,this.h=this.ih),this.SetShadowColour=a.shadowAlpha?this.SetShadowColourAlpha:this.SetShadowColourFixed,this.SetDraw(a)},d.Draw=x,d.HasText=function(){return this.text&&this.text[0].length>0},d.EqualTo=function(a){var b=a.getElementsByTagName('img');return this.a.href!=a.href?0:b.length?this.image.src==b[0].src:(a.innerText||a.textContent)==this.text_original},d.SetImage=function(a){this.image=this.fimage=a},d.SetAudio=function(a){this.audio=a,this.audio.load()},d.SetDraw=function(a){this.Draw=this.fimage?a.ie>7?this.DrawImageIE:this.DrawImage:this.DrawText,a.noSelect&&(this.CheckActive=x)},d.MeasureText=function(d){var a,e=this.text.length,b=0,c;for(a=0;a<e;++a)this.line_widths[a]=c=d.measureText(this.text[a]).width,b=g(b,c);return b},d.Measure=function(e,a){var f=ae(this.text,this.textFont,this.textHeight),b,k,h,i,g,l,j,c,d;j=f?f.max.y+f.min.y:this.textHeight,e.font=this.font=this.textHeight+'px '+this.textFont,l=this.MeasureText(e),a.txtOpt&&(b=a.txtScale,k=b*this.textHeight,h=k+'px '+this.textFont,i=[b*a.shadowOffset[0],b*a.shadowOffset[1]],e.font=h,g=this.MeasureText(e),d=new O(this.text,h,g+b,b*j+b,g,this.line_widths,a.textAlign,a.textVAlign,b),this.image&&d.SetImage(this.image,this.iw,this.ih,a.imagePosition,a.imagePadding,a.imageAlign,a.imageVAlign,a.imageScale),c=d.Create(this.colour,this.bgColour,this.bgOutline,b*this.bgOutlineThickness,a.shadow,b*a.shadowBlur,i,b*this.padding,b*this.bgRadius),a.outlineMethod=='colour'?this.oimage=d.Create(this.outline.colour,this.bgColour,this.outline.colour,b*this.bgOutlineThickness,a.shadow,b*a.shadowBlur,i,b*this.padding,b*this.bgRadius):a.outlineMethod=='size'&&(f=ae(this.text,this.textFont,this.textHeight+a.outlineIncrease),k=f.max.y+f.min.y,h=b*(this.textHeight+a.outlineIncrease)+'px '+this.textFont,e.font=h,g=this.MeasureText(e),d=new O(this.text,h,g+b,b*k+b,g,this.line_widths,a.textAlign,a.textVAlign,b),this.image&&d.SetImage(this.image,this.iw+a.outlineIncrease,this.ih+a.outlineIncrease,a.imagePosition,a.imagePadding,a.imageAlign,a.imageVAlign,a.imageScale),this.oimage=d.Create(this.colour,this.bgColour,this.bgOutline,b*this.bgOutlineThickness,a.shadow,b*a.shadowBlur,i,b*this.padding,b*this.bgRadius),this.oscale=this.oimage.width/c.width,a.outlineIncrease>0?c=H(c,this.oimage.width,this.oimage.height):this.oimage=H(this.oimage,c.width,c.height)),c&&(this.fimage=c,l=this.fimage.width/b,j=this.fimage.height/b),this.SetDraw(a),a.txtOpt=!!this.fimage),this.h=j,this.w=l},d.SetFont=function(a,b,c,d){this.textFont=a,this.colour=b,this.bgColour=c,this.bgOutline=d,this.Measure(this.tc.ctxt,this.tc)},d.SetWeight=function(c){var b=this.tc,e=b.weightMode.split(/[, ]/),d,a,f=c.length;if(!this.HasText())return;this.weighted=!0;for(a=0;a<f;++a)d=e[a]||'size','both'==d?(this.Weight(c[a],b.ctxt,b,'size',b.min_weight[a],b.max_weight[a],a),this.Weight(c[a],b.ctxt,b,'colour',b.min_weight[a],b.max_weight[a],a)):this.Weight(c[a],b.ctxt,b,d,b.min_weight[a],b.max_weight[a],a);this.Measure(b.ctxt,b)},d.Weight=function(b,i,a,d,f,h,e){b=isNaN(b)?1:b;var c=(b-f)/(h-f);'colour'==d?this.colour=L(a,c,e):'bgcolour'==d?this.bgColour=L(a,c,e):'bgoutline'==d?this.bgOutline=L(a,c,e):'outline'==d?this.outline.colour=L(a,c,e):'size'==d&&(a.weightSizeMin>0&&a.weightSizeMax>a.weightSizeMin?this.textHeight=a.weightSize*(a.weightSizeMin+(a.weightSizeMax-a.weightSizeMin)*c):this.textHeight=g(1,b*a.weightSize))},d.SetShadowColourFixed=function(a,b,c){a.shadowColor=b},d.SetShadowColourAlpha=function(a,b,c){a.shadowColor=aE(b,c)},d.DrawText=function(a,h,i){var e=this.tc,g=this.x,f=this.y,c=this.sc,b,d;a.globalAlpha=this.alpha,a.fillStyle=this.colour,e.shadow&&this.SetShadowColour(a,e.shadow,this.alpha),a.font=this.font,g+=h/c,f+=i/c-this.h/2;for(b=0;b<this.text.length;++b)d=g,'right'==e.textAlign?d+=this.w/2-this.line_widths[b]:'centre'==e.textAlign?d-=this.line_widths[b]/2:d-=this.w/2,a.setTransform(c,0,0,c,c*d,c*f),a.fillText(this.text[b],0,0),f+=this.textHeight},d.DrawImage=function(b,i,k,l){var e=this.x,f=this.y,a=this.sc,j=l||this.fimage,c=this.w,d=this.h,g=this.alpha,h=this.shadow;b.globalAlpha=g,h&&this.SetShadowColour(b,h,g),e+=i/a-c/2,f+=k/a-d/2,b.setTransform(a,0,0,a,a*e,a*f),b.drawImage(j,0,0,c,d)},d.DrawImageIE=function(b,d,e){var c=this.fimage,a=this.sc,f=c.width=this.w*a,g=c.height=this.h*a,h=this.x*a+d-f/2,i=this.y*a+e-g/2;b.setTransform(1,0,0,1,0,0),b.globalAlpha=this.alpha,b.drawImage(c,h,i)},d.Calc=function(g,e){var a,b=this.tc,d=b.minBrightness,f=b.maxBrightness,c=b.max_radius;return a=g.xform(this.position),this.xformed=a,a=aI(b,a,b.stretchX,b.stretchY),this.x=a.x,this.y=a.y,this.z=a.z,this.sc=a.w,this.alpha=e*G(d+(f-d)*(c-this.z)/(2*c),0,1),this.xformed},d.UpdateActive=function(h,e,f){var a=this.outline,b=this.w,c=this.h,d=this.x-b/2,g=this.y-c/2;return a.Update(d,g,b,c,this.sc,this.z,e,f),a},d.CheckActive=function(a,d,e){var b=this.tc,c=this.UpdateActive(a,d,e);return c.Active(a,b.mx,b.my)?c:null},d.Clicked=function(f){var b=this.a,a=b.target,d=b.href,e;if(a!=''&&a!='_self'){if(self.frames[a])self.frames[a].document.location=d;else{try{if(top.frames[a]){top.frames[a].document.location=d;return}}catch(a){}window.open(d,a)}return}if(c.createEvent){if(e=c.createEvent('MouseEvents'),e.initMouseEvent('click',1,1,window,0,0,0,0,0,0,0,0,0,0,null),!b.dispatchEvent(e))return}else if(b.fireEvent)if(!b.fireEvent('onclick'))return;c.location=d},d.StopAudio=function(){this.audio&&this.playing&&this.audio.pause(),this.stopped=1,this.playing=0},d.PlayAudio=function(){if(e==='off'||this.tc.audioOff)return;if(!e&&!ag())return;var a=this.tc.audio,c=this.tc.gain,d='suspended',b;if(this.audio)if(this.track||(this.track=e.createMediaElementSource(this.audio),this.gain=e.createGain(),this.track.connect(this.gain),this.gain.connect(e.destination)),a=this.audio,c=this.gain,!a.paused)return 1;if(a){if(e.state==d&&e.resume(),e.state==d)return;return c.gain.value=h(2,g(0,this.tc.audioVolume*1)),a.currentTime=0,this.stopped=0,b=a.play(),b!==void 0&&b.then(a=>{this.stopped?this.audio.pause():this.playing=1}),1}};function a(f,o,k){var d,i,b=c.getElementById(f),l=['id','class','innerHTML'];if(!b)throw 0;if(n(window.G_vmlCanvasManager)&&(b=window.G_vmlCanvasManager.initElement(b),this.ie=parseFloat(navigator.appVersion.split('MSIE')[1])),b&&(!b.getContext||!b.getContext('2d').fillText)){i=c.createElement('DIV');for(d=0;d<l.length;++d)i[l[d]]=b[l[d]];throw b.parentNode.insertBefore(i,b),b.parentNode.removeChild(b),0}for(d in a.options)this[d]=k&&n(k[d])?k[d]:n(a[d])?a[d]:a.options[d];if(this.canvas=b,this.ctxt=b.getContext('2d'),this.z1=250/g(this.depth,.001),this.z2=this.z1/this.zoom,this.radius=h(b.height,b.width)*.0075,this.max_radius=100,this.max_weight=[],this.min_weight=[],this.textFont=this.textFont&&Q(this.textFont),this.textHeight*=1,this.imageRadius=this.imageRadius.toString(),this.pulsateTo=G(this.pulsateTo,0,1),this.minBrightness=G(this.minBrightness,0,1),this.maxBrightness=G(this.maxBrightness,this.minBrightness,1),this.ctxt.textBaseline='top',this.lx=(this.lock+'').indexOf('x')+1,this.ly=(this.lock+'').indexOf('y')+1,this.frozen=this.dx=this.dy=this.fixedAnim=this.touchState=0,this.fixedAlpha=1,this.source=o||f,this.repeatTags=h(64,~~this.repeatTags),this.minTags=h(200,~~this.minTags),~~this.scrollPause>0?a.scrollPause=~~this.scrollPause:this.scrollPause=0,this.minTags>0&&this.repeatTags<1&&(d=this.GetTags().length)&&(this.repeatTags=af(this.minTags/d)-1),this.transform=m.Identity(),this.startTime=this.time=q(),this.mx=this.my=-1,this.centreImage&&av(this),this.Animate=this.dragControl?this.AnimateDrag:this.AnimatePosition,this.animTiming=typeof a[this.animTiming]=='function'?a[this.animTiming]:a.Smooth,this.shadowBlur||this.shadowOffset[0]||this.shadowOffset[1]?(this.ctxt.shadowColor=this.shadow,this.shadow=this.ctxt.shadowColor,this.shadowAlpha=aD()):delete this.shadow,this.activeAudio===!1?e='off':this.activeAudio&&this.LoadAudio(),this.Load(),o&&this.hideTags&&function(b){a.loaded?b.HideTags():t('load',function(){b.HideTags()},window)}(this),this.yaw=this.initial?this.initial[0]*this.maxSpeed:0,this.pitch=this.initial?this.initial[1]*this.maxSpeed:0,this.tooltip?(this.ctitle=b.title,b.title='',this.tooltip=='native'?this.Tooltip=this.TooltipNative:(this.Tooltip=this.TooltipDiv,this.ttdiv||(this.ttdiv=c.createElement('div'),this.ttdiv.className=this.tooltipClass,this.ttdiv.style.position='absolute',this.ttdiv.style.zIndex=b.style.zIndex+1,t('mouseover',function(a){a.target.style.display='none'},this.ttdiv),c.body.appendChild(this.ttdiv)))):this.Tooltip=this.TooltipNone,!this.noMouse&&!j[f]){j[f]=[['mousemove',ad],['mouseout',an],['mouseup',aq],['touchstart',ar],['touchend',ac],['touchcancel',ac],['touchmove',au]],this.dragControl&&(j[f].push(['mousedown',ap]),j[f].push(['selectstart',x])),this.wheelZoom&&(j[f].push(['mousewheel',ab]),j[f].push(['DOMMouseScroll',ab])),this.scrollPause&&j[f].push(['scroll',aw,window]);for(d=0;d<j[f].length;++d)i=j[f][d],t(i[0],i[1],i[2]?i[2]:b)}a.started||(a.NextFrame=window.requestAnimationFrame?az:aA,a.interval=this.interval,a.NextFrame(this.interval),a.started=1)}b=a.prototype,b.SourceElements=function(){return c.querySelectorAll?c.querySelectorAll('#'+this.source):[c.getElementById(this.source)]},b.HideTags=function(){var b=this.SourceElements(),a;for(a=0;a<b.length;++a)b[a].style.display='none'},b.GetTags=function(){var e=this.SourceElements(),c,f=[],a,b,d;for(d=0;d<=this.repeatTags;++d)for(a=0;a<e.length;++a){c=e[a].getElementsByTagName('a');for(b=0;b<c.length;++b)f.push(c[b])}return f},b.Message=function(j){var g=[],a,f,b=j.split(''),d,e,h,i;for(a=0;a<b.length;++a)b[a]!=' '&&(f=a-b.length/2,d=c.createElement('A'),d.href='#',d.innerText=b[a],h=100*o(f/9),i=-100*l(f/9),e=new J(this,b[a],d,[h,0,i],2,18,'#000','#fff',0,0,0,'monospace',2,b[a]),e.Init(),g.push(e));return g},b.AddAudio=function(b,c){if(e==='off')return;var a=b.getElementsByTagName('audio');a.length&&(c.SetAudio(a[0]),this.hasAudio=1)},b.CreateTag=function(b){var e,c,a,f,d,g,h,j,k=[0,0,0],l;if('text'!=this.imageMode)if(e=b.getElementsByTagName('img'),e.length)if(c=new Image,c.src=e[0].src,!this.imageMode)return a=new J(this,"",b,k,0,0),a.SetImage(c),A(c,e[0],e[1],a,this),this.AddAudio(b,a),a;if('image'!=this.imageMode&&(d=new V(b),f=d.Lines(),d.Empty()?d=null:(g=this.textFont||Q(i(b,'font-family')),this.splitWidth&&(f=d.SplitWidth(this.splitWidth,this.ctxt,g,this.textHeight)),h=this.bgColour=='tag'?i(b,'background-color'):this.bgColour,j=this.bgOutline=='tag'?i(b,'color'):this.bgOutline)),d||c)return a=new J(this,f,b,k,2,this.textHeight+2,this.textColour||i(b,'color'),h,this.bgRadius,j,this.bgOutlineThickness,g,this.padding,d&&d.original),c?(a.SetImage(c),A(c,e[0],e[1],a,this)):a.Init(),this.AddAudio(b,a),a},b.UpdateTag=function(a,b){var c=this.textColour||i(b,'color'),d=this.textFont||Q(i(b,'font-family')),e=this.bgColour=='tag'?i(b,'background-color'):this.bgColour,f=this.bgOutline=='tag'?i(b,'color'):this.bgOutline;a.a=b,a.title=b.title,(a.colour!=c||a.textFont!=d||a.bgColour!=e||a.bgOutline!=f)&&a.SetFont(d,c,e,f)},b.Weight=function(d){var f=d.length,c,b,a,e=[],g,h=this.weightFrom?this.weightFrom.split(/[, ]/):[null],i=h.length;for(b=0;b<f;++b){e[b]=[];for(a=0;a<i;++a)c=aj(d[b].a,h[a],this.textHeight),(!this.max_weight[a]||c>this.max_weight[a])&&(this.max_weight[a]=c),(!this.min_weight[a]||c<this.min_weight[a])&&(this.min_weight[a]=c),e[b][a]=c}for(a=0;a<i;++a)this.max_weight[a]>this.min_weight[a]&&(g=1);if(g)for(b=0;b<f;++b)d[b].SetWeight(e[b])},b.Load=function(){var c=this.GetTags(),b=[],d,k,l,h,i,j,f,a,e=[],m={sphere:aB,vcylinder:ax,hcylinder:aH,vring:aG,hring:aF};if(c.length){e.length=c.length;for(a=0;a<c.length;++a)e[a]=a;this.shuffleTags&&ai(e),h=100*this.radiusX,i=100*this.radiusY,j=100*this.radiusZ,this.max_radius=g(h,g(i,j));for(a=0;a<c.length;++a)k=this.CreateTag(c[e[a]]),k&&b.push(k);this.weight&&this.Weight(b,!0),this.shapeArgs?this.shapeArgs[0]=b.length:(l=this.shape.toString().split(/[(),]/),d=l.shift(),typeof window[d]=='function'?this.shape=window[d]:this.shape=m[d]||m.sphere,this.shapeArgs=[b.length,h,i,j].concat(l)),f=this.shape.apply(this,this.shapeArgs),this.listLength=b.length;for(a=0;a<b.length;++a)b[a].position=new s(f[a][0],f[a][1],f[a][2])}this.noTagsMessage&&!b.length&&(a=this.imageMode&&this.imageMode!='both'?this.imageMode+' ':'',b=this.Message('No '+a+'tags')),this.taglist=b},b.Update=function(){var e=this.GetTags(),d=[],j=this.taglist,k,f=[],c=[],h,i,g,a,b;if(!this.shapeArgs)return this.Load();if(e.length){g=this.listLength=e.length,i=j.length;for(a=0;a<i;++a)d.push(j[a]),c.push(a);for(a=0;a<g;++a){for(b=0,k=0;b<i;++b)j[b].EqualTo(e[a])&&(this.UpdateTag(d[b],e[a]),k=c[b]=-1);k||f.push(a)}for(a=0,b=0;a<i;++a)c[b]==-1?c.splice(b,1):++b;if(c.length){for(ai(c);c.length&&f.length;)a=c.shift(),b=f.shift(),d[a]=this.CreateTag(e[b]);for(c.sort(function(a,b){return a-b});c.length;)d.splice(c.pop(),1)}for(b=d.length/(f.length+1),a=0;f.length;)d.splice(af(++a*b),0,this.CreateTag(e[f.shift()]));this.shapeArgs[0]=g=d.length,h=this.shape.apply(this,this.shapeArgs);for(a=0;a<g;++a)d[a].position=new s(h[a][0],h[a][1],h[a][2]);this.weight&&this.Weight(d)}this.taglist=d},b.SetShadow=function(a){a.shadowBlur=this.shadowBlur,a.shadowOffsetX=this.shadowOffset[0],a.shadowOffsetY=this.shadowOffset[1]},b.LoadAudio=function(){if(!e&&!ag())return;this.audio=c.createElement('audio'),this.audio.src=this.activeAudio,this.track=e.createMediaElementSource(this.audio),this.gain=e.createGain(),this.track.connect(this.gain),this.gain.connect(e.destination),this.hasAudio=1,P=function(a){e.resume(),c.removeEventListener('click',P)},c.addEventListener('click',P)},b.ShowAudioIcon=function(){var a=this.audioIconSize,c=this.canvas,d=this.ctxt,k=c.width-a-3,f=c.height-a-3,g=this.audioIconThickness,h='#000',i='#fff',j=this.audioIconDark,b=this.audioOff,l='suspended';if(!e)return;b||(b=e.state===l),this.audioIcon&&this.hasAudio&&($(b,d,a,k,f,g+1,j?i:h),$(b,d,a,k,f,g,j?h:i))},b.CheckAudioIcon=function(){var a=this.audioIconSize,b=this.canvas,c=this.audioIconThickness/2,d=b.width-a-3-c,e=b.height-a-3-c;if(this.audioIcon&&this.mx>=d&&this.my>=e)return!0},b.ToggleAudio=function(){var a=this.audioOff||e&&e.state==='suspended';a||this.currentAudio&&this.currentAudio.StopAudio(),this.audioOff=!a},b.Draw=function(s){if(this.paused)return;var l=this.canvas,i=l.width,j=l.height,q=0,p=(s-this.time)*a.interval/1e3,h=i/2+this.offsetX,g=j/2+this.offsetY,d=this.ctxt,b,f,c,o=-1,e=this.taglist,k=e.length,t=this.active&&this.active.tag,m='',u=this.frontSelect,r=this.centreFunc==x,n;if(this.time=s,this.frozen&&this.drawn)return this.Animate(i,j,p);n=this.AnimateFixed(),d.setTransform(1,0,0,1,0,0);for(c=0;c<k;++c)e[c].Calc(this.transform,this.fixedAlpha);if(e=ak(e,function(a,b){return b.z-a.z}),n&&this.fixedAnim.active)b=this.fixedAnim.tag.UpdateActive(d,h,g);else if(this.active=null,this.CheckAudioIcon())m='pointer';else{for(c=0;c<k;++c)f=this.mx>=0&&this.my>=0&&this.taglist[c].CheckActive(d,h,g),f&&f.sc>q&&(!u||f.z<=0)&&(b=f,o=c,b.tag=this.taglist[c],q=f.sc);this.active=b}this.txtOpt||this.shadow&&this.SetShadow(d),d.clearRect(0,0,i,j);for(c=0;c<k;++c){if(!r&&e[c].z<=0){try{this.centreFunc(d,i,j,h,g)}catch(a){alert(a),this.centreFunc=x}r=!0}b&&b.tag==e[c]&&b.PreDraw(d,e[c],h,g)||e[c].Draw(d,h,g),b&&b.tag==e[c]&&b.PostDraw(d)}this.freezeActive&&b?this.Freeze():(this.UnFreeze(),this.drawn=k==this.listLength),this.fixedCallback&&(this.fixedCallback(this,this.fixedCallbackTag),this.fixedCallback=null),n||this.Animate(i,j,p),b&&(b.LastDraw(d),b.tag!=t&&(this.currentAudio&&this.currentAudio!=b.tag&&this.currentAudio.StopAudio(),b.tag.PlayAudio()&&(this.currentAudio=b.tag)),m=this.activeCursor),l.style.cursor=m,this.Tooltip(b,this.taglist[o]),this.audioIcon&&this.ShowAudioIcon()},b.TooltipNone=function(){},b.TooltipNative=function(b,a){b?this.canvas.title=a&&a.title?a.title:'':this.canvas.title=this.ctitle},b.SetTTDiv=function(c,d){var a=this,b=a.ttdiv.style;c!=a.ttdiv.innerHTML&&(b.display='none'),a.ttdiv.innerHTML=c,d&&(d.title=a.ttdiv.innerHTML),b.display=='none'&&!a.tttimer&&(a.tttimer=setTimeout(function(){var c=X(a.canvas.id);b.display='block',b.left=c.x+a.mx+'px',b.top=c.y+a.my+24+'px',a.tttimer=null},a.tooltipDelay))},b.TooltipDiv=function(b,a){b&&a&&a.title?this.SetTTDiv(a.title,a):!b&&this.mx!=-1&&this.my!=-1&&this.ctitle.length?this.SetTTDiv(this.ctitle):this.ttdiv.style.display='none'},b.Transform=function(c,a,b){if(a||b){var d=o(a),e=l(a),f=o(b),g=l(b),h=new m([g,0,f,0,1,0,-f,0,g]),i=new m([1,0,0,0,e,-d,0,d,e]);c.transform=c.transform.mul(h.mul(i))}},b.AnimateFixed=function(){var a,b,c,d,e;return!!(this.fadeIn&&(b=q()-this.startTime,b>=this.fadeIn?(this.fadeIn=0,this.fixedAlpha=1):this.fixedAlpha=b/this.fadeIn),this.fixedAnim)&&(this.fixedAnim.transform||(this.fixedAnim.transform=this.transform),a=this.fixedAnim,b=q()-a.t0,c=a.angle,d,e=this.animTiming(a.t,b),this.transform=a.transform,b>=a.t?(this.fixedCallbackTag=a.tag,this.fixedCallback=a.cb,this.fixedAnim=this.yaw=this.pitch=0):c*=e,d=m.Rotation(c,a.axis),this.transform=this.transform.mul(d),this.fixedAnim!=0)},b.AnimatePosition=function(g,h,f){var a=this,d=a.mx,e=a.my,b,c;!a.frozen&&d>=0&&e>=0&&d<g&&e<h?(b=a.maxSpeed,c=a.reverse?-1:1,a.lx||(a.yaw=(d*2*b/g-b)*c*f),a.ly||(a.pitch=(e*2*b/h-b)*-c*f),a.initial=null):a.initial||(a.frozen&&!a.freezeDecel?a.yaw=a.pitch=0:a.Decel(a)),this.Transform(a,a.pitch,a.yaw)},b.AnimateDrag=function(d,e,c){var a=this,b=100*c*a.maxSpeed/a.max_radius/a.zoom;a.dx||a.dy?(a.lx||(a.yaw=a.dx*b/a.stretchX),a.ly||(a.pitch=a.dy*-b/a.stretchY),a.dx=a.dy=0,a.initial=null):a.initial||a.Decel(a),this.Transform(a,a.pitch,a.yaw)},b.Freeze=function(){this.frozen||(this.preFreeze=[this.yaw,this.pitch],this.frozen=1,this.drawn=0)},b.UnFreeze=function(){this.frozen&&(this.yaw=this.preFreeze[0],this.pitch=this.preFreeze[1],this.frozen=0)},b.Decel=function(a){var b=a.minSpeed,c=p(a.yaw),d=p(a.pitch);!a.lx&&c>b&&(a.yaw=c>a.z0?a.yaw*a.decel:0),!a.ly&&d>b&&(a.pitch=d>a.z0?a.pitch*a.decel:0)},b.Zoom=function(a){this.z2=this.z1*(1/a),this.drawn=0},b.Clicked=function(b){if(this.CheckAudioIcon()){this.ToggleAudio();return}var a=this.active;try{a&&a.tag&&(this.clickToFront===!1||this.clickToFront===null?a.tag.Clicked(b):this.TagToFront(a.tag,this.clickToFront,function(){a.tag.Clicked(b)},!0))}catch(a){}},b.Wheel=function(a){var b=this.zoom+this.zoomStep*(a?1:-1);this.zoom=h(this.zoomMax,g(this.zoomMin,b)),this.Zoom(this.zoom)},b.BeginDrag=function(a){this.down=K(a,this.canvas),a.cancelBubble=!0,a.returnValue=!1,a.preventDefault&&a.preventDefault()},b.Drag=function(e,a){if(this.dragControl&&this.down){var d=this.dragThreshold*this.dragThreshold,b=a.x-this.down.x,c=a.y-this.down.y;(this.dragging||b*b+c*c>d)&&(this.dx=b,this.dy=c,this.dragging=1,this.down=a)}return this.dragging},b.EndDrag=function(){var a=this.dragging;return this.dragging=this.down=null,a};function ah(a){var b=a.targetTouches[0],c=a.targetTouches[1];return E(w(c.pageX-b.pageX,2)+w(c.pageY-b.pageY,2))}b.BeginPinch=function(a){this.pinched=[ah(a),this.zoom],a.preventDefault&&a.preventDefault()},b.Pinch=function(d){var b,c,a=this.pinched;if(!a)return;c=ah(d),b=a[1]*c/a[0],this.zoom=h(this.zoomMax,g(this.zoomMin,b)),this.Zoom(this.zoom)},b.EndPinch=function(a){this.pinched=null},b.Pause=function(){this.paused=!0},b.Resume=function(){this.paused=!1},b.SetSpeed=function(a){this.initial=a,this.yaw=a[0]*this.maxSpeed,this.pitch=a[1]*this.maxSpeed},b.FindTag=function(a){if(!n(a))return null;if(n(a.index)&&(a=a.index),!B(a))return this.taglist[a];var c,d,b;n(a.id)?(c='id',d=a.id):n(a.text)&&(c='innerText',d=a.text);for(b=0;b<this.taglist.length;++b)if(this.taglist[b].a[c]==d)return this.taglist[b]},b.RotateTag=function(a,h,i,j,f,g){var b=a.Calc(this.transform,1),c=new s(b.x,b.y,b.z),d=ay(i,h),e=c.angle(d),k=c.cross(d).unit();e==0?(this.fixedCallbackTag=a,this.fixedCallback=f):this.fixedAnim={angle:-e,axis:k,t:j,t0:q(),cb:f,tag:a,active:g}},b.TagToFront=function(a,b,c,d){this.RotateTag(a,0,0,b,c,d)},b.Volume=function(a){this.audioVolume=a*1},a.Start=function(b,c,d){a.Delete(b),a.tc[b]=new a(b,c,d)};function N(c,b){a.tc[b]&&a.tc[b][c]()}a.Linear=function(a,b){return b/a},a.Smooth=function(a,b){return.5-l(b*Math.PI/a)/2},a.Pause=function(a){N('Pause',a)},a.Resume=function(a){N('Resume',a)},a.Reload=function(a){N('Load',a)},a.Update=function(a){N('Update',a)},a.SetSpeed=function(c,b){return!!(B(b)&&a.tc[c]&&!isNaN(b[0])&&!isNaN(b[1]))&&(a.tc[c].SetSpeed(b),!0)},a.TagToFront=function(c,b){return!!B(b)&&(b.lat=b.lng=0,a.RotateTag(c,b))},a.RotateTag=function(c,b){if(B(b)&&a.tc[c]){isNaN(b.time)&&(b.time=500);var d=a.tc[c].FindTag(b);if(d)return a.tc[c].RotateTag(d,b.lat,b.lng,b.time,b.callback,b.active),!0}return!1},a.Delete=function(b){var d,e;if(j[b])if(e=c.getElementById(b),e)for(d=0;d<j[b].length;++d)am(j[b][d][0],j[b][d][1],e);delete j[b],delete a.tc[b]},a.tc={},a.options={z1:2e4,z2:2e4,z0:2e-4,freezeActive:!1,freezeDecel:!1,activeCursor:'pointer',pulsateTo:1,pulsateTime:3,reverse:!1,depth:.5,maxSpeed:.05,minSpeed:0,decel:.95,interval:20,minBrightness:.1,maxBrightness:1,outlineColour:'#ffff99',outlineThickness:2,outlineOffset:5,outlineMethod:'outline',outlineRadius:0,textColour:'#ff99ff',textHeight:15,textFont:'Helvetica, Arial, sans-serif',shadow:'#000',shadowBlur:0,shadowOffset:[0,0],initial:null,hideTags:!0,zoom:1,weight:!1,weightMode:'size',weightFrom:null,weightSize:1,weightSizeMin:null,weightSizeMax:null,weightGradient:{0:'#f00',0.33:'#ff0',0.66:'#0f0',1:'#00f'},txtOpt:!0,txtScale:2,frontSelect:!1,wheelZoom:!0,zoomMin:.3,zoomMax:3,zoomStep:.05,shape:'sphere',lock:null,tooltip:null,tooltipDelay:300,tooltipClass:'tctooltip',radiusX:1,radiusY:1,radiusZ:1,stretchX:1,stretchY:1,offsetX:0,offsetY:0,shuffleTags:!1,noSelect:!1,noMouse:!1,imageScale:1,paused:!1,dragControl:!1,dragThreshold:4,centreFunc:x,splitWidth:0,animTiming:'Smooth',clickToFront:!1,fadeIn:0,padding:0,bgColour:null,bgRadius:0,bgOutline:null,bgOutlineThickness:0,outlineIncrease:4,textAlign:'centre',textVAlign:'middle',imageMode:null,imagePosition:null,imagePadding:2,imageAlign:'centre',imageVAlign:'middle',noTagsMessage:!0,centreImage:null,pinchZoom:!1,repeatTags:0,minTags:0,imageRadius:0,scrollPause:!1,outlineDash:0,outlineDashSpace:0,outlineDashSpeed:1,activeAudio:'',audioVolume:1,audioIcon:1,audioIconSize:20,audioIconThickness:2,audioIconDark:0,altImage:0};for(r in a.options)a[r]=a.options[r];window.TagCanvas=a,t('load',function(){a.loaded=1},window)})()
diff --git a/packages/client/src/components/tag-cloud.vue b/packages/client/src/components/tag-cloud.vue
new file mode 100644
index 000000000..43ab49357
--- /dev/null
+++ b/packages/client/src/components/tag-cloud.vue
@@ -0,0 +1,80 @@
+<template>
+<div class="root">
+	<canvas :id="idForCanvas" ref="canvasEl" class="canvas" width="300" height="300"></canvas>
+	<div :id="idForTags" ref="tagsEl" class="tags">
+		<ul>
+			<slot></slot>
+		</ul>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, ref, watch, PropType, onBeforeUnmount } from 'vue';
+import tinycolor from 'tinycolor2';
+
+const props = defineProps<{}>();
+
+const loaded = !!window.TagCanvas;
+const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
+const computedStyle = getComputedStyle(document.documentElement);
+const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
+const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
+let available = $ref(false);
+let canvasEl = $ref<HTMLCanvasElement | null>(null);
+let tagsEl = $ref<HTMLElement | null>(null);
+
+watch($$(available), () => {
+	window.TagCanvas.Start(idForCanvas, idForTags, {
+		textColour: '#ffffff',
+		outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(),
+		outlineRadius: 10,
+		initial: [-0.030, -0.010],
+		frontSelect: true,
+		imageRadius: 8,
+		//dragControl: true,
+		dragThreshold: 3,
+		wheelZoom: false,
+		reverse: true,
+		depth: 0.5,
+		maxSpeed: 0.2,
+		minSpeed: 0.003,
+		stretchX: 0.8,
+		stretchY: 0.8,
+	});
+});
+
+onMounted(() => {
+	if (loaded) {
+		available = true;
+	} else {
+		document.head.appendChild(Object.assign(document.createElement('script'), {
+			async: true,
+			src: '/client-assets/tagcanvas.min.js',
+		})).addEventListener('load', () => available = true);
+	}
+});
+
+onBeforeUnmount(() => {
+	window.TagCanvas.Delete(idForCanvas);
+});
+</script>
+
+<style lang="scss" scoped>
+.root {
+	position: relative;
+	overflow: clip;
+	display: grid;
+	place-items: center;
+
+	> .canvas {
+		display: block;
+	}
+
+	> .tags {
+		position: absolute;
+		top: 999px;
+		left: 999px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index 190f756f7..6ccee8aea 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -108,6 +108,17 @@
 					</div>
 				</div>
 			</div>
+			<div class="container tagCloud">
+				<div class="body">
+					<MkTagCloud v-if="activeInstances">
+						<li v-for="instance in activeInstances">
+							<a @click.prevent="onInstanceClick(instance)">
+								<img style="width: 32px;" :src="instance.iconUrl">
+							</a>
+						</li>
+					</MkTagCloud>
+				</div>
+			</div>
 			<div v-if="fedStats" class="container federationPies">
 				<div class="body">
 					<div class="chart deliver">
@@ -154,8 +165,8 @@ import XFederation from './overview.federation.vue';
 import XQueueChart from './overview.queue-chart.vue';
 import XUser from './overview.user.vue';
 import XPie from './overview.pie.vue';
-import MkInstanceStats from '@/components/instance-stats.vue';
 import MkNumberDiff from '@/components/number-diff.vue';
+import MkTagCloud from '@/components/tag-cloud.vue';
 import { version, url } from '@/config';
 import number from '@/filters/number';
 import * as os from '@/os';
@@ -197,6 +208,7 @@ let federationPubActiveDiff = $ref<number | null>(null);
 let federationSubActive = $ref<number | null>(null);
 let federationSubActiveDiff = $ref<number | null>(null);
 let newUsers = $ref(null);
+let activeInstances = $shallowRef(null);
 const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
 const now = new Date();
 let chartInstance: Chart = null;
@@ -363,6 +375,10 @@ async function renderChart() {
 	});
 }
 
+function onInstanceClick(i) {
+	os.pageWindow(`/instance-info/${i.host}`);
+}
+
 onMounted(async () => {
 	/*
 	const magicGrid = new MagicGrid({
@@ -410,6 +426,13 @@ onMounted(async () => {
 		newUsers = res;
 	});
 
+	os.api('federation/instances', {
+		sort: '+lastCommunicatedAt',
+		limit: 25,
+	}).then(res => {
+		activeInstances = res;
+	});
+
 	nextTick(() => {
 		queueStatsConnection.send('requestLog', {
 			id: Math.random().toString().substr(2, 8),
@@ -577,6 +600,14 @@ definePageMetadata({
 					}
 				}
 			}
+
+			&.tagCloud {
+				> .body {
+					background: var(--panel);
+					border-radius: var(--radius);
+					overflow: clip;
+				}
+			}
 		}
 	}
 

From 91957ab215b328cdcb4453a75b1b903407f60585 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 21:22:47 +0900
Subject: [PATCH 011/100] 12.112.0-beta.8

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

diff --git a/package.json b/package.json
index 9b1fd6b1f..11962a72a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.7",
+	"version": "12.112.0-beta.8",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 7a9497649d9acb4925b96c8ffd8d970bfd469f2d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 23:06:03 +0900
Subject: [PATCH 012/100] Update .eslintrc.js

---
 packages/client/.eslintrc.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js
index 902214348..981b08d74 100644
--- a/packages/client/.eslintrc.js
+++ b/packages/client/.eslintrc.js
@@ -69,6 +69,7 @@ module.exports = {
 		// Vue
 		'$$': false,
 		'$ref': false,
+		'$shallowRef': false,
 		'$computed': false,
 
 		// Misskey

From 8af0818ee7a95aad5eeb1c41ca410e0bc11d58dd Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 23:28:52 +0900
Subject: [PATCH 013/100] feat(client): add instance-cloud widget

---
 CHANGELOG.md                                  |  1 +
 locales/ja-JP.yml                             |  1 +
 packages/client/src/widgets/index.ts          |  2 +
 .../client/src/widgets/instance-cloud.vue     | 80 +++++++++++++++++++
 4 files changed, 84 insertions(+)
 create mode 100644 packages/client/src/widgets/instance-cloud.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9bb600ff..063ebf525 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ You should also include the user name that made the change.
 - Server: Add rate limit to i/notifications @tamaina
 - Client: Improve control panel @syuilo
 - Client: Show warning in control panel when there is an unresolved abuse report @syuilo
+- Client: Add instance-cloud widget @syuilo
 - Make possible to delete an account by admin @syuilo
 - Improve player detection in URL preview @mei23
 - Add Badge Image to Push Notification #8012 @tamaina
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 139643f72..9de5b99d1 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1247,6 +1247,7 @@ _widgets:
   photos: "フォト"
   digitalClock: "デジタル時計"
   federation: "連合"
+  instanceCloud: "インスタンスクラウド"
   postForm: "投稿フォーム"
   slideshow: "スライドショー"
   button: "ボタン"
diff --git a/packages/client/src/widgets/index.ts b/packages/client/src/widgets/index.ts
index 51a82af08..feda16c91 100644
--- a/packages/client/src/widgets/index.ts
+++ b/packages/client/src/widgets/index.ts
@@ -17,6 +17,7 @@ export default function(app: App) {
 	app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue')));
 	app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
 	app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue')));
+	app.component('MkwInstanceCloud', defineAsyncComponent(() => import('./instance-cloud.vue')));
 	app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
 	app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
 	app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
@@ -34,6 +35,7 @@ export const widgets = [
 	'photos',
 	'digitalClock',
 	'federation',
+	'instance-cloud',
 	'postForm',
 	'slideshow',
 	'serverMetric',
diff --git a/packages/client/src/widgets/instance-cloud.vue b/packages/client/src/widgets/instance-cloud.vue
new file mode 100644
index 000000000..aa76c37a0
--- /dev/null
+++ b/packages/client/src/widgets/instance-cloud.vue
@@ -0,0 +1,80 @@
+<template>
+<MkContainer :naked="widgetProps.transparent" :show-header="widgetProps.showHeader" class="mkw-instance-cloud">
+	<div class="">
+		<MkTagCloud v-if="activeInstances">
+			<li v-for="instance in activeInstances">
+				<a @click.prevent="onInstanceClick(instance)">
+					<img style="width: 32px;" :src="instance.iconUrl">
+				</a>
+			</li>
+		</MkTagCloud>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { GetFormResultType } from '@/scripts/form';
+import MkContainer from '@/components/ui/container.vue';
+import MkTagCloud from '@/components/tag-cloud.vue';
+import * as os from '@/os';
+import { useInterval } from '@/scripts/use-interval';
+
+const name = 'instanceCloud';
+
+const widgetPropsDef = {
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+let cloud = $ref<InstanceType<typeof MkTagCloud> | null>();
+let activeInstances = $shallowRef(null);
+
+function onInstanceClick(i) {
+	os.pageWindow(`/instance-info/${i.host}`);
+}
+
+useInterval(() => {
+	os.api('federation/instances', {
+		sort: '+lastCommunicatedAt',
+		limit: 25,
+	}).then(res => {
+		activeInstances = res;
+		if (cloud) cloud.update();
+	});
+}, 1000 * 60 * 3, {
+	immediate: true,
+	afterMounted: true,
+});
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>

From b3983ee3f7bdeee2a8a7b58d85ba2481145f9d26 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Wed, 29 Jun 2022 17:44:03 +0200
Subject: [PATCH 014/100] fix 'assignment to const' error

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

diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index f5fa8f336..412b6db34 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -57,7 +57,7 @@ const XWidgets = defineAsyncComponent(() => import('./classic.widgets.vue'));
 
 const DESKTOP_THRESHOLD = 1100;
 
-const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
+let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
 
 let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
 const widgetsShowing = $ref(false);

From 5d6c624757a751146db170d0bcae5ecb6f4cb8b1 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Wed, 29 Jun 2022 22:09:44 +0200
Subject: [PATCH 015/100] fix client router catchall

fixes #8903
---
 packages/client/src/router.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index 54cd7231d..1197976d5 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -227,7 +227,7 @@ export const routes = [{
 	component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')),
 	globalCacheKey: 'index',
 }, {
-	path: '/(*)',
+	path: '/:(*)',
 	component: page(() => import('./pages/not-found.vue')),
 }];
 

From bd76b1fed8b9335187cab9b125253290a21d55be Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 10:13:27 +0900
Subject: [PATCH 016/100] tweak client

---
 packages/client/src/components/tag-cloud.vue   | 14 ++++++++++++--
 packages/client/src/ui/visitor/b.vue           |  4 ++++
 packages/client/src/widgets/instance-cloud.vue |  6 +-----
 3 files changed, 17 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/components/tag-cloud.vue b/packages/client/src/components/tag-cloud.vue
index 43ab49357..8df8d0b05 100644
--- a/packages/client/src/components/tag-cloud.vue
+++ b/packages/client/src/components/tag-cloud.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="root">
-	<canvas :id="idForCanvas" ref="canvasEl" class="canvas" width="300" height="300"></canvas>
+<div ref="rootEl" class="root">
+	<canvas :id="idForCanvas" ref="canvasEl" class="canvas" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
 	<div :id="idForTags" ref="tagsEl" class="tags">
 		<ul>
 			<slot></slot>
@@ -21,8 +21,10 @@ const computedStyle = getComputedStyle(document.documentElement);
 const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
 const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
 let available = $ref(false);
+let rootEl = $ref<HTMLElement | null>(null);
 let canvasEl = $ref<HTMLCanvasElement | null>(null);
 let tagsEl = $ref<HTMLElement | null>(null);
+let width = $ref(300);
 
 watch($$(available), () => {
 	window.TagCanvas.Start(idForCanvas, idForTags, {
@@ -45,6 +47,8 @@ watch($$(available), () => {
 });
 
 onMounted(() => {
+	width = rootEl.offsetWidth;
+
 	if (loaded) {
 		available = true;
 	} else {
@@ -58,6 +62,12 @@ onMounted(() => {
 onBeforeUnmount(() => {
 	window.TagCanvas.Delete(idForCanvas);
 });
+
+defineExpose({
+	update: () => {
+		window.TagCanvas.Update(idForCanvas);
+	},
+});
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/ui/visitor/b.vue b/packages/client/src/ui/visitor/b.vue
index 28933f272..09be12d68 100644
--- a/packages/client/src/ui/visitor/b.vue
+++ b/packages/client/src/ui/visitor/b.vue
@@ -116,6 +116,10 @@ onMounted(() => {
 		}, { passive: true });
 	}
 });
+
+defineExpose({
+	showMenu: $$(showMenu),
+});
 </script>
 
 <style>
diff --git a/packages/client/src/widgets/instance-cloud.vue b/packages/client/src/widgets/instance-cloud.vue
index aa76c37a0..cb66c5fa3 100644
--- a/packages/client/src/widgets/instance-cloud.vue
+++ b/packages/client/src/widgets/instance-cloud.vue
@@ -1,5 +1,5 @@
 <template>
-<MkContainer :naked="widgetProps.transparent" :show-header="widgetProps.showHeader" class="mkw-instance-cloud">
+<MkContainer :naked="widgetProps.transparent" class="mkw-instance-cloud">
 	<div class="">
 		<MkTagCloud v-if="activeInstances">
 			<li v-for="instance in activeInstances">
@@ -28,10 +28,6 @@ const widgetPropsDef = {
 		type: 'boolean' as const,
 		default: false,
 	},
-	showHeader: {
-		type: 'boolean' as const,
-		default: true,
-	},
 };
 
 type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

From 7be20ff1a713bb81127432feec9f05d317c0228a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 10:53:40 +0900
Subject: [PATCH 017/100] tweak client

---
 locales/ja-JP.yml                             |   2 +
 packages/client/src/components/form/group.vue |  36 ---
 packages/client/src/components/form/range.vue |  17 +-
 .../src/components/global/router-view.vue     |   3 +-
 packages/client/src/menu.ts                   |   2 +
 .../client/src/pages/admin/object-storage.vue |   1 -
 packages/client/src/pages/gallery/edit.vue    |   7 +-
 packages/client/src/pages/settings/deck.vue   |   6 +-
 .../client/src/pages/settings/general.vue     |  12 +-
 packages/client/src/scripts/get-note-menu.ts  | 248 +++++++++---------
 packages/client/src/store.ts                  |   4 +
 .../client/src/widgets/instance-cloud.vue     |   2 +-
 12 files changed, 156 insertions(+), 184 deletions(-)
 delete mode 100644 packages/client/src/components/form/group.vue

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 9de5b99d1..17de04ebc 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -858,6 +858,8 @@ isSystemAccount: "システムにより自動で作成・管理されている
 typeToConfirm: "この操作を行うには {x} と入力してください"
 deleteAccount: "アカウント削除"
 document: "ドキュメント"
+numberOfPageCache: "ページキャッシュ数"
+numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
 
 _emailUnavailable:
   used: "既に使用されています"
diff --git a/packages/client/src/components/form/group.vue b/packages/client/src/components/form/group.vue
deleted file mode 100644
index 1e8376ca4..000000000
--- a/packages/client/src/components/form/group.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-<div v-sticky-container class="adfeebaf _formBlock">
-	<div class="label"><slot name="label"></slot></div>
-	<div class="main _formRoot">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-});
-</script>
-
-<style lang="scss" scoped>
-.adfeebaf {
-	padding: 24px 24px;
-	border: solid 1px var(--divider);
-	border-radius: var(--radius);
-
-	> .label {
-		font-weight: bold;
-		padding: 0 0 16px 0;
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .main {
-
-	}
-}
-</style>
diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index ac4a781e3..9bb0164a2 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -4,7 +4,7 @@
 	<div v-adaptive-border class="body">
 		<div ref="containerEl" class="container">
 			<div class="track">
-				<div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div>
+				<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
 			</div>
 			<div v-if="steps" class="ticks">
 				<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
@@ -12,6 +12,7 @@
 			<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
 		</div>
 	</div>
+	<div class="caption"><slot name="caption"></slot></div>
 </div>
 </template>
 
@@ -62,7 +63,7 @@ export default defineComponent({
 		const thumbEl = ref<HTMLElement>();
 
 		const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
-		const steppedValue = computed(() => {
+		const steppedRawValue = computed(() => {
 			if (props.step) {
 				const step = props.step / (props.max - props.min);
 				return (step * Math.round(rawValue.value / step));
@@ -71,7 +72,11 @@ export default defineComponent({
 			}
 		});
 		const finalValue = computed(() => {
-			return (steppedValue.value * (props.max - props.min)) + props.min;
+			if (Number.isInteger(props.step)) {
+				return Math.round((steppedRawValue.value * (props.max - props.min)) + props.min);
+			} else {
+				return (steppedRawValue.value * (props.max - props.min)) + props.min;
+			}
 		});
 		watch(finalValue, () => {
 			context.emit('update:modelValue', finalValue.value);
@@ -86,10 +91,10 @@ export default defineComponent({
 			if (containerEl.value == null) {
 				thumbPosition.value = 0;
 			} else {
-				thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
+				thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedRawValue.value;
 			}
 		};
-		watch([steppedValue, containerEl], calcThumbPosition);
+		watch([steppedRawValue, containerEl], calcThumbPosition);
 
 		let ro: ResizeObserver | undefined;
 
@@ -154,7 +159,7 @@ export default defineComponent({
 		return {
 			rawValue,
 			finalValue,
-			steppedValue,
+			steppedRawValue,
 			onMousedown,
 			containerEl,
 			thumbEl,
diff --git a/packages/client/src/components/global/router-view.vue b/packages/client/src/components/global/router-view.vue
index 56b53e012..7138faaa9 100644
--- a/packages/client/src/components/global/router-view.vue
+++ b/packages/client/src/components/global/router-view.vue
@@ -1,5 +1,5 @@
 <template>
-<KeepAlive max="5">
+<KeepAlive :max="defaultStore.state.numberOfPageCache">
 	<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
 </KeepAlive>
 </template>
@@ -7,6 +7,7 @@
 <script lang="ts" setup>
 import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue';
 import { Router } from '@/nirax';
+import { defaultStore } from '@/store';
 
 const props = defineProps<{
 	router?: Router;
diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
index 2c0126eb8..72e395160 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -81,12 +81,14 @@ export const menuDef = reactive({
 			os.popupMenu(items, ev.currentTarget ?? ev.target);
 		},
 	},
+	/*
 	groups: {
 		title: 'groups',
 		icon: 'fas fa-users',
 		show: computed(() => $i != null),
 		to: '/my/groups',
 	},
+	*/
 	antennas: {
 		title: 'antennas',
 		icon: 'fas fa-satellite',
diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue
index bae5277f4..450fd134e 100644
--- a/packages/client/src/pages/admin/object-storage.vue
+++ b/packages/client/src/pages/admin/object-storage.vue
@@ -73,7 +73,6 @@ import { } from 'vue';
 import XHeader from './_header_.vue';
 import FormSwitch from '@/components/form/switch.vue';
 import FormInput from '@/components/form/input.vue';
-import FormGroup from '@/components/form/group.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import FormSplit from '@/components/form/split.vue';
 import FormSection from '@/components/form/section.vue';
diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue
index 6d1140ba3..fa3063bde 100644
--- a/packages/client/src/pages/gallery/edit.vue
+++ b/packages/client/src/pages/gallery/edit.vue
@@ -9,13 +9,13 @@
 			<template #label>{{ $ts.description }}</template>
 		</FormTextarea>
 
-		<FormGroup>
-			<div v-for="file in files" :key="file.id" class="_formGroup wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
+		<div class="">
+			<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
 				<div class="name">{{ file.name }}</div>
 				<button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
 			</div>
 			<FormButton primary @click="selectFile"><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
-		</FormGroup>
+		</div>
 
 		<FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
 
@@ -33,7 +33,6 @@ import FormButton from '@/components/ui/button.vue';
 import FormInput from '@/components/form/input.vue';
 import FormTextarea from '@/components/form/textarea.vue';
 import FormSwitch from '@/components/form/switch.vue';
-import FormGroup from '@/components/form/group.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import { selectFiles } from '@/scripts/select-file';
 import * as os from '@/os';
diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue
index edada683a..295357377 100644
--- a/packages/client/src/pages/settings/deck.vue
+++ b/packages/client/src/pages/settings/deck.vue
@@ -1,9 +1,6 @@
 <template>
 <div class="_formRoot">
-	<FormGroup>
-		<template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
-		<FormSwitch v-model="navWindow">{{ i18n.ts.openInWindow }}</FormSwitch>
-	</FormGroup>
+	<FormSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</FormSwitch>
 
 	<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
 
@@ -35,7 +32,6 @@ import FormSwitch from '@/components/form/switch.vue';
 import FormLink from '@/components/form/link.vue';
 import FormRadios from '@/components/form/radios.vue';
 import FormInput from '@/components/form/input.vue';
-import FormGroup from '@/components/form/group.vue';
 import { deckStore } from '@/ui/deck/deck-store';
 import * as os from '@/os';
 import { unisonReload } from '@/scripts/unison-reload';
diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue
index ac2e3a496..e7339af14 100644
--- a/packages/client/src/pages/settings/general.vue
+++ b/packages/client/src/pages/settings/general.vue
@@ -81,10 +81,10 @@
 		<option value="force">{{ i18n.ts._nsfw.force }}</option>
 	</FormSelect>
 
-	<FormGroup>
-		<template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
-		<FormSwitch v-model="defaultSideView">{{ i18n.ts.openInSideView }}</FormSwitch>
-	</FormGroup>
+	<FormRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" class="_formBlock">
+		<template #label>{{ i18n.ts.numberOfPageCache }}</template>
+		<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
+	</FormRange>
 
 	<FormLink to="/settings/deck" class="_formBlock">{{ i18n.ts.deck }}</FormLink>
 
@@ -97,7 +97,7 @@ import { computed, ref, watch } from 'vue';
 import FormSwitch from '@/components/form/switch.vue';
 import FormSelect from '@/components/form/select.vue';
 import FormRadios from '@/components/form/radios.vue';
-import FormGroup from '@/components/form/group.vue';
+import FormRange from '@/components/form/range.vue';
 import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
 import MkLink from '@/components/link.vue';
@@ -137,7 +137,7 @@ const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
 const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
 const disablePagesScript = computed(defaultStore.makeGetterSetter('disablePagesScript'));
 const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
-const defaultSideView = computed(defaultStore.makeGetterSetter('defaultSideView'));
+const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
 const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
 const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
 const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts
index 283c90362..632143f51 100644
--- a/packages/client/src/scripts/get-note-menu.ts
+++ b/packages/client/src/scripts/get-note-menu.ts
@@ -1,5 +1,6 @@
 import { defineAsyncComponent, Ref, inject } from 'vue';
 import * as misskey from 'misskey-js';
+import { pleaseLogin } from './please-login';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { instance } from '@/instance';
@@ -7,7 +8,6 @@ import * as os from '@/os';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { url } from '@/config';
 import { noteActions } from '@/store';
-import { pleaseLogin } from './please-login';
 
 export function getNoteMenu(props: {
 	note: misskey.entities.Note;
@@ -34,7 +34,7 @@ export function getNoteMenu(props: {
 			if (canceled) return;
 
 			os.api('notes/delete', {
-				noteId: appearNote.id
+				noteId: appearNote.id,
 			});
 		});
 	}
@@ -47,7 +47,7 @@ export function getNoteMenu(props: {
 			if (canceled) return;
 
 			os.api('notes/delete', {
-				noteId: appearNote.id
+				noteId: appearNote.id,
 			});
 
 			os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
@@ -56,19 +56,19 @@ export function getNoteMenu(props: {
 
 	function toggleFavorite(favorite: boolean): void {
 		os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
-			noteId: appearNote.id
+			noteId: appearNote.id,
 		});
 	}
 
 	function toggleWatch(watch: boolean): void {
 		os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
-			noteId: appearNote.id
+			noteId: appearNote.id,
 		});
 	}
 
 	function toggleThreadMute(mute: boolean): void {
 		os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
-			noteId: appearNote.id
+			noteId: appearNote.id,
 		});
 	}
 
@@ -84,12 +84,12 @@ export function getNoteMenu(props: {
 
 	function togglePin(pin: boolean): void {
 		os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
-			noteId: appearNote.id
+			noteId: appearNote.id,
 		}, undefined, null, res => {
 			if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
 				os.alert({
 					type: 'error',
-					text: i18n.ts.pinLimitExceeded
+					text: i18n.ts.pinLimitExceeded,
 				});
 			}
 		});
@@ -104,26 +104,26 @@ export function getNoteMenu(props: {
 				const { canceled, result } = await os.form(i18n.ts.createNewClip, {
 					name: {
 						type: 'string',
-						label: i18n.ts.name
+						label: i18n.ts.name,
 					},
 					description: {
 						type: 'string',
 						required: false,
 						multiline: true,
-						label: i18n.ts.description
+						label: i18n.ts.description,
 					},
 					isPublic: {
 						type: 'boolean',
 						label: i18n.ts.public,
-						default: false
-					}
+						default: false,
+					},
 				});
 				if (canceled) return;
 
 				const clip = await os.apiWithDialog('clips/create', result);
 
 				os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
-			}
+			},
 		}, null, ...clips.map(clip => ({
 			text: clip.name,
 			action: () => {
@@ -146,9 +146,9 @@ export function getNoteMenu(props: {
 								text: err.message + '\n' + err.id,
 							});
 						}
-					}
+					},
 				);
-			}
+			},
 		}))], props.menuButton.value, {
 		}).then(focus);
 	}
@@ -193,86 +193,86 @@ export function getNoteMenu(props: {
 	let menu;
 	if ($i) {
 		const statePromise = os.api('notes/state', {
-			noteId: appearNote.id
+			noteId: appearNote.id,
 		});
 
 		menu = [
-		...(
-			props.currentClipPage?.value.userId === $i.id ? [{
-				icon: 'fas fa-circle-minus',
-				text: i18n.ts.unclip,
-				danger: true,
-				action: unclip,
-			}, null] : []
-		),
-		{
-			icon: 'fas fa-copy',
-			text: i18n.ts.copyContent,
-			action: copyContent
-		}, {
-			icon: 'fas fa-link',
-			text: i18n.ts.copyLink,
-			action: copyLink
-		}, (appearNote.url || appearNote.uri) ? {
-			icon: 'fas fa-external-link-square-alt',
-			text: i18n.ts.showOnRemote,
-			action: () => {
-				window.open(appearNote.url || appearNote.uri, '_blank');
-			}
-		} : undefined,
-		{
-			icon: 'fas fa-share-alt',
-			text: i18n.ts.share,
-			action: share
-		},
-		instance.translatorAvailable ? {
-			icon: 'fas fa-language',
-			text: i18n.ts.translate,
-			action: translate
-		} : undefined,
-		null,
-		statePromise.then(state => state.isFavorited ? {
-			icon: 'fas fa-star',
-			text: i18n.ts.unfavorite,
-			action: () => toggleFavorite(false)
-		} : {
-			icon: 'fas fa-star',
-			text: i18n.ts.favorite,
-			action: () => toggleFavorite(true)
-		}),
-		{
-			icon: 'fas fa-paperclip',
-			text: i18n.ts.clip,
-			action: () => clip()
-		},
-		(appearNote.userId !== $i.id) ? statePromise.then(state => state.isWatching ? {
-			icon: 'fas fa-eye-slash',
-			text: i18n.ts.unwatch,
-			action: () => toggleWatch(false)
-		} : {
-			icon: 'fas fa-eye',
-			text: i18n.ts.watch,
-			action: () => toggleWatch(true)
-		}) : undefined,
-		statePromise.then(state => state.isMutedThread ? {
-			icon: 'fas fa-comment-slash',
-			text: i18n.ts.unmuteThread,
-			action: () => toggleThreadMute(false)
-		} : {
-			icon: 'fas fa-comment-slash',
-			text: i18n.ts.muteThread,
-			action: () => toggleThreadMute(true)
-		}),
-		appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
-			icon: 'fas fa-thumbtack',
-			text: i18n.ts.unpin,
-			action: () => togglePin(false)
-		} : {
-			icon: 'fas fa-thumbtack',
-			text: i18n.ts.pin,
-			action: () => togglePin(true)
-		} : undefined,
-		/*
+			...(
+				props.currentClipPage?.value.userId === $i.id ? [{
+					icon: 'fas fa-circle-minus',
+					text: i18n.ts.unclip,
+					danger: true,
+					action: unclip,
+				}, null] : []
+			),
+			{
+				icon: 'fas fa-copy',
+				text: i18n.ts.copyContent,
+				action: copyContent,
+			}, {
+				icon: 'fas fa-link',
+				text: i18n.ts.copyLink,
+				action: copyLink,
+			}, (appearNote.url || appearNote.uri) ? {
+				icon: 'fas fa-external-link-square-alt',
+				text: i18n.ts.showOnRemote,
+				action: () => {
+					window.open(appearNote.url || appearNote.uri, '_blank');
+				},
+			} : undefined,
+			{
+				icon: 'fas fa-share-alt',
+				text: i18n.ts.share,
+				action: share,
+			},
+			instance.translatorAvailable ? {
+				icon: 'fas fa-language',
+				text: i18n.ts.translate,
+				action: translate,
+			} : undefined,
+			null,
+			statePromise.then(state => state.isFavorited ? {
+				icon: 'fas fa-star',
+				text: i18n.ts.unfavorite,
+				action: () => toggleFavorite(false),
+			} : {
+				icon: 'fas fa-star',
+				text: i18n.ts.favorite,
+				action: () => toggleFavorite(true),
+			}),
+			{
+				icon: 'fas fa-paperclip',
+				text: i18n.ts.clip,
+				action: () => clip(),
+			},
+			(appearNote.userId !== $i.id) ? statePromise.then(state => state.isWatching ? {
+				icon: 'fas fa-eye-slash',
+				text: i18n.ts.unwatch,
+				action: () => toggleWatch(false),
+			} : {
+				icon: 'fas fa-eye',
+				text: i18n.ts.watch,
+				action: () => toggleWatch(true),
+			}) : undefined,
+			statePromise.then(state => state.isMutedThread ? {
+				icon: 'fas fa-comment-slash',
+				text: i18n.ts.unmuteThread,
+				action: () => toggleThreadMute(false),
+			} : {
+				icon: 'fas fa-comment-slash',
+				text: i18n.ts.muteThread,
+				action: () => toggleThreadMute(true),
+			}),
+			appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
+				icon: 'fas fa-thumbtack',
+				text: i18n.ts.unpin,
+				action: () => togglePin(false),
+			} : {
+				icon: 'fas fa-thumbtack',
+				text: i18n.ts.pin,
+				action: () => togglePin(true),
+			} : undefined,
+			/*
 		...($i.isModerator || $i.isAdmin ? [
 			null,
 			{
@@ -282,52 +282,52 @@ export function getNoteMenu(props: {
 			}]
 			: []
 		),*/
-		...(appearNote.userId !== $i.id ? [
-			null,
-			{
-				icon: 'fas fa-exclamation-circle',
-				text: i18n.ts.reportAbuse,
-				action: () => {
-					const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
-					os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), {
-						user: appearNote.user,
-						initialComment: `Note: ${u}\n-----\n`
-					}, {}, 'closed');
-				}
-			}]
+			...(appearNote.userId !== $i.id ? [
+				null,
+				{
+					icon: 'fas fa-exclamation-circle',
+					text: i18n.ts.reportAbuse,
+					action: () => {
+						const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
+						os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), {
+							user: appearNote.user,
+							initialComment: `Note: ${u}\n-----\n`,
+						}, {}, 'closed');
+					},
+				}]
 			: []
-		),
-		...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
-			null,
-			appearNote.userId === $i.id ? {
-				icon: 'fas fa-edit',
-				text: i18n.ts.deleteAndEdit,
-				action: delEdit
-			} : undefined,
-			{
-				icon: 'fas fa-trash-alt',
-				text: i18n.ts.delete,
-				danger: true,
-				action: del
-			}]
+			),
+			...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
+				null,
+				appearNote.userId === $i.id ? {
+					icon: 'fas fa-edit',
+					text: i18n.ts.deleteAndEdit,
+					action: delEdit,
+				} : undefined,
+				{
+					icon: 'fas fa-trash-alt',
+					text: i18n.ts.delete,
+					danger: true,
+					action: del,
+				}]
 			: []
-		)]
+			)]
 		.filter(x => x !== undefined);
 	} else {
 		menu = [{
 			icon: 'fas fa-copy',
 			text: i18n.ts.copyContent,
-			action: copyContent
+			action: copyContent,
 		}, {
 			icon: 'fas fa-link',
 			text: i18n.ts.copyLink,
-			action: copyLink
+			action: copyLink,
 		}, (appearNote.url || appearNote.uri) ? {
 			icon: 'fas fa-external-link-square-alt',
 			text: i18n.ts.showOnRemote,
 			action: () => {
 				window.open(appearNote.url || appearNote.uri, '_blank');
-			}
+			},
 		} : undefined]
 		.filter(x => x !== undefined);
 	}
@@ -338,7 +338,7 @@ export function getNoteMenu(props: {
 			text: action.title,
 			action: () => {
 				action.handler(appearNote);
-			}
+			},
 		}))]);
 	}
 
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index a273a46c7..94d9d9138 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -233,6 +233,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
+	numberOfPageCache: {
+		where: 'device',
+		default: 5,
+	},
 	aiChanMode: {
 		where: 'device',
 		default: false,
diff --git a/packages/client/src/widgets/instance-cloud.vue b/packages/client/src/widgets/instance-cloud.vue
index cb66c5fa3..7dc4b4ce9 100644
--- a/packages/client/src/widgets/instance-cloud.vue
+++ b/packages/client/src/widgets/instance-cloud.vue
@@ -1,5 +1,5 @@
 <template>
-<MkContainer :naked="widgetProps.transparent" class="mkw-instance-cloud">
+<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-instance-cloud">
 	<div class="">
 		<MkTagCloud v-if="activeInstances">
 			<li v-for="instance in activeInstances">

From c9ad7e1a5b141a1d7dce33e37e7efe7d30ea4128 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 12:42:35 +0900
Subject: [PATCH 018/100] perf(client): improve range control performance

---
 packages/client/src/components/form/range.vue | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index 9bb0164a2..7ef727d57 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -78,9 +78,6 @@ export default defineComponent({
 				return (steppedRawValue.value * (props.max - props.min)) + props.min;
 			}
 		});
-		watch(finalValue, () => {
-			context.emit('update:modelValue', finalValue.value);
-		});
 
 		const thumbWidth = computed(() => {
 			if (thumbEl.value == null) return 0;
@@ -141,6 +138,8 @@ export default defineComponent({
 				rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
 			};
 
+			let beforeValue = finalValue.value;
+
 			const onMouseup = () => {
 				document.head.removeChild(style);
 				tooltipShowing.value = false;
@@ -148,6 +147,11 @@ export default defineComponent({
 				window.removeEventListener('touchmove', onDrag);
 				window.removeEventListener('mouseup', onMouseup);
 				window.removeEventListener('touchend', onMouseup);
+
+				// 値が変わってたら通知
+				if (beforeValue !== finalValue.value) {
+					context.emit('update:modelValue', finalValue.value);
+				}
 			};
 
 			window.addEventListener('mousemove', onDrag);

From 36ec7462d7d937d6a2bc8f63ce2bf41bc740219f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 12:48:42 +0900
Subject: [PATCH 019/100] refactor(client): use setup syntax

---
 packages/client/src/components/form/range.vue | 270 ++++++++----------
 1 file changed, 117 insertions(+), 153 deletions(-)

diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index 7ef727d57..d46174acc 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -16,162 +16,126 @@
 </div>
 </template>
 
-<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue';
 import * as os from '@/os';
 
-export default defineComponent({
-	props: {
-		modelValue: {
-			type: Number,
-			required: false,
-			default: 0,
-		},
-		disabled: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		min: {
-			type: Number,
-			required: false,
-			default: 0,
-		},
-		max: {
-			type: Number,
-			required: false,
-			default: 100,
-		},
-		step: {
-			type: Number,
-			required: false,
-			default: 1,
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-		},
-		textConverter: {
-			type: Function,
-			required: false,
-			default: (v) => v.toString(),
-		},
-	},
-
-	setup(props, context) {
-		const containerEl = ref<HTMLElement>();
-		const thumbEl = ref<HTMLElement>();
-
-		const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
-		const steppedRawValue = computed(() => {
-			if (props.step) {
-				const step = props.step / (props.max - props.min);
-				return (step * Math.round(rawValue.value / step));
-			} else {
-				return rawValue.value;
-			}
-		});
-		const finalValue = computed(() => {
-			if (Number.isInteger(props.step)) {
-				return Math.round((steppedRawValue.value * (props.max - props.min)) + props.min);
-			} else {
-				return (steppedRawValue.value * (props.max - props.min)) + props.min;
-			}
-		});
-
-		const thumbWidth = computed(() => {
-			if (thumbEl.value == null) return 0;
-			return thumbEl.value!.offsetWidth;
-		});
-		const thumbPosition = ref(0);
-		const calcThumbPosition = () => {
-			if (containerEl.value == null) {
-				thumbPosition.value = 0;
-			} else {
-				thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedRawValue.value;
-			}
-		};
-		watch([steppedRawValue, containerEl], calcThumbPosition);
-
-		let ro: ResizeObserver | undefined;
-
-		onMounted(() => {
-			ro = new ResizeObserver((entries, observer) => {
-				calcThumbPosition();
-			});
-			ro.observe(containerEl.value);
-		});
-		
-		onUnmounted(() => {
-			if (ro) ro.disconnect();
-		});
-
-		const steps = computed(() => {
-			if (props.step) {
-				return (props.max - props.min) / props.step;
-			} else {
-				return 0;
-			}
-		});
-
-		const onMousedown = (ev: MouseEvent | TouchEvent) => {
-			ev.preventDefault();
-
-			const tooltipShowing = ref(true);
-			os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
-				showing: tooltipShowing,
-				text: computed(() => {
-					return props.textConverter(finalValue.value);
-				}),
-				targetElement: thumbEl,
-			}, {}, 'closed');
-
-			const style = document.createElement('style');
-			style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
-			document.head.appendChild(style);
-
-			const onDrag = (ev: MouseEvent | TouchEvent) => {
-				ev.preventDefault();
-				const containerRect = containerEl.value!.getBoundingClientRect();
-				const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
-				const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2));
-				rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
-			};
-
-			let beforeValue = finalValue.value;
-
-			const onMouseup = () => {
-				document.head.removeChild(style);
-				tooltipShowing.value = false;
-				window.removeEventListener('mousemove', onDrag);
-				window.removeEventListener('touchmove', onDrag);
-				window.removeEventListener('mouseup', onMouseup);
-				window.removeEventListener('touchend', onMouseup);
-
-				// 値が変わってたら通知
-				if (beforeValue !== finalValue.value) {
-					context.emit('update:modelValue', finalValue.value);
-				}
-			};
-
-			window.addEventListener('mousemove', onDrag);
-			window.addEventListener('touchmove', onDrag);
-			window.addEventListener('mouseup', onMouseup, { once: true });
-			window.addEventListener('touchend', onMouseup, { once: true });
-		};
-
-		return {
-			rawValue,
-			finalValue,
-			steppedRawValue,
-			onMousedown,
-			containerEl,
-			thumbEl,
-			thumbPosition,
-			steps,
-		};
-	},
+const props = withDefaults(defineProps<{
+	modelValue: number;
+	disabled?: boolean;
+	min: number;
+	max: number;
+	step?: number;
+	textConverter?: (value: number) => string,
+}>(), {
+	step: 1,
+	textConverter: (v) => v.toString(),
 });
+
+const emit = defineEmits<{
+	(ev: 'update:modelValue', value: number): void;
+}>();
+
+const containerEl = ref<HTMLElement>();
+const thumbEl = ref<HTMLElement>();
+
+const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
+const steppedRawValue = computed(() => {
+	if (props.step) {
+		const step = props.step / (props.max - props.min);
+		return (step * Math.round(rawValue.value / step));
+	} else {
+		return rawValue.value;
+	}
+});
+const finalValue = computed(() => {
+	if (Number.isInteger(props.step)) {
+		return Math.round((steppedRawValue.value * (props.max - props.min)) + props.min);
+	} else {
+		return (steppedRawValue.value * (props.max - props.min)) + props.min;
+	}
+});
+
+const thumbWidth = computed(() => {
+	if (thumbEl.value == null) return 0;
+	return thumbEl.value!.offsetWidth;
+});
+const thumbPosition = ref(0);
+const calcThumbPosition = () => {
+	if (containerEl.value == null) {
+		thumbPosition.value = 0;
+	} else {
+		thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedRawValue.value;
+	}
+};
+watch([steppedRawValue, containerEl], calcThumbPosition);
+
+let ro: ResizeObserver | undefined;
+
+onMounted(() => {
+	ro = new ResizeObserver((entries, observer) => {
+		calcThumbPosition();
+	});
+	ro.observe(containerEl.value);
+});
+
+onUnmounted(() => {
+	if (ro) ro.disconnect();
+});
+
+const steps = computed(() => {
+	if (props.step) {
+		return (props.max - props.min) / props.step;
+	} else {
+		return 0;
+	}
+});
+
+const onMousedown = (ev: MouseEvent | TouchEvent) => {
+	ev.preventDefault();
+
+	const tooltipShowing = ref(true);
+	os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
+		showing: tooltipShowing,
+		text: computed(() => {
+			return props.textConverter(finalValue.value);
+		}),
+		targetElement: thumbEl,
+	}, {}, 'closed');
+
+	const style = document.createElement('style');
+	style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
+	document.head.appendChild(style);
+
+	const onDrag = (ev: MouseEvent | TouchEvent) => {
+		ev.preventDefault();
+		const containerRect = containerEl.value!.getBoundingClientRect();
+		const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
+		const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2));
+		rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
+	};
+
+	let beforeValue = finalValue.value;
+
+	const onMouseup = () => {
+		document.head.removeChild(style);
+		tooltipShowing.value = false;
+		window.removeEventListener('mousemove', onDrag);
+		window.removeEventListener('touchmove', onDrag);
+		window.removeEventListener('mouseup', onMouseup);
+		window.removeEventListener('touchend', onMouseup);
+
+		// 値が変わってたら通知
+		if (beforeValue !== finalValue.value) {
+			emit('update:modelValue', finalValue.value);
+		}
+	};
+
+	window.addEventListener('mousemove', onDrag);
+	window.addEventListener('touchmove', onDrag);
+	window.addEventListener('mouseup', onMouseup, { once: true });
+	window.addEventListener('touchend', onMouseup, { once: true });
+};
 </script>
 
 <style lang="scss" scoped>

From d9c7409a6cf56f6d2e34fa2c2bde83a0cc5fcad7 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 15:09:10 +0900
Subject: [PATCH 020/100] enhance(client): show confirm dialog when logout

---
 locales/ja-JP.yml                            | 1 +
 packages/client/src/pages/settings/index.vue | 8 +++++++-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 17de04ebc..743640725 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -860,6 +860,7 @@ deleteAccount: "アカウント削除"
 document: "ドキュメント"
 numberOfPageCache: "ページキャッシュ数"
 numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
+logoutConfirm: "ログアウトしますか?"
 
 _emailUnavailable:
   used: "既に使用されています"
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 011962c2e..8143298cc 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -32,6 +32,7 @@ import { unisonReload } from '@/scripts/unison-reload';
 import { instance } from '@/instance';
 import { useRouter } from '@/router';
 import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
+import * as os from '@/os';
 
 const props = withDefaults(defineProps<{
   initialPage?: string;
@@ -181,7 +182,12 @@ const menuDef = computed(() => [{
 		type: 'button',
 		icon: 'fas fa-sign-in-alt fa-flip-horizontal',
 		text: i18n.ts.logout,
-		action: () => {
+		action: async () => {
+			const { canceled } = await os.confirm({
+				type: 'warning',
+				text: i18n.ts.logoutConfirm,
+			});
+			if (canceled) return;
 			signout();
 		},
 		danger: true,

From 38cdd46063f9035fa558cc2246f58022b509e314 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 15:32:11 +0900
Subject: [PATCH 021/100] chore(client): tweak client

---
 packages/backend/src/server/api/endpoints/users.ts | 10 ++++++++++
 packages/client/src/pages/instance-info.vue        |  3 +--
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts
index 2377faebd..3a8211374 100644
--- a/packages/backend/src/server/api/endpoints/users.ts
+++ b/packages/backend/src/server/api/endpoints/users.ts
@@ -27,6 +27,12 @@ export const paramDef = {
 		sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
 		state: { type: 'string', enum: ['all', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: 'all' },
 		origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
+		hostname: {
+			type: 'string',
+			nullable: true,
+			default: null,
+			description: 'The local host is represented with `null`.',
+		},
 	},
 	required: [],
 } as const;
@@ -48,6 +54,10 @@ export default define(meta, paramDef, async (ps, me) => {
 		case 'remote': query.andWhere('user.host IS NOT NULL'); break;
 	}
 
+	if (ps.hostname) {
+		query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
+	}
+
 	switch (ps.sort) {
 		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
 		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
index b72fcb152..83f3354df 100644
--- a/packages/client/src/pages/instance-info.vue
+++ b/packages/client/src/pages/instance-info.vue
@@ -143,12 +143,11 @@ let suspended = $ref(false);
 let isBlocked = $ref(false);
 
 const usersPagination = {
-	endpoint: 'admin/show-users' as const,
+	endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
 	limit: 10,
 	params: {
 		sort: '+updatedAt',
 		state: 'all',
-		origin: 'remote',
 		hostname: props.host,
 	},
 	offsetMode: true,

From f571188dd0d63e0c9a8911acc33985e37132bb88 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 15:36:09 +0900
Subject: [PATCH 022/100] fix(server): cannot show users

---
 .../api/common/generate-muted-user-query.ts   | 21 +------------------
 1 file changed, 1 insertion(+), 20 deletions(-)

diff --git a/packages/backend/src/server/api/common/generate-muted-user-query.ts b/packages/backend/src/server/api/common/generate-muted-user-query.ts
index e276ff2bd..470ece1a6 100644
--- a/packages/backend/src/server/api/common/generate-muted-user-query.ts
+++ b/packages/backend/src/server/api/common/generate-muted-user-query.ts
@@ -51,26 +51,7 @@ export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: {
 		.select('muting.muteeId')
 		.where('muting.muterId = :muterId', { muterId: me.id });
 
-	const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
-		.select('user_profile.mutedInstances')
-		.where('user_profile.userId = :muterId', { muterId: me.id });
-
-	q
-		.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`)
-		// mute instances
-		.andWhere(new Brackets(qb => { qb
-			.andWhere('note.userHost IS NULL')
-			.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
-		}))
-		.andWhere(new Brackets(qb => { qb
-			.where('note.replyUserHost IS NULL')
-			.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
-		}))
-		.andWhere(new Brackets(qb => { qb
-			.where('note.renoteUserHost IS NULL')
-			.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
-		}));
+	q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
 
 	q.setParameters(mutingQuery.getParameters());
-	q.setParameters(mutingInstanceQuery.getParameters());
 }

From 8d2c1c8658768592790199d0697370b10221b448 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 19:19:54 +0900
Subject: [PATCH 023/100] chore(client): tweak ui

---
 packages/client/src/components/widgets.vue | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/widgets.vue b/packages/client/src/components/widgets.vue
index 74dd79f73..be84cdbf7 100644
--- a/packages/client/src/components/widgets.vue
+++ b/packages/client/src/components/widgets.vue
@@ -19,7 +19,9 @@
 				<div class="customize-container">
 					<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
 					<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
-					<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="handle" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
+					<div class="handle">
+						<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
+					</div>
 				</div>
 			</template>
 		</XDraggable>
@@ -141,6 +143,12 @@ export default defineComponent({
 		> .remove {
 			right: 8px;
 		}
+
+		> .handle {
+			> .widget {
+				pointer-events: none;
+			}
+		}
 	}
 }
 </style>

From 986b12d4a82c4f654a27782f181ceaf5c441bc7b Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 20:15:14 +0900
Subject: [PATCH 024/100] chore(client): tweak ui

---
 .../server/api/endpoints/federation/stats.ts  |  5 +++--
 .../client/src/components/instance-stats.vue  | 20 +++++++++----------
 .../client/src/pages/admin/overview.pie.vue   | 14 ++++++-------
 packages/client/src/pages/admin/overview.vue  |  6 +++---
 4 files changed, 23 insertions(+), 22 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts
index d3c265908..cbe47dc7c 100644
--- a/packages/backend/src/server/api/endpoints/federation/stats.ts
+++ b/packages/backend/src/server/api/endpoints/federation/stats.ts
@@ -15,6 +15,7 @@ export const meta = {
 export const paramDef = {
 	type: 'object',
 	properties: {
+		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 	},
 	required: [],
 } as const;
@@ -29,7 +30,7 @@ export default define(meta, paramDef, async (ps) => {
 			order: {
 				followersCount: 'DESC',
 			},
-			take: 10,
+			take: ps.limit,
 		}),
 		Instances.find({
 			where: {
@@ -38,7 +39,7 @@ export default define(meta, paramDef, async (ps) => {
 			order: {
 				followingCount: 'DESC',
 			},
-			take: 10,
+			take: ps.limit,
 		}),
 		Followings.count({
 			where: {
diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue
index 9a1769a3a..1646a7e93 100644
--- a/packages/client/src/components/instance-stats.vue
+++ b/packages/client/src/components/instance-stats.vue
@@ -112,21 +112,21 @@ function createDoughnut(chartEl, tooltip, data) {
 			labels: data.map(x => x.name),
 			datasets: [{
 				backgroundColor: data.map(x => x.color),
+				borderWidth: 0,
+				spacing: 4,
+				hoverOffset: 4,
 				data: data.map(x => x.value),
 			}],
 		},
 		options: {
 			layout: {
 				padding: {
-					left: 8,
-					right: 8,
-					top: 8,
-					bottom: 8,
+					left: 16,
+					right: 16,
+					top: 16,
+					bottom: 16,
 				},
 			},
-			interaction: {
-				intersect: false,
-			},
 			plugins: {
 				legend: {
 					display: false,
@@ -145,9 +145,9 @@ function createDoughnut(chartEl, tooltip, data) {
 }
 
 onMounted(() => {
-	os.apiGet('federation/stats').then(fedStats => {
-		createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowersCount }]));
-		createDoughnut(pubDoughnutEl, externalTooltipHandler1, fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowingCount }]));
+	os.apiGet('federation/stats', { limit: 15 }).then(fedStats => {
+		createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
+		createDoughnut(pubDoughnutEl, externalTooltipHandler1, fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
 	});
 });
 </script>
diff --git a/packages/client/src/pages/admin/overview.pie.vue b/packages/client/src/pages/admin/overview.pie.vue
index d14b3cc6d..41a5e53ae 100644
--- a/packages/client/src/pages/admin/overview.pie.vue
+++ b/packages/client/src/pages/admin/overview.pie.vue
@@ -64,21 +64,21 @@ onMounted(() => {
 			labels: props.data.map(x => x.name),
 			datasets: [{
 				backgroundColor: props.data.map(x => x.color),
+				borderWidth: 0,
+				spacing: 4,
+				hoverOffset: 4,
 				data: props.data.map(x => x.value),
 			}],
 		},
 		options: {
 			layout: {
 				padding: {
-					left: 8,
-					right: 8,
-					top: 8,
-					bottom: 8,
+					left: 16,
+					right: 16,
+					top: 16,
+					bottom: 16,
 				},
 			},
-			interaction: {
-				intersect: false,
-			},
 			plugins: {
 				legend: {
 					display: false,
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index 6ccee8aea..393ee6645 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -123,12 +123,12 @@
 				<div class="body">
 					<div class="chart deliver">
 						<div class="title">Sub</div>
-						<XPie :data="fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowersCount }])"/>
+						<XPie :data="fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }])"/>
 						<div class="subTitle">Top 10</div>
 					</div>
 					<div class="chart inbox">
 						<div class="title">Pub</div>
-						<XPie :data="fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowingCount }])"/>
+						<XPie :data="fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }])"/>
 						<div class="subTitle">Top 10</div>
 					</div>
 				</div>
@@ -411,7 +411,7 @@ onMounted(async () => {
 		federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
 	});
 
-	os.apiGet('federation/stats').then(res => {
+	os.apiGet('federation/stats', { limit: 10 }).then(res => {
 		fedStats = res;
 	});
 

From bbafaace6e1c2be6215720d5e97a2c839890c7e0 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 20:15:40 +0900
Subject: [PATCH 025/100] update vite

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

diff --git a/packages/client/package.json b/packages/client/package.json
index f1ab23a55..56467fc84 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": "2.3.3",
+		"@vitejs/plugin-vue": "3.0.0-beta.0",
 		"@vue/compiler-sfc": "3.2.37",
 		"abort-controller": "3.0.0",
 		"autobind-decorator": "2.4.0",
@@ -74,7 +74,7 @@
 		"uuid": "8.3.2",
 		"v-debounce": "0.1.2",
 		"vanilla-tilt": "1.7.2",
-		"vite": "2.9.10",
+		"vite": "3.0.0-beta.5",
 		"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 0dc898800..7bbbe08d3 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@2.3.3":
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.3.tgz#fbf80cc039b82ac21a1acb0f0478de8f61fbf600"
-  integrity sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==
+"@vitejs/plugin-vue@3.0.0-beta.0":
+  version "3.0.0-beta.0"
+  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.0-beta.0.tgz#092f4f50ee183818e252331833541dbdcae1b91d"
+  integrity sha512-t8os1QK1qpovpgYAJSOWYEu+Doy/DZRW1cNwMvUl0qo+Yv7D9a3cxo24oL01lbojcc9ABQhyvUP3BsvFNtriqg==
 
 "@vue/compiler-core@3.2.37":
   version "3.2.37"
@@ -1534,131 +1534,131 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3:
     d "^1.0.1"
     ext "^1.1.2"
 
-esbuild-android-64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz#5b94a1306df31d55055f64a62ff6b763a47b7f64"
-  integrity sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw==
+esbuild-android-64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.48.tgz#7e6394a0e517f738641385aaf553c7e4fb6d1ae3"
+  integrity sha512-3aMjboap/kqwCUpGWIjsk20TtxVoKck8/4Tu19rubh7t5Ra0Yrpg30Mt1QXXlipOazrEceGeWurXKeFJgkPOUg==
 
-esbuild-android-arm64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz#78acc80773d16007de5219ccce544c036abd50b8"
-  integrity sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA==
+esbuild-android-arm64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.48.tgz#6877566be0f82dd5a43030c0007d06ece7f7c02f"
+  integrity sha512-vptI3K0wGALiDq+EvRuZotZrJqkYkN5282iAfcffjI5lmGG9G1ta/CIVauhY42MBXwEgDJkweiDcDMRLzBZC4g==
 
-esbuild-darwin-64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz#e02b1291f629ebdc2aa46fabfacc9aa28ff6aa46"
-  integrity sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA==
+esbuild-darwin-64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.48.tgz#ea3caddb707d88f844b1aa1dea5ff3b0a71ef1fd"
+  integrity sha512-gGQZa4+hab2Va/Zww94YbshLuWteyKGD3+EsVon8EWTWhnHFRm5N9NbALNbwi/7hQ/hM1Zm4FuHg+k6BLsl5UA==
 
-esbuild-darwin-arm64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz#01eb6650ec010b18c990e443a6abcca1d71290a9"
-  integrity sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ==
+esbuild-darwin-arm64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.48.tgz#4e5eaab54df66cc319b76a2ac0e8af4e6f0d9c2f"
+  integrity sha512-bFjnNEXjhZT+IZ8RvRGNJthLWNHV5JkCtuOFOnjvo5pC0sk2/QVk0Qc06g2PV3J0TcU6kaPC3RN9yy9w2PSLEA==
 
-esbuild-freebsd-64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz#790b8786729d4aac7be17648f9ea8e0e16475b5e"
-  integrity sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig==
+esbuild-freebsd-64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.48.tgz#47b5abc7426eae66861490ffbb380acc67af5b15"
+  integrity sha512-1NOlwRxmOsnPcWOGTB10JKAkYSb2nue0oM1AfHWunW/mv3wERfJmnYlGzL3UAOIUXZqW8GeA2mv+QGwq7DToqA==
 
-esbuild-freebsd-arm64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz#b66340ab28c09c1098e6d9d8ff656db47d7211e6"
-  integrity sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ==
+esbuild-freebsd-arm64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.48.tgz#e8c54c8637cd44feed967ea12338b0a4da3a7b11"
+  integrity sha512-gXqKdO8wabVcYtluAbikDH2jhXp+Klq5oCD5qbVyUG6tFiGhrC9oczKq3vIrrtwcxDQqK6+HDYK8Zrd4bCA9Gw==
 
-esbuild-linux-32@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz#7927f950986fd39f0ff319e92839455912b67f70"
-  integrity sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g==
+esbuild-linux-32@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.48.tgz#229cf3246de2b7937c3ac13fac622d4d7a1344c5"
+  integrity sha512-ghGyDfS289z/LReZQUuuKq9KlTiTspxL8SITBFQFAFRA/IkIvDpnZnCAKTCjGXAmUqroMQfKJXMxyjJA69c/nQ==
 
-esbuild-linux-64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz#4893d07b229d9cfe34a2b3ce586399e73c3ac519"
-  integrity sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q==
+esbuild-linux-64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.48.tgz#7c0e7226c02c42aacc5656c36977493dc1e96c4f"
+  integrity sha512-vni3p/gppLMVZLghI7oMqbOZdGmLbbKR23XFARKnszCIBpEMEDxOMNIKPmMItQrmH/iJrL1z8Jt2nynY0bE1ug==
 
-esbuild-linux-arm64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz#8442402e37d0b8ae946ac616784d9c1a2041056a"
-  integrity sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA==
+esbuild-linux-arm64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.48.tgz#0af1eda474b5c6cc0cace8235b74d0cb8fcf57a7"
+  integrity sha512-3CFsOlpoxlKPRevEHq8aAntgYGYkE1N9yRYAcPyng/p4Wyx0tPR5SBYsxLKcgPB9mR8chHEhtWYz6EZ+H199Zw==
 
-esbuild-linux-arm@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz#d5dbf32d38b7f79be0ec6b5fb2f9251fd9066986"
-  integrity sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA==
+esbuild-linux-arm@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.48.tgz#de4d1fa6b77cdcd00e2bb43dd0801e4680f0ab52"
+  integrity sha512-+VfSV7Akh1XUiDNXgqgY1cUP1i2vjI+BmlyXRfVz5AfV3jbpde8JTs5Q9sYgaoq5cWfuKfoZB/QkGOI+QcL1Tw==
 
-esbuild-linux-mips64le@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz#95081e42f698bbe35d8ccee0e3a237594b337eb5"
-  integrity sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ==
+esbuild-linux-mips64le@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.48.tgz#822c1778495f7868e990d4da47ad7281df28fd15"
+  integrity sha512-cs0uOiRlPp6ymknDnjajCgvDMSsLw5mST2UXh+ZIrXTj2Ifyf2aAP3Iw4DiqgnyYLV2O/v/yWBJx+WfmKEpNLA==
 
-esbuild-linux-ppc64le@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz#dceb0a1b186f5df679618882a7990bd422089b47"
-  integrity sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q==
+esbuild-linux-ppc64le@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.48.tgz#55de0a9ec4a48fedfe82a63e083164d001709447"
+  integrity sha512-+2F0vJMkuI0Wie/wcSPDCqXvSFEELH7Jubxb7mpWrA/4NpT+/byjxDz0gG6R1WJoeDefcrMfpBx4GFNN1JQorQ==
 
-esbuild-linux-riscv64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz#61fb8edb75f475f9208c4a93ab2bfab63821afd2"
-  integrity sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ==
+esbuild-linux-riscv64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.48.tgz#cd2b7381880b2f4b21a5a598fb673492120f18a5"
+  integrity sha512-BmaK/GfEE+5F2/QDrIXteFGKnVHGxlnK9MjdVKMTfvtmudjY3k2t8NtlY4qemKSizc+QwyombGWTBDc76rxePA==
 
-esbuild-linux-s390x@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz#34c7126a4937406bf6a5e69100185fd702d12fe0"
-  integrity sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ==
+esbuild-linux-s390x@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.48.tgz#4b319eca2a5c64637fc7397ffbd9671719cdb6bf"
+  integrity sha512-tndw/0B9jiCL+KWKo0TSMaUm5UWBLsfCKVdbfMlb3d5LeV9WbijZ8Ordia8SAYv38VSJWOEt6eDCdOx8LqkC4g==
 
-esbuild-netbsd-64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz#322ea9937d9e529183ee281c7996b93eb38a5d95"
-  integrity sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q==
+esbuild-netbsd-64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.48.tgz#c27cde8b5cb55dcc227943a18ab078fb98d0adbf"
+  integrity sha512-V9hgXfwf/T901Lr1wkOfoevtyNkrxmMcRHyticybBUHookznipMOHoF41Al68QBsqBxnITCEpjjd4yAos7z9Tw==
 
-esbuild-openbsd-64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz#1ca29bb7a2bf09592dcc26afdb45108f08a2cdbd"
-  integrity sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ==
+esbuild-openbsd-64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.48.tgz#af5ab2d1cb41f09064bba9465fc8bf1309150df1"
+  integrity sha512-+IHf4JcbnnBl4T52egorXMatil/za0awqzg2Vy6FBgPcBpisDWT2sVz/tNdrK9kAqj+GZG/jZdrOkj7wsrNTKA==
 
-esbuild-sunos-64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz#c9446f7d8ebf45093e7bb0e7045506a88540019b"
-  integrity sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA==
+esbuild-sunos-64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.48.tgz#db3ae20526055cf6fd5c4582676233814603ac54"
+  integrity sha512-77m8bsr5wOpOWbGi9KSqDphcq6dFeJyun8TA+12JW/GAjyfTwVtOnN8DOt6DSPUfEV+ltVMNqtXUeTeMAxl5KA==
 
-esbuild-windows-32@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz#f8e9b4602fd0ccbd48e5c8d117ec0ba4040f2ad1"
-  integrity sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw==
+esbuild-windows-32@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.48.tgz#021ffceb0a3f83078262870da88a912293c57475"
+  integrity sha512-EPgRuTPP8vK9maxpTGDe5lSoIBHGKO/AuxDncg5O3NkrPeLNdvvK8oywB0zGaAZXxYWfNNSHskvvDgmfVTguhg==
 
-esbuild-windows-64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz#280f58e69f78535f470905ce3e43db1746518107"
-  integrity sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw==
+esbuild-windows-64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.48.tgz#a4d3407b580f9faac51f61eec095fa985fb3fee4"
+  integrity sha512-YmpXjdT1q0b8ictSdGwH3M8VCoqPpK1/UArze3X199w6u8hUx3V8BhAi1WjbsfDYRBanVVtduAhh2sirImtAvA==
 
-esbuild-windows-arm64@0.14.38:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz#d97e9ac0f95a4c236d9173fa9f86c983d6a53f54"
-  integrity sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw==
+esbuild-windows-arm64@0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.48.tgz#762c0562127d8b09bfb70a3c816460742dd82880"
+  integrity sha512-HHaOMCsCXp0rz5BT2crTka6MPWVno121NKApsGs/OIW5QC0ggC69YMGs1aJct9/9FSUF4A1xNE/cLvgB5svR4g==
 
-esbuild@^0.14.27:
-  version "0.14.38"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.38.tgz#99526b778cd9f35532955e26e1709a16cca2fb30"
-  integrity sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA==
+esbuild@^0.14.47:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.48.tgz#da5d8d25cd2d940c45ea0cfecdca727f7aee2b85"
+  integrity sha512-w6N1Yn5MtqK2U1/WZTX9ZqUVb8IOLZkZ5AdHkT6x3cHDMVsYWC7WPdiLmx19w3i4Rwzy5LqsEMtVihG3e4rFzA==
   optionalDependencies:
-    esbuild-android-64 "0.14.38"
-    esbuild-android-arm64 "0.14.38"
-    esbuild-darwin-64 "0.14.38"
-    esbuild-darwin-arm64 "0.14.38"
-    esbuild-freebsd-64 "0.14.38"
-    esbuild-freebsd-arm64 "0.14.38"
-    esbuild-linux-32 "0.14.38"
-    esbuild-linux-64 "0.14.38"
-    esbuild-linux-arm "0.14.38"
-    esbuild-linux-arm64 "0.14.38"
-    esbuild-linux-mips64le "0.14.38"
-    esbuild-linux-ppc64le "0.14.38"
-    esbuild-linux-riscv64 "0.14.38"
-    esbuild-linux-s390x "0.14.38"
-    esbuild-netbsd-64 "0.14.38"
-    esbuild-openbsd-64 "0.14.38"
-    esbuild-sunos-64 "0.14.38"
-    esbuild-windows-32 "0.14.38"
-    esbuild-windows-64 "0.14.38"
-    esbuild-windows-arm64 "0.14.38"
+    esbuild-android-64 "0.14.48"
+    esbuild-android-arm64 "0.14.48"
+    esbuild-darwin-64 "0.14.48"
+    esbuild-darwin-arm64 "0.14.48"
+    esbuild-freebsd-64 "0.14.48"
+    esbuild-freebsd-arm64 "0.14.48"
+    esbuild-linux-32 "0.14.48"
+    esbuild-linux-64 "0.14.48"
+    esbuild-linux-arm "0.14.48"
+    esbuild-linux-arm64 "0.14.48"
+    esbuild-linux-mips64le "0.14.48"
+    esbuild-linux-ppc64le "0.14.48"
+    esbuild-linux-riscv64 "0.14.48"
+    esbuild-linux-s390x "0.14.48"
+    esbuild-netbsd-64 "0.14.48"
+    esbuild-openbsd-64 "0.14.48"
+    esbuild-sunos-64 "0.14.48"
+    esbuild-windows-32 "0.14.48"
+    esbuild-windows-64 "0.14.48"
+    esbuild-windows-arm64 "0.14.48"
 
 escalade@^3.1.1:
   version "3.1.1"
@@ -2484,6 +2484,13 @@ is-core-module@^2.8.1:
   dependencies:
     has "^1.0.3"
 
+is-core-module@^2.9.0:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
+  integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
+  dependencies:
+    has "^1.0.3"
+
 is-date-object@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
@@ -2995,6 +3002,11 @@ nanoid@3.3.3, nanoid@^3.3.3:
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
   integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
 
+nanoid@^3.3.4:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
+  integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -3302,7 +3314,7 @@ postcss-selector-parser@^6.0.9:
     cssesc "^3.0.0"
     util-deprecate "^1.0.2"
 
-postcss@^8.1.10, postcss@^8.4.13:
+postcss@^8.1.10:
   version "8.4.13"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575"
   integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==
@@ -3311,6 +3323,15 @@ postcss@^8.1.10, postcss@^8.4.13:
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
 
+postcss@^8.4.14:
+  version "8.4.14"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
+  integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
+  dependencies:
+    nanoid "^3.3.4"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -3591,6 +3612,15 @@ resolve@^1.22.0:
     path-parse "^1.0.7"
     supports-preserve-symlinks-flag "^1.0.0"
 
+resolve@^1.22.1:
+  version "1.22.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+  integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+  dependencies:
+    is-core-module "^2.9.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
 restore-cursor@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
@@ -3626,10 +3656,10 @@ rollup@2.75.6:
   optionalDependencies:
     fsevents "~2.3.2"
 
-rollup@^2.59.0:
-  version "2.70.2"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.2.tgz#808d206a8851628a065097b7ba2053bd83ba0c0d"
-  integrity sha512-EitogNZnfku65I1DD5Mxe8JYRUCy0hkK5X84IlDtUs+O6JRMpRciXTzyCUuX11b5L5pvjH+OmFXiQ3XjabcXgg==
+rollup@^2.75.6:
+  version "2.75.7"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.75.7.tgz#221ff11887ae271e37dcc649ba32ce1590aaa0b9"
+  integrity sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ==
   optionalDependencies:
     fsevents "~2.3.2"
 
@@ -4192,15 +4222,15 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
-vite@2.9.10:
-  version "2.9.10"
-  resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.10.tgz#f574d96655622c2e0fbc662edd0ed199c60fe91a"
-  integrity sha512-TwZRuSMYjpTurLqXspct+HZE7ONiW9d+wSWgvADGxhDPPyoIcNywY+RX4ng+QpK30DCa1l/oZgi2PLZDibhzbQ==
+vite@3.0.0-beta.5:
+  version "3.0.0-beta.5"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.0-beta.5.tgz#708d5b732dee98d77877cb094b567f5596508b5b"
+  integrity sha512-SfesZuCME4fEmLy4hgsJAg55HRiTgDhH3oPM44XePrdKP5FqYvDkzpSWl6ldDOJYTskKWafGyyuYfXoxodv40Q==
   dependencies:
-    esbuild "^0.14.27"
-    postcss "^8.4.13"
-    resolve "^1.22.0"
-    rollup "^2.59.0"
+    esbuild "^0.14.47"
+    postcss "^8.4.14"
+    resolve "^1.22.1"
+    rollup "^2.75.6"
   optionalDependencies:
     fsevents "~2.3.2"
 

From 943fc6ebf202b7539103ba30682c7024da3b2b0f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 20:15:47 +0900
Subject: [PATCH 026/100] 12.112.0-beta.9

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

diff --git a/package.json b/package.json
index 11962a72a..56ffe9ee4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.8",
+	"version": "12.112.0-beta.9",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 0c04efa42500e0651679a7e06558e99da0e5bf84 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 21:38:34 +0900
Subject: [PATCH 027/100] chore(client): tweak ui

---
 .../client/src/components/instance-stats.vue  |   6 +-
 .../client/src/pages/admin/overview.pie.vue   |   6 +-
 packages/client/src/pages/gallery/edit.vue    |  49 ++++----
 packages/client/src/pages/gallery/index.vue   |  33 ++++--
 packages/client/src/pages/gallery/post.vue    | 110 +++++++++---------
 packages/client/src/pages/user/gallery.vue    |  40 +++----
 6 files changed, 126 insertions(+), 118 deletions(-)

diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue
index 1646a7e93..2f83b2f89 100644
--- a/packages/client/src/components/instance-stats.vue
+++ b/packages/client/src/components/instance-stats.vue
@@ -112,9 +112,9 @@ function createDoughnut(chartEl, tooltip, data) {
 			labels: data.map(x => x.name),
 			datasets: [{
 				backgroundColor: data.map(x => x.color),
-				borderWidth: 0,
-				spacing: 4,
-				hoverOffset: 4,
+				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
+				borderWidth: 2,
+				hoverOffset: 0,
 				data: data.map(x => x.value),
 			}],
 		},
diff --git a/packages/client/src/pages/admin/overview.pie.vue b/packages/client/src/pages/admin/overview.pie.vue
index 41a5e53ae..667f236d9 100644
--- a/packages/client/src/pages/admin/overview.pie.vue
+++ b/packages/client/src/pages/admin/overview.pie.vue
@@ -64,9 +64,9 @@ onMounted(() => {
 			labels: props.data.map(x => x.name),
 			datasets: [{
 				backgroundColor: props.data.map(x => x.color),
-				borderWidth: 0,
-				spacing: 4,
-				hoverOffset: 4,
+				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
+				borderWidth: 2,
+				hoverOffset: 0,
 				data: props.data.map(x => x.value),
 			}],
 		},
diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue
index fa3063bde..1de8328fe 100644
--- a/packages/client/src/pages/gallery/edit.vue
+++ b/packages/client/src/pages/gallery/edit.vue
@@ -1,30 +1,33 @@
 <template>
-<div>
-	<FormSuspense :p="init">
-		<FormInput v-model="title">
-			<template #label>{{ $ts.title }}</template>
-		</FormInput>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
+		<FormSuspense :p="init">
+			<FormInput v-model="title">
+				<template #label>{{ $ts.title }}</template>
+			</FormInput>
 
-		<FormTextarea v-model="description" :max="500">
-			<template #label>{{ $ts.description }}</template>
-		</FormTextarea>
+			<FormTextarea v-model="description" :max="500">
+				<template #label>{{ $ts.description }}</template>
+			</FormTextarea>
 
-		<div class="">
-			<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
-				<div class="name">{{ file.name }}</div>
-				<button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
+			<div class="">
+				<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
+					<div class="name">{{ file.name }}</div>
+					<button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
+				</div>
+				<FormButton primary @click="selectFile"><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
 			</div>
-			<FormButton primary @click="selectFile"><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
-		</div>
 
-		<FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
+			<FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
 
-		<FormButton v-if="postId" primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-		<FormButton v-else primary @click="save"><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
+			<FormButton v-if="postId" primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+			<FormButton v-else primary @click="save"><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
 
-		<FormButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
-	</FormSuspense>
-</div>
+			<FormButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
+		</FormSuspense>
+	</MkSpacer>
+</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
@@ -71,7 +74,7 @@ async function save() {
 			fileIds: files.map(file => file.id),
 			isSensitive: isSensitive,
 		});
-		mainRouter.push(`/gallery/${props.postId}`);
+		router.push(`/gallery/${props.postId}`);
 	} else {
 		const created = await os.apiWithDialog('gallery/posts/create', {
 			title: title,
@@ -92,7 +95,7 @@ async function del() {
 	await os.apiWithDialog('gallery/posts/delete', {
 		postId: props.postId,
 	});
-	mainRouter.push('/gallery');
+	router.push('/gallery');
 }
 
 watch(() => props.postId, () => {
@@ -113,9 +116,11 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => props.postId ? {
 	title: i18n.ts.edit,
 	icon: 'fas fa-pencil-alt',
+	bg: 'var(--bg)',
 } : {
 	title: i18n.ts.postToGallery,
 	icon: 'fas fa-pencil-alt',
+	bg: 'var(--bg)',
 }));
 </script>
 
diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue
index b26470dbe..1eb6ce22f 100644
--- a/packages/client/src/pages/gallery/index.vue
+++ b/packages/client/src/pages/gallery/index.vue
@@ -1,14 +1,8 @@
 <template>
 <MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="1400">
 		<div class="_root">
-			<MkTab v-if="$i" v-model="tab">
-				<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
-				<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
-				<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
-			</MkTab>
-
 			<div v-if="tab === 'explore'">
 				<MkFolder class="_gap">
 					<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
@@ -60,6 +54,9 @@ import number from '@/filters/number';
 import * as os from '@/os';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { i18n } from '@/i18n';
+import { useRouter } from '@/router';
+
+const router = useRouter();
 
 const props = defineProps<{
 	tag?: string;
@@ -100,9 +97,27 @@ watch(() => props.tag, () => {
 	if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
 });
 
-const headerActions = $computed(() => []);
+const headerActions = $computed(() => [{
+	icon: 'fas fa-plus',
+	text: i18n.ts.create,
+	handler: () => {
+		router.push('/gallery/new');
+	},
+}]);
 
-const headerTabs = $computed(() => []);
+const headerTabs = $computed(() => [{
+	key: 'explore',
+	title: i18n.ts.gallery,
+	icon: 'fas fa-icons',
+}, {
+	key: 'liked',
+	title: i18n.ts._gallery.liked,
+	icon: 'fas fa-heart',
+}, {
+	key: 'my',
+	title: i18n.ts._gallery.my,
+	icon: 'fas fa-edit',
+}]);
 
 definePageMetadata({
 	title: i18n.ts.gallery,
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index d5f3253b3..6651c3e28 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -1,52 +1,57 @@
 <template>
-<div class="_root">
-	<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
-		<div v-if="post" class="rkxwuolj">
-			<div class="files">
-				<div v-for="file in post.files" :key="file.id" class="file">
-					<img :src="file.url"/>
-				</div>
-			</div>
-			<div class="body _block">
-				<div class="title">{{ post.title }}</div>
-				<div class="description"><Mfm :text="post.description"/></div>
-				<div class="info">
-					<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
-				</div>
-				<div class="actions">
-					<div class="like">
-						<MkButton v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
-						<MkButton v-else v-tooltip="$ts._gallery.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="1000" :margin-min="16" :margin-max="32">
+		<div class="_root">
+			<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
+				<div v-if="post" class="rkxwuolj">
+					<div class="files">
+						<div v-for="file in post.files" :key="file.id" class="file">
+							<img :src="file.url"/>
+						</div>
 					</div>
-					<div class="other">
-						<button v-if="$i && $i.id === post.user.id" v-tooltip="$ts.edit" v-click-anime class="_button" @click="edit"><i class="fas fa-pencil-alt fa-fw"></i></button>
-						<button v-tooltip="$ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
-						<button v-tooltip="$ts.share" v-click-anime class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
+					<div class="body _block">
+						<div class="title">{{ post.title }}</div>
+						<div class="description"><Mfm :text="post.description"/></div>
+						<div class="info">
+							<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
+						</div>
+						<div class="actions">
+							<div class="like">
+								<MkButton v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
+								<MkButton v-else v-tooltip="$ts._gallery.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
+							</div>
+							<div class="other">
+								<button v-if="$i && $i.id === post.user.id" v-tooltip="$ts.edit" v-click-anime class="_button" @click="edit"><i class="fas fa-pencil-alt fa-fw"></i></button>
+								<button v-tooltip="$ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
+								<button v-tooltip="$ts.share" v-click-anime class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
+							</div>
+						</div>
+						<div class="user">
+							<MkAvatar :user="post.user" class="avatar"/>
+							<div class="name">
+								<MkUserName :user="post.user" style="display: block;"/>
+								<MkAcct :user="post.user"/>
+							</div>
+							<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
+						</div>
 					</div>
+					<MkAd :prefer="['horizontal', 'horizontal-big']"/>
+					<MkContainer :max-height="300" :foldable="true" class="other">
+						<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
+						<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
+							<div class="sdrarzaf">
+								<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
+							</div>
+						</MkPagination>
+					</MkContainer>
 				</div>
-				<div class="user">
-					<MkAvatar :user="post.user" class="avatar"/>
-					<div class="name">
-						<MkUserName :user="post.user" style="display: block;"/>
-						<MkAcct :user="post.user"/>
-					</div>
-					<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
-				</div>
-			</div>
-			<MkAd :prefer="['horizontal', 'horizontal-big']"/>
-			<MkContainer :max-height="300" :foldable="true" class="other">
-				<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
-				<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
-					<div class="sdrarzaf">
-						<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
-					</div>
-				</MkPagination>
-			</MkContainer>
+				<MkError v-else-if="error" @retry="fetch()"/>
+				<MkLoading v-else/>
+			</transition>
 		</div>
-		<MkError v-else-if="error" @retry="fetch()"/>
-		<MkLoading v-else/>
-	</transition>
-</div>
+	</MkSpacer>
+</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
@@ -133,23 +138,18 @@ function edit() {
 
 watch(() => props.postId, fetchPost, { immediate: true });
 
-const headerActions = $computed(() => []);
+const headerActions = $computed(() => [{
+	icon: 'fas fa-pencil-alt',
+	text: i18n.ts.edit,
+	handler: edit,
+}]);
 
 const headerTabs = $computed(() => []);
 
 definePageMetadata(computed(() => post ? {
 	title: post.title,
 	avatar: post.user,
-	path: `/gallery/${post.id}`,
-	share: {
-		title: post.title,
-		text: post.description,
-	},
-	actions: [{
-		icon: 'fas fa-pencil-alt',
-		text: i18n.ts.edit,
-		handler: edit,
-	}],
+	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue
index 07dda4a29..6af28d455 100644
--- a/packages/client/src/pages/user/gallery.vue
+++ b/packages/client/src/pages/user/gallery.vue
@@ -8,36 +8,24 @@
 </div>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
 import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
 import MkPagination from '@/components/ui/pagination.vue';
 
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkGalleryPostPreview,
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/gallery/posts' as const,
-				limit: 6,
-				params: computed(() => ({
-					userId: this.user.id
-				})),
-			},
-		};
-	},
+const props = withDefaults(defineProps<{
+	user: misskey.entities.User;
+}>(), {
 });
+
+const pagination = {
+	endpoint: 'users/gallery/posts' as const,
+	limit: 6,
+	params: computed(() => ({
+		userId: props.user.id,
+	})),
+};
 </script>
 
 <style lang="scss" scoped>

From 03b377eea6249d351e5ff06b3d3e1a8548a5f509 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 22:02:08 +0900
Subject: [PATCH 028/100] chore(client): tweak ui

---
 .../client/src/components/instance-stats.vue  | 29 +++++++++++++++++--
 .../client/src/pages/admin/overview.pie.vue   |  8 ++++-
 packages/client/src/pages/admin/overview.vue  | 26 +++++++++++++----
 3 files changed, 54 insertions(+), 9 deletions(-)

diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue
index 2f83b2f89..c210371c6 100644
--- a/packages/client/src/components/instance-stats.vue
+++ b/packages/client/src/components/instance-stats.vue
@@ -106,7 +106,7 @@ const { handler: externalTooltipHandler1 } = useChartTooltip();
 const { handler: externalTooltipHandler2 } = useChartTooltip();
 
 function createDoughnut(chartEl, tooltip, data) {
-	return new Chart(chartEl, {
+	const chartInstance = new Chart(chartEl, {
 		type: 'doughnut',
 		data: {
 			labels: data.map(x => x.name),
@@ -127,6 +127,12 @@ function createDoughnut(chartEl, tooltip, data) {
 					bottom: 16,
 				},
 			},
+			onClick: (ev) => {
+				const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
+				if (hit && data[hit.index].onClick) {
+					data[hit.index].onClick();
+				}
+			},
 			plugins: {
 				legend: {
 					display: false,
@@ -142,12 +148,29 @@ function createDoughnut(chartEl, tooltip, data) {
 			},
 		},
 	});
+
+	return chartInstance;
 }
 
 onMounted(() => {
 	os.apiGet('federation/stats', { limit: 15 }).then(fedStats => {
-		createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
-		createDoughnut(pubDoughnutEl, externalTooltipHandler1, fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
+		createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
+			name: x.host,
+			color: x.themeColor,
+			value: x.followersCount,
+			onClick: () => {
+				os.pageWindow(`/instance-info/${x.host}`);
+			},
+		})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
+
+		createDoughnut(pubDoughnutEl, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
+			name: x.host,
+			color: x.themeColor,
+			value: x.followingCount,
+			onClick: () => {
+				os.pageWindow(`/instance-info/${x.host}`);
+			},
+		})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
 	});
 });
 </script>
diff --git a/packages/client/src/pages/admin/overview.pie.vue b/packages/client/src/pages/admin/overview.pie.vue
index 667f236d9..d3b203287 100644
--- a/packages/client/src/pages/admin/overview.pie.vue
+++ b/packages/client/src/pages/admin/overview.pie.vue
@@ -45,7 +45,7 @@ Chart.register(
 );
 
 const props = defineProps<{
-	data: { name: string; value: number; color: string; }[];
+	data: { name: string; value: number; color: string; onClick?: () => void }[];
 }>();
 
 const chartEl = ref<HTMLCanvasElement>(null);
@@ -79,6 +79,12 @@ onMounted(() => {
 					bottom: 16,
 				},
 			},
+			onClick: (ev) => {
+				const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
+				if (hit && props.data[hit.index].onClick) {
+					props.data[hit.index].onClick();
+				}
+			},
 			plugins: {
 				legend: {
 					display: false,
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index 393ee6645..d2fa4e0e4 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -119,16 +119,16 @@
 					</MkTagCloud>
 				</div>
 			</div>
-			<div v-if="fedStats" class="container federationPies">
+			<div v-if="topSubInstancesForPie && topPubInstancesForPie" class="container federationPies">
 				<div class="body">
 					<div class="chart deliver">
 						<div class="title">Sub</div>
-						<XPie :data="fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }])"/>
+						<XPie :data="topSubInstancesForPie"/>
 						<div class="subTitle">Top 10</div>
 					</div>
 					<div class="chart inbox">
 						<div class="title">Pub</div>
-						<XPie :data="fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }])"/>
+						<XPie :data="topPubInstancesForPie"/>
 						<div class="subTitle">Top 10</div>
 					</div>
 				</div>
@@ -200,7 +200,8 @@ const rootEl = $ref<HTMLElement>();
 const chartEl = $ref<HTMLCanvasElement>(null);
 let stats: any = $ref(null);
 let serverInfo: any = $ref(null);
-let fedStats: any = $ref(null);
+let topSubInstancesForPie: any = $ref(null);
+let topPubInstancesForPie: any = $ref(null);
 let usersComparedToThePrevDay: any = $ref(null);
 let notesComparedToThePrevDay: any = $ref(null);
 let federationPubActive = $ref<number | null>(null);
@@ -412,7 +413,22 @@ onMounted(async () => {
 	});
 
 	os.apiGet('federation/stats', { limit: 10 }).then(res => {
-		fedStats = res;
+		topSubInstancesForPie = fedStats.topSubInstances.map(x => ({
+			name: x.host,
+			color: x.themeColor,
+			value: x.followersCount,
+			onClick: () => {
+				os.pageWindow(`/instance-info/${x.host}`);
+			},
+		})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]);
+		topPubInstancesForPie = fedStats.topPubInstances.map(x => ({
+			name: x.host,
+			color: x.themeColor,
+			value: x.followingCount,
+			onClick: () => {
+				os.pageWindow(`/instance-info/${x.host}`);
+			},
+		})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]);
 	});
 
 	os.api('admin/server-info').then(serverInfoResponse => {

From b3c442860d4c7eb068a00143e4053c4660f4c376 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 23:07:45 +0900
Subject: [PATCH 029/100] chore(client): tweak style

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

diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue
index 05e95a091..fe8c08cd6 100644
--- a/packages/client/src/components/form/select.vue
+++ b/packages/client/src/components/form/select.vue
@@ -214,6 +214,7 @@ const onClick = (ev: MouseEvent) => {
 			cursor: pointer;
 			transition: border-color 0.1s ease-out;
 			pointer-events: none;
+			user-select: none;
 		}
 
 		> .prefix,

From 04845cd3e105a3a57b153e970393e083943876d8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 23:45:11 +0900
Subject: [PATCH 030/100] feat(client): add rss-marquee widget

---
 CHANGELOG.md                                |   1 +
 locales/ja-JP.yml                           |   1 +
 packages/client/package.json                |   1 +
 packages/client/src/widgets/index.ts        |   4 +-
 packages/client/src/widgets/rss-marquee.vue | 115 ++++++++++++++++++++
 packages/client/src/widgets/rss.vue         |  12 +-
 packages/client/yarn.lock                   |  15 ++-
 7 files changed, 141 insertions(+), 8 deletions(-)
 create mode 100644 packages/client/src/widgets/rss-marquee.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 063ebf525..405c92bb3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ You should also include the user name that made the change.
 - Client: Improve control panel @syuilo
 - Client: Show warning in control panel when there is an unresolved abuse report @syuilo
 - Client: Add instance-cloud widget @syuilo
+- Client: Add rss-marquee widget @syuilo
 - Make possible to delete an account by admin @syuilo
 - Improve player detection in URL preview @mei23
 - Add Badge Image to Push Notification #8012 @tamaina
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 743640725..1f52c2c25 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1246,6 +1246,7 @@ _widgets:
   trends: "トレンド"
   clock: "時計"
   rss: "RSSリーダー"
+  rssMarquee: "RSSリーダー(マーキー)"
   activity: "アクティビティ"
   photos: "フォト"
   digitalClock: "デジタル時計"
diff --git a/packages/client/package.json b/packages/client/package.json
index 56467fc84..54ee7dee5 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -76,6 +76,7 @@
 		"vanilla-tilt": "1.7.2",
 		"vite": "3.0.0-beta.5",
 		"vue": "3.2.37",
+		"vue-marquee-text-component": "2.0.1",
 		"vue-prism-editor": "2.0.0-alpha.2",
 		"vuedraggable": "4.0.1",
 		"websocket": "1.0.34",
diff --git a/packages/client/src/widgets/index.ts b/packages/client/src/widgets/index.ts
index feda16c91..ed65f5291 100644
--- a/packages/client/src/widgets/index.ts
+++ b/packages/client/src/widgets/index.ts
@@ -6,6 +6,7 @@ export default function(app: App) {
 	app.component('MkwTimeline', defineAsyncComponent(() => import('./timeline.vue')));
 	app.component('MkwCalendar', defineAsyncComponent(() => import('./calendar.vue')));
 	app.component('MkwRss', defineAsyncComponent(() => import('./rss.vue')));
+	app.component('MkwRssMarquee', defineAsyncComponent(() => import('./rss-marquee.vue')));
 	app.component('MkwTrends', defineAsyncComponent(() => import('./trends.vue')));
 	app.component('MkwClock', defineAsyncComponent(() => import('./clock.vue')));
 	app.component('MkwActivity', defineAsyncComponent(() => import('./activity.vue')));
@@ -29,13 +30,14 @@ export const widgets = [
 	'timeline',
 	'calendar',
 	'rss',
+	'rssMarquee',
 	'trends',
 	'clock',
 	'activity',
 	'photos',
 	'digitalClock',
 	'federation',
-	'instance-cloud',
+	'instanceCloud',
 	'postForm',
 	'slideshow',
 	'serverMetric',
diff --git a/packages/client/src/widgets/rss-marquee.vue b/packages/client/src/widgets/rss-marquee.vue
new file mode 100644
index 000000000..e23f916d3
--- /dev/null
+++ b/packages/client/src/widgets/rss-marquee.vue
@@ -0,0 +1,115 @@
+<template>
+<MkContainer :naked="widgetProps.transparent" :show-header="widgetProps.showHeader" class="mkw-rss-marquee">
+	<template #header><i class="fas fa-rss-square"></i>RSS</template>
+	<template #func><button class="_button" @click="configure"><i class="fas fa-cog"></i></button></template>
+
+	<div class="ekmkgxbk">
+		<MkLoading v-if="fetching"/>
+		<div v-else class="feed">
+			<MarqueeText :duration="widgetProps.speed" :reverse="widgetProps.reverse">
+				<a v-for="item in items" class="item" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
+			</MarqueeText>
+		</div>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref, watch } from 'vue';
+import MarqueeText from 'vue-marquee-text-component';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { GetFormResultType } from '@/scripts/form';
+import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
+import { useInterval } from '@/scripts/use-interval';
+
+const name = 'rssMarquee';
+
+const widgetPropsDef = {
+	url: {
+		type: 'string' as const,
+		default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
+	},
+	showHeader: {
+		type: 'boolean' as const,
+		default: false,
+	},
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+	speed: {
+		type: 'radio' as const,
+		default: 70,
+		options: [{
+			value: 170, label: 'very slow',
+		}, {
+			value: 100, label: 'slow',
+		}, {
+			value: 70, label: 'medium',
+		}, {
+			value: 40, label: 'fast',
+		}, {
+			value: 20, label: 'very fast',
+		}],
+	},
+	reverse: {
+		type: 'boolean' as const,
+		default: false,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const items = ref([]);
+const fetching = ref(true);
+
+const tick = () => {
+	fetch(`https://api.rss2json.com/v1/api.json?rss_url=${widgetProps.url}`, {}).then(res => {
+		res.json().then(feed => {
+			items.value = feed.items;
+			fetching.value = false;
+		});
+	});
+};
+
+watch(() => widgetProps.url, tick);
+
+useInterval(tick, 60000, {
+	immediate: true,
+	afterMounted: true,
+});
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
+});
+</script>
+
+<style lang="scss" scoped>
+.ekmkgxbk {
+	> .feed {
+		padding: 0;
+		font-size: 0.9em;
+
+		::v-deep(.item) {
+			display: inline-block;
+			color: var(--fg);
+			margin: 12px 3em 12px 0;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue
index e5da291a8..ea896478a 100644
--- a/packages/client/src/widgets/rss.vue
+++ b/packages/client/src/widgets/rss.vue
@@ -6,7 +6,7 @@
 	<div class="ekmkgxbj">
 		<MkLoading v-if="fetching"/>
 		<div v-else class="feed">
-			<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
+			<a v-for="item in items" class="item" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
 		</div>
 	</div>
 </MkContainer>
@@ -23,14 +23,14 @@ import { useInterval } from '@/scripts/use-interval';
 const name = 'rss';
 
 const widgetPropsDef = {
-	showHeader: {
-		type: 'boolean' as const,
-		default: true,
-	},
 	url: {
 		type: 'string' as const,
 		default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
 	},
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
 };
 
 type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@@ -79,7 +79,7 @@ defineExpose<WidgetComponentExpose>({
 		padding: 0;
 		font-size: 0.9em;
 
-		> a {
+		> .item {
 			display: block;
 			padding: 8px 16px;
 			color: var(--fg);
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index 7bbbe08d3..58df1576f 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -1221,6 +1221,11 @@ content-disposition@0.5.4:
   dependencies:
     safe-buffer "5.2.1"
 
+core-js@^3.18.0:
+  version "3.23.3"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.3.tgz#3b977612b15da6da0c9cc4aec487e8d24f371112"
+  integrity sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q==
+
 core-util-is@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -4252,12 +4257,20 @@ vue-eslint-parser@^9.0.1:
     lodash "^4.17.21"
     semver "^7.3.6"
 
+vue-marquee-text-component@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/vue-marquee-text-component/-/vue-marquee-text-component-2.0.1.tgz#62691df195f755471fa9bdc9b1969f836a922b9a"
+  integrity sha512-dbeRwDY5neOJcWZrDFU2tJMhPSsxN25ZpNYeZdt0jkseg1MbyGKzrfEH9nrCFZRkEfqhxG+ukyzwVwR9US5sTQ==
+  dependencies:
+    core-js "^3.18.0"
+    vue "^3.2.19"
+
 vue-prism-editor@2.0.0-alpha.2:
   version "2.0.0-alpha.2"
   resolved "https://registry.yarnpkg.com/vue-prism-editor/-/vue-prism-editor-2.0.0-alpha.2.tgz#aa53a88efaaed628027cbb282c2b1d37fc7c5c69"
   integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
 
-vue@3.2.37:
+vue@3.2.37, vue@^3.2.19:
   version "3.2.37"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
   integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==

From d9b55a4bea5753edb8533dcdddde23f95fda5cdf Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 23:51:18 +0900
Subject: [PATCH 031/100] chore(client): tweak ui

---
 .../components/reactions-viewer.reaction.vue  | 135 +++++++-----------
 packages/client/src/scripts/use-tooltip.ts    |   5 +-
 2 files changed, 58 insertions(+), 82 deletions(-)

diff --git a/packages/client/src/components/reactions-viewer.reaction.vue b/packages/client/src/components/reactions-viewer.reaction.vue
index 91a90a699..ee40615f1 100644
--- a/packages/client/src/components/reactions-viewer.reaction.vue
+++ b/packages/client/src/components/reactions-viewer.reaction.vue
@@ -12,106 +12,81 @@
 </button>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, onMounted, ref, watch } from 'vue';
+<script lang="ts" setup>
+import { computed, onMounted, ref, watch } from 'vue';
+import * as misskey from 'misskey-js';
 import XDetails from '@/components/reactions-viewer.details.vue';
 import XReactionIcon from '@/components/reaction-icon.vue';
 import * as os from '@/os';
 import { useTooltip } from '@/scripts/use-tooltip';
 import { $i } from '@/account';
 
-export default defineComponent({
-	components: {
-		XReactionIcon
-	},
+const props = defineProps<{
+	reaction: string;
+	count: number;
+	isInitial: boolean;
+	note: misskey.entities.Note;
+}>();
 
-	props: {
-		reaction: {
-			type: String,
-			required: true,
-		},
-		count: {
-			type: Number,
-			required: true,
-		},
-		isInitial: {
-			type: Boolean,
-			required: true,
-		},
-		note: {
-			type: Object,
-			required: true,
-		},
-	},
+const buttonRef = ref<HTMLElement>();
 
-	setup(props) {
-		const buttonRef = ref<HTMLElement>();
+const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
 
-		const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
+const toggleReaction = () => {
+	if (!canToggle.value) return;
 
-		const toggleReaction = () => {
-			if (!canToggle.value) return;
-
-			const oldReaction = props.note.myReaction;
-			if (oldReaction) {
-				os.api('notes/reactions/delete', {
-					noteId: props.note.id
-				}).then(() => {
-					if (oldReaction !== props.reaction) {
-						os.api('notes/reactions/create', {
-							noteId: props.note.id,
-							reaction: props.reaction
-						});
-					}
-				});
-			} else {
+	const oldReaction = props.note.myReaction;
+	if (oldReaction) {
+		os.api('notes/reactions/delete', {
+			noteId: props.note.id,
+		}).then(() => {
+			if (oldReaction !== props.reaction) {
 				os.api('notes/reactions/create', {
 					noteId: props.note.id,
-					reaction: props.reaction
+					reaction: props.reaction,
 				});
 			}
-		};
-
-		const anime = () => {
-			if (document.hidden) return;
-
-			// TODO: 新しくリアクションが付いたことが視覚的に分かりやすいアニメーション
-		};
-
-		watch(() => props.count, (newCount, oldCount) => {
-			if (oldCount < newCount) anime();
 		});
-
-		onMounted(() => {
-			if (!props.isInitial) anime();
+	} else {
+		os.api('notes/reactions/create', {
+			noteId: props.note.id,
+			reaction: props.reaction,
 		});
+	}
+};
 
-		useTooltip(buttonRef, async (showing) => {
-			const reactions = await os.api('notes/reactions', {
-				noteId: props.note.id,
-				type: props.reaction,
-				limit: 11
-			});
+const anime = () => {
+	if (document.hidden) return;
 
-			const users = reactions.map(x => x.user);
+	// TODO: 新しくリアクションが付いたことが視覚的に分かりやすいアニメーション
+};
 
-			os.popup(XDetails, {
-				showing,
-				reaction: props.reaction,
-				emojis: props.note.emojis,
-				users,
-				count: props.count,
-				targetElement: buttonRef.value,
-			}, {}, 'closed');
-		});
-
-		return {
-			buttonRef,
-			canToggle,
-			toggleReaction,
-		};
-	},
+watch(() => props.count, (newCount, oldCount) => {
+	if (oldCount < newCount) anime();
 });
+
+onMounted(() => {
+	if (!props.isInitial) anime();
+});
+
+useTooltip(buttonRef, async (showing) => {
+	const reactions = await os.api('notes/reactions', {
+		noteId: props.note.id,
+		type: props.reaction,
+		limit: 11,
+	});
+
+	const users = reactions.map(x => x.user);
+
+	os.popup(XDetails, {
+		showing,
+		reaction: props.reaction,
+		emojis: props.note.emojis,
+		users,
+		count: props.count,
+		targetElement: buttonRef.value,
+	}, {}, 'closed');
+}, 100);
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/scripts/use-tooltip.ts b/packages/client/src/scripts/use-tooltip.ts
index bc8f27a03..1f6e0fb6c 100644
--- a/packages/client/src/scripts/use-tooltip.ts
+++ b/packages/client/src/scripts/use-tooltip.ts
@@ -3,6 +3,7 @@ import { Ref, ref, watch, onUnmounted } from 'vue';
 export function useTooltip(
 	elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>,
 	onShow: (showing: Ref<boolean>) => void,
+	delay = 300,
 ): void {
 	let isHovering = false;
 
@@ -40,7 +41,7 @@ export function useTooltip(
 		if (isHovering) return;
 		if (shouldIgnoreMouseover) return;
 		isHovering = true;
-		timeoutId = window.setTimeout(open, 300);
+		timeoutId = window.setTimeout(open, delay);
 	};
 
 	const onMouseleave = () => {
@@ -54,7 +55,7 @@ export function useTooltip(
 		shouldIgnoreMouseover = true;
 		if (isHovering) return;
 		isHovering = true;
-		timeoutId = window.setTimeout(open, 300);
+		timeoutId = window.setTimeout(open, delay);
 	};
 
 	const onTouchend = () => {

From 63e6c7f72a99501193fc3586953fd56723c73350 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 30 Jun 2022 23:53:58 +0900
Subject: [PATCH 032/100] chore(client): tweak rss-marquee

---
 packages/client/src/widgets/rss-marquee.vue | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/widgets/rss-marquee.vue b/packages/client/src/widgets/rss-marquee.vue
index e23f916d3..e7516476a 100644
--- a/packages/client/src/widgets/rss-marquee.vue
+++ b/packages/client/src/widgets/rss-marquee.vue
@@ -6,7 +6,7 @@
 	<div class="ekmkgxbk">
 		<MkLoading v-if="fetching"/>
 		<div v-else class="feed">
-			<MarqueeText :duration="widgetProps.speed" :reverse="widgetProps.reverse">
+			<MarqueeText :key="key" :duration="widgetProps.speed" :reverse="widgetProps.reverse">
 				<a v-for="item in items" class="item" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
 			</MarqueeText>
 		</div>
@@ -75,12 +75,14 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 
 const items = ref([]);
 const fetching = ref(true);
+let key = $ref(0);
 
 const tick = () => {
 	fetch(`https://api.rss2json.com/v1/api.json?rss_url=${widgetProps.url}`, {}).then(res => {
 		res.json().then(feed => {
 			items.value = feed.items;
 			fetching.value = false;
+			key++;
 		});
 	});
 };

From 66c0059868c7948b35b2ae4a7faf32b4ecf1818b Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 00:21:25 +0900
Subject: [PATCH 033/100] update deps

---
 CHANGELOG.md                          |   3 +-
 package.json                          |   6 +-
 packages/backend/package.json         |  44 +-
 packages/backend/src/mfm/from-html.ts |   8 +-
 packages/backend/yarn.lock            | 588 ++++++++++++++------------
 packages/client/package.json          |  24 +-
 packages/client/yarn.lock             | 175 ++++----
 yarn.lock                             |  72 ++--
 8 files changed, 479 insertions(+), 441 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 405c92bb3..c08133aaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,10 +18,11 @@ You should also include the user name that made the change.
 - Client: Show warning in control panel when there is an unresolved abuse report @syuilo
 - Client: Add instance-cloud widget @syuilo
 - Client: Add rss-marquee widget @syuilo
+- Client: Removing entries from a clip @futchitwo
 - Make possible to delete an account by admin @syuilo
 - Improve player detection in URL preview @mei23
 - Add Badge Image to Push Notification #8012 @tamaina
-- Client: Removing entries from a clip @futchitwo
+- Server: Improve performance
 - Server: Supports IPv6 on Redis transport. @mei23  
   IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`.
 
diff --git a/package.json b/package.json
index 56ffe9ee4..d3fdcc8c5 100644
--- a/package.json
+++ b/package.json
@@ -41,10 +41,10 @@
 	"devDependencies": {
 		"@types/gulp": "4.0.9",
 		"@types/gulp-rename": "2.0.1",
-		"@typescript-eslint/parser": "5.27.1",
+		"@typescript-eslint/parser": "5.30.0",
 		"cross-env": "7.0.3",
-		"cypress": "10.0.3",
+		"cypress": "10.3.0",
 		"start-server-and-test": "1.14.0",
-		"typescript": "4.7.3"
+		"typescript": "4.7.4"
 	}
 }
diff --git a/packages/backend/package.json b/packages/backend/package.json
index c6c76b30a..c5ab587cf 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -14,7 +14,7 @@
 		"lodash": "^4.17.21"
 	},
 	"dependencies": {
-		"@bull-board/koa": "3.11.1",
+		"@bull-board/koa": "4.0.0",
 		"@discordapp/twemoji": "14.0.2",
 		"@elastic/elasticsearch": "7.11.0",
 		"@koa/cors": "3.1.0",
@@ -28,10 +28,10 @@
 		"archiver": "5.3.1",
 		"autobind-decorator": "2.4.0",
 		"autwh": "0.1.0",
-		"aws-sdk": "2.1152.0",
+		"aws-sdk": "2.1165.0",
 		"bcryptjs": "2.4.3",
 		"blurhash": "1.1.5",
-		"bull": "4.8.3",
+		"bull": "4.8.4",
 		"cacheable-lookup": "6.0.4",
 		"cbor": "8.1.0",
 		"chalk": "5.0.1",
@@ -51,7 +51,7 @@
 		"ip-cidr": "3.0.10",
 		"is-svg": "4.3.2",
 		"js-yaml": "4.1.0",
-		"jsdom": "19.0.0",
+		"jsdom": "20.0.0",
 		"json5": "2.2.1",
 		"json5-loader": "4.0.1",
 		"jsonld": "6.0.0",
@@ -73,20 +73,21 @@
 		"multer": "1.4.4",
 		"nested-property": "4.0.0",
 		"node-fetch": "3.2.6",
-		"nodemailer": "6.7.5",
+		"nodemailer": "6.7.6",
 		"os-utils": "0.0.14",
-		"parse5": "6.0.1",
+		"parse5": "7.0.0",
+		"parse5-htmlparser2-tree-adapter": "7.0.0",
 		"pg": "8.7.3",
 		"private-ip": "2.3.3",
 		"probe-image-size": "7.2.3",
 		"promise-limit": "2.7.0",
 		"pug": "3.0.2",
 		"punycode": "2.1.1",
-		"pureimage": "0.3.8",
+		"pureimage": "0.3.14",
 		"qrcode": "1.5.0",
 		"random-seed": "0.3.0",
 		"ratelimiter": "3.4.1",
-		"re2": "1.17.4",
+		"re2": "1.17.7",
 		"redis-lock": "0.1.4",
 		"reflect-metadata": "0.1.13",
 		"rename": "1.0.4",
@@ -102,15 +103,15 @@
 		"style-loader": "3.3.1",
 		"summaly": "2.6.0",
 		"syslog-pro": "1.0.0",
-		"systeminformation": "5.11.16",
+		"systeminformation": "5.11.22",
 		"tinycolor2": "1.4.2",
 		"tmp": "0.2.1",
-		"ts-loader": "9.3.0",
+		"ts-loader": "9.3.1",
 		"ts-node": "10.8.1",
-		"tsc-alias": "1.6.9",
+		"tsc-alias": "1.6.11",
 		"tsconfig-paths": "4.0.0",
 		"twemoji-parser": "14.0.0",
-		"typeorm": "0.3.6",
+		"typeorm": "0.3.7",
 		"ulid": "2.3.0",
 		"unzipper": "0.10.11",
 		"uuid": "8.3.2",
@@ -121,7 +122,6 @@
 	},
 	"devDependencies": {
 		"@redocly/openapi-core": "1.0.0-beta.97",
-		"@types/semver": "7.3.9",
 		"@types/bcryptjs": "2.4.2",
 		"@types/bull": "3.15.8",
 		"@types/cbor": "6.0.0",
@@ -144,11 +144,10 @@
 		"@types/koa__multer": "2.0.4",
 		"@types/koa__router": "8.0.11",
 		"@types/mocha": "9.1.1",
-		"@types/node": "17.0.41",
+		"@types/node": "18.0.0",
 		"@types/node-fetch": "3.0.3",
 		"@types/nodemailer": "6.4.4",
 		"@types/oauth": "0.9.1",
-		"@types/parse5": "6.0.3",
 		"@types/pug": "2.0.6",
 		"@types/punycode": "2.1.0",
 		"@types/qrcode": "1.4.2",
@@ -157,7 +156,8 @@
 		"@types/redis": "4.0.11",
 		"@types/rename": "1.0.4",
 		"@types/sanitize-html": "2.6.2",
-		"@types/sharp": "0.30.2",
+		"@types/semver": "7.3.10",
+		"@types/sharp": "0.30.4",
 		"@types/sinonjs__fake-timers": "8.1.2",
 		"@types/speakeasy": "2.0.7",
 		"@types/tinycolor2": "1.4.3",
@@ -166,12 +166,12 @@
 		"@types/web-push": "3.3.2",
 		"@types/websocket": "1.0.5",
 		"@types/ws": "8.5.3",
-		"@typescript-eslint/eslint-plugin": "5.27.1",
-		"@typescript-eslint/parser": "5.27.1",
-		"typescript": "4.7.3",
-		"eslint": "8.17.0",
-		"eslint-plugin-import": "2.26.0",
+		"@typescript-eslint/eslint-plugin": "5.30.0",
+		"@typescript-eslint/parser": "5.30.0",
 		"cross-env": "7.0.3",
-		"execa": "6.1.0"
+		"eslint": "8.18.0",
+		"eslint-plugin-import": "2.26.0",
+		"execa": "6.1.0",
+		"typescript": "4.7.4"
 	}
 }
diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts
index 15110b6b7..710c18f44 100644
--- a/packages/backend/src/mfm/from-html.ts
+++ b/packages/backend/src/mfm/from-html.ts
@@ -1,8 +1,8 @@
-import * as parse5 from 'parse5';
-import treeAdapter from 'parse5/lib/tree-adapters/default.js';
 import { URL } from 'node:url';
+import * as parse5 from 'parse5';
+import { adapter as treeAdapter } from 'parse5-htmlparser2-tree-adapter';
 
-const urlRegex     = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
+const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
 const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
 
 export function fromHtml(html: string, hashtagNames?: string[]): string {
@@ -170,7 +170,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
 				const t = getText(node);
 				if (t) {
 					text += '\n> ';
-					text += t.split('\n').join(`\n> `);
+					text += t.split('\n').join('\n> ');
 				}
 				break;
 			}
diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock
index 25c4ee9c2..a9c20c3c0 100644
--- a/packages/backend/yarn.lock
+++ b/packages/backend/yarn.lock
@@ -21,20 +21,20 @@
     lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
-"@bull-board/api@3.11.1":
-  version "3.11.1"
-  resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.11.1.tgz#98b2c9556f643718bb5bde4a1306e6706af8192e"
-  integrity sha512-ElwX7sM+Ng4ZL9KUsbDubRE+r2hu/gss85OsROeE9bmyfkW14jOJkgr5MKUyjTTgPEeMs1Mw55TgQs2vxoWBiA==
+"@bull-board/api@4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-4.0.0.tgz#63931cbee56ff3b1525f8771d9b8a6df12838962"
+  integrity sha512-4STXOhQv07/8d/Ei6LA38D3aaYtMuOHJMejkkF2CTAW3gAzEtwhDHmrKlk7tG01Gq2jnPNIcYxbd4WIbtP/+fQ==
   dependencies:
     redis-info "^3.0.8"
 
-"@bull-board/koa@3.11.1":
-  version "3.11.1"
-  resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-3.11.1.tgz#1872aba2c65d116d1183b3003e4a2cb2c1e2fbbf"
-  integrity sha512-F/thrTuC1JWpdBO7DPdKD/wr8c+d7MJGu0sr5ARsT1WXhng7sU7OqBEP/5Y7HhByurjDFXDxcgk/mc78Tmeb/Q==
+"@bull-board/koa@4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-4.0.0.tgz#e9d39f166abdc942c0d9e045bbc23941e9e47db1"
+  integrity sha512-8UN8h0NKkpND2w47YmvacG3TZPp0GHjw2By/fuX/MqoLG2Wtu58GCzhmKij8DHW5nfAr5c/0azWwyKJZ6jR5wA==
   dependencies:
-    "@bull-board/api" "3.11.1"
-    "@bull-board/ui" "3.11.1"
+    "@bull-board/api" "4.0.0"
+    "@bull-board/ui" "4.0.0"
     ejs "^3.1.7"
     koa "^2.13.1"
     koa-mount "^4.0.0"
@@ -42,12 +42,12 @@
     koa-static "^5.0.0"
     koa-views "^7.0.1"
 
-"@bull-board/ui@3.11.1":
-  version "3.11.1"
-  resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-3.11.1.tgz#17a2af5573f31811a543105b9a96249c95e93ce7"
-  integrity sha512-SRrfvxHF/WaBICiAFuWAoAlTvoBYUBmX94oRbSKzVILRFZMe3gs0hN071BFohrn4yOTFHAkWPN7cjMbaqHwCag==
+"@bull-board/ui@4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-4.0.0.tgz#6702d2fa286ba54d3f18a0af2e2344c3fe21d836"
+  integrity sha512-sesp3n3e/Zkw7oFxrihB/AGsPWRzLywTXlcc3N6ttGLE1U5ow5yRSg6F/1LFe9OpHsYko0VsYJMcTAeZk7AJ+w==
   dependencies:
-    "@bull-board/api" "3.11.1"
+    "@bull-board/api" "4.0.0"
 
 "@cspotcode/source-map-support@^0.8.0":
   version "0.8.1"
@@ -106,10 +106,10 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@gar/promisify@^1.0.1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
-  integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==
+"@gar/promisify@^1.1.3":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
+  integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
 
 "@humanwhocodes/config-array@^0.9.2":
   version "0.9.2"
@@ -217,18 +217,18 @@
     "@nodelib/fs.scandir" "2.1.3"
     fastq "^1.6.0"
 
-"@npmcli/fs@^1.0.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.0.tgz#bec1d1b89c170d40e1b73ad6c943b0b75e7d2951"
-  integrity sha512-VhP1qZLXcrXRIaPoqb4YA55JQxLNF3jNR4T55IdOJa3+IFJKNYHtPvtXx8slmeMavj37vCzCfrqQM1vWLsYKLA==
+"@npmcli/fs@^2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.0.tgz#f2a21c28386e299d1a9fae8051d35ad180e33109"
+  integrity sha512-DmfBvNXGaetMxj9LTp8NAN9vEidXURrf5ZTslQzEAi/6GbW+4yjaLFQc6Tue5cpZ9Frlk4OBo/Snf1Bh/S7qTQ==
   dependencies:
-    "@gar/promisify" "^1.0.1"
+    "@gar/promisify" "^1.1.3"
     semver "^7.3.5"
 
-"@npmcli/move-file@^1.0.1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
-  integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
+"@npmcli/move-file@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.0.tgz#417f585016081a0184cef3e38902cd917a9bbd02"
+  integrity sha512-UR6D5f4KEGWJV6BGPH3Qb2EtgH+t+1XQ1Tt85c7qicN6cezzuHPdZwwAxqZr4JLtnQu0LZsTza/5gmNmSl8XLg==
   dependencies:
     mkdirp "^1.0.4"
     rimraf "^3.0.2"
@@ -327,11 +327,6 @@
   resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
   integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
 
-"@tootallnate/once@1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
-  integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
-
 "@tootallnate/once@2":
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -679,10 +674,10 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
   integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
 
-"@types/node@17.0.41":
-  version "17.0.41"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b"
-  integrity sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==
+"@types/node@18.0.0":
+  version "18.0.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a"
+  integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==
 
 "@types/node@^14.11.8":
   version "14.17.9"
@@ -708,11 +703,6 @@
   resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
   integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
 
-"@types/parse5@6.0.3":
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
-  integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
-
 "@types/pug@2.0.6":
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6"
@@ -785,10 +775,10 @@
   dependencies:
     htmlparser2 "^6.0.0"
 
-"@types/semver@7.3.9":
-  version "7.3.9"
-  resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc"
-  integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==
+"@types/semver@7.3.10":
+  version "7.3.10"
+  resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.10.tgz#5f19ee40cbeff87d916eedc8c2bfe2305d957f73"
+  integrity sha512-zsv3fsC7S84NN6nPK06u79oWgrPVd0NvOyqgghV1haPaFcVxIrP4DLomRwGAXk0ui4HZA7mOcSFL98sMVW9viw==
 
 "@types/serve-static@*":
   version "1.13.3"
@@ -798,10 +788,10 @@
     "@types/express-serve-static-core" "*"
     "@types/mime" "*"
 
-"@types/sharp@0.30.2":
-  version "0.30.2"
-  resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.2.tgz#df5ff34140b3bad165482e6f3d26b08e42a0503a"
-  integrity sha512-uLCBwjDg/BTcQit0dpNGvkIjvH3wsb8zpaJePCjvONBBSfaKHoxXBIuq1MT8DMQEfk2fKYnpC9QExCgFhkGkMQ==
+"@types/sharp@0.30.4":
+  version "0.30.4"
+  resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.4.tgz#7430b5fcf37f35dd860112c4cf6dcd6a1ba0011b"
+  integrity sha512-6oJEzKt7wZeS7e+6x9QFEOWGs0T/6of00+0onZGN1zSmcSjcTDZKgIGZ6YWJnHowpaKUCFBPH52mYljWqU32Eg==
   dependencies:
     "@types/node" "*"
 
@@ -858,14 +848,14 @@
   dependencies:
     "@types/node" "*"
 
-"@typescript-eslint/eslint-plugin@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.1.tgz#fdf59c905354139046b41b3ed95d1609913d0758"
-  integrity sha512-6dM5NKT57ZduNnJfpY81Phe9nc9wolnMCnknb1im6brWi1RYv84nbMS3olJa27B6+irUVV1X/Wb+Am0FjJdGFw==
+"@typescript-eslint/eslint-plugin@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.0.tgz#524a11e15c09701733033c96943ecf33f55d9ca1"
+  integrity sha512-lvhRJ2pGe2V9MEU46ELTdiHgiAFZPKtLhiU5wlnaYpMc2+c1R8fh8i80ZAa665drvjHKUJyRRGg3gEm1If54ow==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.27.1"
-    "@typescript-eslint/type-utils" "5.27.1"
-    "@typescript-eslint/utils" "5.27.1"
+    "@typescript-eslint/scope-manager" "5.30.0"
+    "@typescript-eslint/type-utils" "5.30.0"
+    "@typescript-eslint/utils" "5.30.0"
     debug "^4.3.4"
     functional-red-black-tree "^1.0.1"
     ignore "^5.2.0"
@@ -873,69 +863,69 @@
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/parser@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.27.1.tgz#3a4dcaa67e45e0427b6ca7bb7165122c8b569639"
-  integrity sha512-7Va2ZOkHi5NP+AZwb5ReLgNF6nWLGTeUJfxdkVUAPPSaAdbWNnFZzLZ4EGGmmiCTg+AwlbE1KyUYTBglosSLHQ==
+"@typescript-eslint/parser@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.30.0.tgz#a2184fb5f8ef2bf1db0ae61a43907e2e32aa1b8f"
+  integrity sha512-2oYYUws5o2liX6SrFQ5RB88+PuRymaM2EU02/9Ppoyu70vllPnHVO7ioxDdq/ypXHA277R04SVjxvwI8HmZpzA==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.27.1"
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/typescript-estree" "5.27.1"
+    "@typescript-eslint/scope-manager" "5.30.0"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/typescript-estree" "5.30.0"
     debug "^4.3.4"
 
-"@typescript-eslint/scope-manager@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.1.tgz#4d1504392d01fe5f76f4a5825991ec78b7b7894d"
-  integrity sha512-fQEOSa/QroWE6fAEg+bJxtRZJTH8NTskggybogHt4H9Da8zd4cJji76gA5SBlR0MgtwF7rebxTbDKB49YUCpAg==
+"@typescript-eslint/scope-manager@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.30.0.tgz#bf585ee801ab4ad84db2f840174e171a6bb002c7"
+  integrity sha512-3TZxvlQcK5fhTBw5solQucWSJvonXf5yua5nx8OqK94hxdrT7/6W3/CS42MLd/f1BmlmmbGEgQcTHHCktUX5bQ==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/visitor-keys" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/visitor-keys" "5.30.0"
 
-"@typescript-eslint/type-utils@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.27.1.tgz#369f695199f74c1876e395ebea202582eb1d4166"
-  integrity sha512-+UC1vVUWaDHRnC2cQrCJ4QtVjpjjCgjNFpg8b03nERmkHv9JV9X5M19D7UFMd+/G7T/sgFwX2pGmWK38rqyvXw==
+"@typescript-eslint/type-utils@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.30.0.tgz#98f3af926a5099153f092d4dad87148df21fbaae"
+  integrity sha512-GF8JZbZqSS+azehzlv/lmQQ3EU3VfWYzCczdZjJRxSEeXDQkqFhCBgFhallLDbPwQOEQ4MHpiPfkjKk7zlmeNg==
   dependencies:
-    "@typescript-eslint/utils" "5.27.1"
+    "@typescript-eslint/utils" "5.30.0"
     debug "^4.3.4"
     tsutils "^3.21.0"
 
-"@typescript-eslint/types@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.1.tgz#34e3e629501349d38be6ae97841298c03a6ffbf1"
-  integrity sha512-LgogNVkBhCTZU/m8XgEYIWICD6m4dmEDbKXESCbqOXfKZxRKeqpiJXQIErv66sdopRKZPo5l32ymNqibYEH/xg==
+"@typescript-eslint/types@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.30.0.tgz#db7d81d585a3da3801432a9c1d2fafbff125e110"
+  integrity sha512-vfqcBrsRNWw/LBXyncMF/KrUTYYzzygCSsVqlZ1qGu1QtGs6vMkt3US0VNSQ05grXi5Yadp3qv5XZdYLjpp8ag==
 
-"@typescript-eslint/typescript-estree@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.1.tgz#7621ee78607331821c16fffc21fc7a452d7bc808"
-  integrity sha512-DnZvvq3TAJ5ke+hk0LklvxwYsnXpRdqUY5gaVS0D4raKtbznPz71UJGnPTHEFo0GDxqLOLdMkkmVZjSpET1hFw==
+"@typescript-eslint/typescript-estree@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.0.tgz#4565ee8a6d2ac368996e20b2344ea0eab1a8f0bb"
+  integrity sha512-hDEawogreZB4n1zoqcrrtg/wPyyiCxmhPLpZ6kmWfKF5M5G0clRLaEexpuWr31fZ42F96SlD/5xCt1bT5Qm4Nw==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/visitor-keys" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/visitor-keys" "5.30.0"
     debug "^4.3.4"
     globby "^11.1.0"
     is-glob "^4.0.3"
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/utils@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.27.1.tgz#b4678b68a94bc3b85bf08f243812a6868ac5128f"
-  integrity sha512-mZ9WEn1ZLDaVrhRaYgzbkXBkTPghPFsup8zDbbsYTxC5OmqrFE7skkKS/sraVsLP3TcT3Ki5CSyEFBRkLH/H/w==
+"@typescript-eslint/utils@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.30.0.tgz#1dac771fead5eab40d31860716de219356f5f754"
+  integrity sha512-0bIgOgZflLKIcZsWvfklsaQTM3ZUbmtH0rJ1hKyV3raoUYyeZwcjQ8ZUJTzS7KnhNcsVT1Rxs7zeeMHEhGlltw==
   dependencies:
     "@types/json-schema" "^7.0.9"
-    "@typescript-eslint/scope-manager" "5.27.1"
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/typescript-estree" "5.27.1"
+    "@typescript-eslint/scope-manager" "5.30.0"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/typescript-estree" "5.30.0"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
-"@typescript-eslint/visitor-keys@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.1.tgz#05a62666f2a89769dac2e6baa48f74e8472983af"
-  integrity sha512-xYs6ffo01nhdJgPieyk7HAOpjhTsx7r/oB9LWEhwAXgwn33tkr+W8DI2ChboqhZlC4q3TC6geDYPoiX8ROqyOQ==
+"@typescript-eslint/visitor-keys@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.0.tgz#07721d23daca2ec4c2da7f1e660d41cd78bacac3"
+  integrity sha512-6WcIeRk2DQ3pHKxU1Ni0qMXJkjO/zLjBymlYBy/53qxe7yjEFSvzKLDToJjURUhSl2Fzhkl4SMXQoETauF74cw==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
     eslint-visitor-keys "^3.3.0"
 
 "@ungap/promise-all-settled@1.1.2":
@@ -943,10 +933,10 @@
   resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
   integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
 
-abab@^2.0.3, abab@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
-  integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
+abab@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
+  integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
 
 abbrev@1:
   version "1.1.1"
@@ -1001,11 +991,6 @@ acorn@^8.4.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
   integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
 
-acorn@^8.5.0:
-  version "8.7.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
-  integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
-
 acorn@^8.7.1:
   version "8.7.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
@@ -1018,10 +1003,10 @@ agent-base@6, agent-base@^6.0.2:
   dependencies:
     debug "4"
 
-agentkeepalive@^4.1.3:
-  version "4.1.4"
-  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b"
-  integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ==
+agentkeepalive@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717"
+  integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==
   dependencies:
     debug "^4.1.0"
     depd "^1.1.2"
@@ -1274,10 +1259,10 @@ autwh@0.1.0:
   dependencies:
     oauth "0.9.15"
 
-aws-sdk@2.1152.0:
-  version "2.1152.0"
-  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1152.0.tgz#73e4fb81b3a9c289234b5d6848bcdb854f169bdf"
-  integrity sha512-Lqwk0bDhm3vzpYb3AAM9VgGHeDpbB8+o7UJnP9R+CO23kJfi/XRpKihAcbyKDD/AUQ+O1LJaUVpvaJYLS9Am7w==
+aws-sdk@2.1165.0:
+  version "2.1165.0"
+  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1165.0.tgz#4da669d1e9020344cef75d961882f52a7931a379"
+  integrity sha512-2oVkSuXsLeErt+H4M2OGIz4p1LPS+QRfY2WnW4QKMndASOcvHKZTfzuY8jmc9ZnDGyguiGdT3idYU8KpNg0sGw==
   dependencies:
     buffer "4.9.2"
     events "1.1.1"
@@ -1486,10 +1471,10 @@ bufferutil@^4.0.1:
   dependencies:
     node-gyp-build "~3.7.0"
 
-bull@4.8.3:
-  version "4.8.3"
-  resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.3.tgz#4ab67029fee1183dcb7185895b20dc08c02d6bf2"
-  integrity sha512-oOHr+KTLu3JM5V9TXsg18/1xyVQceoYCFiGrXZOpu9abZn3W3vXJtMBrwB6Yvl/RxSKVVBpoa25RF/ya3750qg==
+bull@4.8.4:
+  version "4.8.4"
+  resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.4.tgz#c538610492050d5160dbd9180704145f135a0aa9"
+  integrity sha512-vDNhM/pvfFY3+msulMbqPBdBO7ntKxRZRtMfi3EguVW/Ozo4uez+B81I8ZoDxYCLgSOBfwRuPnFtcv7QNzm4Ew==
   dependencies:
     cron-parser "^4.2.1"
     debuglog "^1.0.0"
@@ -1514,28 +1499,28 @@ bytes@3.1.0, bytes@^3.1.0:
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
-cacache@^15.2.0:
-  version "15.3.0"
-  resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb"
-  integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==
+cacache@^16.1.0:
+  version "16.1.1"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.1.tgz#4e79fb91d3efffe0630d5ad32db55cc1b870669c"
+  integrity sha512-VDKN+LHyCQXaaYZ7rA/qtkURU+/yYhviUdvqEv2LT6QPZU8jpyzEkEVAcKlKLt5dJ5BRp11ym8lo3NKLluEPLg==
   dependencies:
-    "@npmcli/fs" "^1.0.0"
-    "@npmcli/move-file" "^1.0.1"
+    "@npmcli/fs" "^2.1.0"
+    "@npmcli/move-file" "^2.0.0"
     chownr "^2.0.0"
-    fs-minipass "^2.0.0"
-    glob "^7.1.4"
+    fs-minipass "^2.1.0"
+    glob "^8.0.1"
     infer-owner "^1.0.4"
-    lru-cache "^6.0.0"
-    minipass "^3.1.1"
+    lru-cache "^7.7.1"
+    minipass "^3.1.6"
     minipass-collect "^1.0.2"
     minipass-flush "^1.0.5"
-    minipass-pipeline "^1.2.2"
-    mkdirp "^1.0.3"
+    minipass-pipeline "^1.2.4"
+    mkdirp "^1.0.4"
     p-map "^4.0.0"
     promise-inflight "^1.0.1"
     rimraf "^3.0.2"
-    ssri "^8.0.1"
-    tar "^6.0.2"
+    ssri "^9.0.0"
+    tar "^6.1.11"
     unique-filename "^1.1.1"
 
 cache-content-type@^1.0.0:
@@ -2075,14 +2060,14 @@ data-uri-to-buffer@^4.0.0:
   resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
   integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
 
-data-urls@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.1.tgz#597fc2ae30f8bc4dbcf731fcd1b1954353afc6f8"
-  integrity sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw==
+data-urls@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
+  integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==
   dependencies:
-    abab "^2.0.3"
+    abab "^2.0.6"
     whatwg-mimetype "^3.0.0"
-    whatwg-url "^10.0.0"
+    whatwg-url "^11.0.0"
 
 date-fns@2.28.0, date-fns@^2.28.0:
   version "2.28.0"
@@ -2346,6 +2331,11 @@ domelementtype@^2.2.0:
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
   integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
 
+domelementtype@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
 domexception@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
@@ -2374,6 +2364,13 @@ domhandler@^4.2.0:
   dependencies:
     domelementtype "^2.2.0"
 
+domhandler@^5.0.2:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
+  integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
+  dependencies:
+    domelementtype "^2.3.0"
+
 domutils@1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
@@ -2468,7 +2465,7 @@ encodeurl@^1.0.2:
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
 
-encoding@^0.1.12:
+encoding@^0.1.13:
   version "0.1.13"
   resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
   integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
@@ -2500,6 +2497,11 @@ entities@^2.0.0:
   resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
   integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
 
+entities@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.0.tgz#62915f08d67353bb4eb67e3d62641a4059aec656"
+  integrity sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg==
+
 env-paths@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
@@ -2676,10 +2678,10 @@ eslint-visitor-keys@^3.3.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
   integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
 
-eslint@8.17.0:
-  version "8.17.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.17.0.tgz#1cfc4b6b6912f77d24b874ca1506b0fe09328c21"
-  integrity sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==
+eslint@8.18.0:
+  version "8.18.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.18.0.tgz#78d565d16c993d0b73968c523c0446b13da784fd"
+  integrity sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==
   dependencies:
     "@eslint/eslintrc" "^1.3.0"
     "@humanwhocodes/config-array" "^0.9.2"
@@ -3023,7 +3025,7 @@ fs-extra@^8.0.1:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
-fs-minipass@^2.0.0:
+fs-minipass@^2.0.0, fs-minipass@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
   integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
@@ -3190,6 +3192,17 @@ glob@^7.1.3, glob@^7.1.4:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+glob@^8.0.1:
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e"
+  integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^5.0.1"
+    once "^1.3.0"
+
 globals@^13.15.0:
   version "13.15.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac"
@@ -3404,15 +3417,6 @@ http-errors@~1.6.2:
     setprototypeof "1.1.0"
     statuses ">= 1.4.0 < 2"
 
-http-proxy-agent@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
-  integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==
-  dependencies:
-    "@tootallnate/once" "1"
-    agent-base "6"
-    debug "4"
-
 http-proxy-agent@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
@@ -3453,6 +3457,14 @@ https-proxy-agent@^5.0.0:
     agent-base "6"
     debug "4"
 
+https-proxy-agent@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
+  integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
+  dependencies:
+    agent-base "6"
+    debug "4"
+
 human-signals@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5"
@@ -3562,10 +3574,10 @@ ini@^1.3.4, ini@~1.3.0:
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
   integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
 
-install-artifact-from-github@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.0.tgz#cab6ff821976b8a35b0c079da19a727c90381a40"
-  integrity sha512-iT8v1GwOAX0pPXifF/5ihnMhHOCo3OeK7z3TQa4CtSNCIg8k0UxqBEk9jRwz8OP68hHXvJ2gxRa89KYHtBkqGA==
+install-artifact-from-github@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz#eefaad9af35d632e5d912ad1569c1de38c3c2462"
+  integrity sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==
 
 internal-slot@^1.0.3:
   version "1.0.3"
@@ -3934,28 +3946,28 @@ jschardet@3.0.0:
   resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882"
   integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==
 
-jsdom@19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a"
-  integrity sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==
+jsdom@20.0.0:
+  version "20.0.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.0.tgz#882825ac9cc5e5bbee704ba16143e1fa78361ebf"
+  integrity sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==
   dependencies:
-    abab "^2.0.5"
-    acorn "^8.5.0"
+    abab "^2.0.6"
+    acorn "^8.7.1"
     acorn-globals "^6.0.0"
     cssom "^0.5.0"
     cssstyle "^2.3.0"
-    data-urls "^3.0.1"
+    data-urls "^3.0.2"
     decimal.js "^10.3.1"
     domexception "^4.0.0"
     escodegen "^2.0.0"
     form-data "^4.0.0"
     html-encoding-sniffer "^3.0.0"
     http-proxy-agent "^5.0.0"
-    https-proxy-agent "^5.0.0"
+    https-proxy-agent "^5.0.1"
     is-potential-custom-element-name "^1.0.1"
     nwsapi "^2.2.0"
-    parse5 "6.0.1"
-    saxes "^5.0.1"
+    parse5 "^7.0.0"
+    saxes "^6.0.0"
     symbol-tree "^3.2.4"
     tough-cookie "^4.0.0"
     w3c-hr-time "^1.0.2"
@@ -3963,8 +3975,8 @@ jsdom@19.0.0:
     webidl-conversions "^7.0.0"
     whatwg-encoding "^2.0.0"
     whatwg-mimetype "^3.0.0"
-    whatwg-url "^10.0.0"
-    ws "^8.2.3"
+    whatwg-url "^11.0.0"
+    ws "^8.8.0"
     xml-name-validator "^4.0.0"
 
 json-buffer@3.0.1:
@@ -4457,6 +4469,11 @@ lru-cache@^6.0.0:
   dependencies:
     yallist "^4.0.0"
 
+lru-cache@^7.7.1:
+  version "7.12.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.12.0.tgz#be2649a992c8a9116efda5c487538dcf715f3476"
+  integrity sha512-OIP3DwzRZDfLg9B9VP/huWBlpvbkmbfiBy8xmsXp4RPmE4A3MhwNozc5ZJ3fWnSg8fDcdlE/neRTPG2ycEKliw==
+
 luxon@^1.28.0:
   version "1.28.0"
   resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf"
@@ -4472,27 +4489,27 @@ make-error@^1.1.1:
   resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
   integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
 
-make-fetch-happen@^9.1.0:
-  version "9.1.0"
-  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968"
-  integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==
+make-fetch-happen@^10.0.3:
+  version "10.1.8"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.1.8.tgz#3b6e93dd8d8fdb76c0d7bf32e617f37c3108435a"
+  integrity sha512-0ASJbG12Au6+N5I84W+8FhGS6iM8MyzvZady+zaQAu+6IOaESFzCLLD0AR1sAFF3Jufi8bxm586ABN6hWd3k7g==
   dependencies:
-    agentkeepalive "^4.1.3"
-    cacache "^15.2.0"
+    agentkeepalive "^4.2.1"
+    cacache "^16.1.0"
     http-cache-semantics "^4.1.0"
-    http-proxy-agent "^4.0.1"
+    http-proxy-agent "^5.0.0"
     https-proxy-agent "^5.0.0"
     is-lambda "^1.0.1"
-    lru-cache "^6.0.0"
-    minipass "^3.1.3"
+    lru-cache "^7.7.1"
+    minipass "^3.1.6"
     minipass-collect "^1.0.2"
-    minipass-fetch "^1.3.2"
+    minipass-fetch "^2.0.3"
     minipass-flush "^1.0.5"
     minipass-pipeline "^1.2.4"
-    negotiator "^0.6.2"
+    negotiator "^0.6.3"
     promise-retry "^2.0.1"
-    socks-proxy-agent "^6.0.0"
-    ssri "^8.0.0"
+    socks-proxy-agent "^7.0.0"
+    ssri "^9.0.0"
 
 media-typer@0.3.0:
   version "0.3.0"
@@ -4607,16 +4624,16 @@ minipass-collect@^1.0.2:
   dependencies:
     minipass "^3.0.0"
 
-minipass-fetch@^1.3.2:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a"
-  integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==
+minipass-fetch@^2.0.3:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.0.tgz#ca1754a5f857a3be99a9271277246ac0b44c3ff8"
+  integrity sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==
   dependencies:
-    minipass "^3.1.0"
+    minipass "^3.1.6"
     minipass-sized "^1.0.3"
-    minizlib "^2.0.0"
+    minizlib "^2.1.2"
   optionalDependencies:
-    encoding "^0.1.12"
+    encoding "^0.1.13"
 
 minipass-flush@^1.0.5:
   version "1.0.5"
@@ -4625,7 +4642,7 @@ minipass-flush@^1.0.5:
   dependencies:
     minipass "^3.0.0"
 
-minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4:
+minipass-pipeline@^1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
   integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
@@ -4639,14 +4656,21 @@ minipass-sized@^1.0.3:
   dependencies:
     minipass "^3.0.0"
 
-minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3:
+minipass@^3.0.0, minipass@^3.1.1:
   version "3.1.6"
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee"
   integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==
   dependencies:
     yallist "^4.0.0"
 
-minizlib@^2.0.0, minizlib@^2.1.1:
+minipass@^3.1.6:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae"
+  integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==
+  dependencies:
+    yallist "^4.0.0"
+
+minizlib@^2.1.1, minizlib@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
   integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
@@ -4781,11 +4805,16 @@ mz@^2.4.0, mz@^2.7.0:
     object-assign "^4.0.1"
     thenify-all "^1.0.0"
 
-nan@^2.14.2, nan@^2.15.0:
+nan@^2.14.2:
   version "2.15.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
   integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
 
+nan@^2.16.0:
+  version "2.16.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
+  integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
+
 nanoid@3.3.3:
   version "3.3.3"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
@@ -4815,11 +4844,16 @@ needle@^2.5.2:
     iconv-lite "^0.4.4"
     sax "^1.2.4"
 
-negotiator@0.6.2, negotiator@^0.6.2:
+negotiator@0.6.2:
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
   integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
 
+negotiator@^0.6.3:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
+  integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
 nested-property@4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-4.0.0.tgz#a67b5a31991e701e03cdbaa6453bc5b1011bb88d"
@@ -4887,15 +4921,15 @@ node-gyp-build@~3.7.0:
   resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
   integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
 
-node-gyp@^8.4.1:
-  version "8.4.1"
-  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
-  integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==
+node-gyp@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.0.0.tgz#e1da2067427f3eb5bb56820cb62bc6b1e4bd2089"
+  integrity sha512-Ma6p4s+XCTPxCuAMrOA/IJRmVy16R8Sdhtwl4PrCr7IBlj4cPawF0vg/l7nOT1jPbuNS7lIRJpBSvVsXwEZuzw==
   dependencies:
     env-paths "^2.2.0"
     glob "^7.1.4"
     graceful-fs "^4.2.6"
-    make-fetch-happen "^9.1.0"
+    make-fetch-happen "^10.0.3"
     nopt "^5.0.0"
     npmlog "^6.0.0"
     rimraf "^3.0.2"
@@ -4903,10 +4937,10 @@ node-gyp@^8.4.1:
     tar "^6.1.2"
     which "^2.0.2"
 
-nodemailer@6.7.5:
-  version "6.7.5"
-  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.5.tgz#b30b1566f5fa2249f7bd49ced4c58bec6b25915e"
-  integrity sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg==
+nodemailer@6.7.6:
+  version "6.7.6"
+  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.6.tgz#d3de8f644eaa0dad784d1be1375c596de492f3fc"
+  integrity sha512-/6KF/umU7r7X21Y648/yiRLrgkfz0dmpyuo4BfgYWIpnT/jCbkPTvegMfxCsDAu+O810p2L1BGXieMTPp3nJVA==
 
 nofilter@^2.0.3:
   version "2.0.3"
@@ -5214,6 +5248,14 @@ parse-srcset@^1.0.2:
   resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
   integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=
 
+parse5-htmlparser2-tree-adapter@7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
+  integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==
+  dependencies:
+    domhandler "^5.0.2"
+    parse5 "^7.0.0"
+
 parse5-htmlparser2-tree-adapter@^6.0.0:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
@@ -5221,16 +5263,23 @@ parse5-htmlparser2-tree-adapter@^6.0.0:
   dependencies:
     parse5 "^6.0.1"
 
-parse5@6.0.1, parse5@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
-  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+parse5@7.0.0, parse5@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a"
+  integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==
+  dependencies:
+    entities "^4.3.0"
 
 parse5@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
   integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
 
+parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+
 parseurl@^1.3.2:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -5638,10 +5687,10 @@ punycode@2.1.1, punycode@^2.1.0, punycode@^2.1.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-pureimage@0.3.8:
-  version "0.3.8"
-  resolved "https://registry.yarnpkg.com/pureimage/-/pureimage-0.3.8.tgz#b9c2a127f3182ab94fb4520e83f4fbcbdd9b38f1"
-  integrity sha512-+CuR0HM0VmBfKKQTM56myBonDZAhZkS6ymJ8W5oYYDXG7y7X34B/dEH3UesbJI497Vc2OkA+g8T1/Xj/FTyQ8A==
+pureimage@0.3.14:
+  version "0.3.14"
+  resolved "https://registry.yarnpkg.com/pureimage/-/pureimage-0.3.14.tgz#e5fde69c7999d5114667926bda620ba462f72823"
+  integrity sha512-MoXNFWnJaaxMCqfB97Gyw73rI4MEY075VW/WJ+Z+F/ZgQP7HH8kdcIf8Meif15sdCXhTFlMTSHQxSIrSWkQILw==
   dependencies:
     jpeg-js "^0.4.1"
     opentype.js "^0.4.3"
@@ -5733,14 +5782,14 @@ rdf-canonize@^3.0.0:
   dependencies:
     setimmediate "^1.0.5"
 
-re2@1.17.4:
-  version "1.17.4"
-  resolved "https://registry.yarnpkg.com/re2/-/re2-1.17.4.tgz#7bf29290bdde963014e77bd2c2e799a6d788386e"
-  integrity sha512-xyZ4h5PqE8I9tAxTh3G0UttcK5ufrcUxReFjGzfX61vtanNbS1XZHjnwRSyPcLgChI4KLxVgOT/ioZXnUAdoTA==
+re2@1.17.7:
+  version "1.17.7"
+  resolved "https://registry.yarnpkg.com/re2/-/re2-1.17.7.tgz#e14cab85a177a5534c7215c322d1b043c55aa1e9"
+  integrity sha512-X8GSuiBoVWwcjuppqSjsIkRxNUKDdjhkO9SBekQbZ2ksqWUReCy7DQPWOVpoTnpdtdz5PIpTTxTFzvJv5UMfjA==
   dependencies:
-    install-artifact-from-github "^1.3.0"
-    nan "^2.15.0"
-    node-gyp "^8.4.1"
+    install-artifact-from-github "^1.3.1"
+    nan "^2.16.0"
+    node-gyp "^9.0.0"
 
 readable-stream@1.1.x:
   version "1.1.14"
@@ -6008,10 +6057,10 @@ sax@>=0.6.0, sax@^1.2.4:
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
-saxes@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
-  integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
+saxes@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
+  integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
   dependencies:
     xmlchars "^2.2.0"
 
@@ -6176,27 +6225,27 @@ slash@^3.0.0:
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
-smart-buffer@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba"
-  integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==
+smart-buffer@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
+  integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
 
-socks-proxy-agent@^6.0.0:
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87"
-  integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==
+socks-proxy-agent@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6"
+  integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==
   dependencies:
     agent-base "^6.0.2"
-    debug "^4.3.1"
-    socks "^2.6.1"
+    debug "^4.3.3"
+    socks "^2.6.2"
 
-socks@^2.6.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e"
-  integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==
+socks@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.2.tgz#ec042d7960073d40d94268ff3bb727dc685f111a"
+  integrity sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==
   dependencies:
     ip "^1.1.5"
-    smart-buffer "^4.1.0"
+    smart-buffer "^4.2.0"
 
 source-map-js@^0.6.2:
   version "0.6.2"
@@ -6242,10 +6291,10 @@ sshpk@^1.14.1:
     safer-buffer "^2.0.2"
     tweetnacl "~0.14.0"
 
-ssri@^8.0.0, ssri@^8.0.1:
-  version "8.0.1"
-  resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
-  integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
+ssri@^9.0.0:
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057"
+  integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==
   dependencies:
     minipass "^3.1.1"
 
@@ -6469,10 +6518,10 @@ syslog-pro@1.0.0:
   dependencies:
     moment "^2.22.2"
 
-systeminformation@5.11.16:
-  version "5.11.16"
-  resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.16.tgz#5f6fda2447fafe204bd2ab543475f1ffa8c14a85"
-  integrity sha512-/a1VfP9WELKLT330yhAHJ4lWCXRYynel1kMMHKc/qdzCgDt3BIcMlo+3tKcTiRHFefjV3fz4AvqMx7dGO/72zw==
+systeminformation@5.11.22:
+  version "5.11.22"
+  resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.22.tgz#52eb78fd6bb48eef372f502b494ff59aacf82c02"
+  integrity sha512-sBZJ/WBCf2vDLeMZaEyVuo+aXylOSmNHHB2cX0jHULFxSBLXHX+QUHYrCvmz+YiflKY3bsahVWX7vwuz1p1QZA==
 
 tapable@^2.2.0:
   version "2.2.0"
@@ -6521,7 +6570,7 @@ tar-stream@^2.1.4, tar-stream@^2.2.0:
     inherits "^2.0.3"
     readable-stream "^3.1.1"
 
-tar@^6.0.2, tar@^6.1.2:
+tar@^6.1.11, tar@^6.1.2:
   version "6.1.11"
   resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
   integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
@@ -6630,10 +6679,10 @@ trace-redirect@1.0.6:
   resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
   integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=
 
-ts-loader@9.3.0:
-  version "9.3.0"
-  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.0.tgz#980f4dbfb60e517179e15e10ed98e454b132159f"
-  integrity sha512-2kLLAdAD+FCKijvGKi9sS0OzoqxLCF3CxHpok7rVgCZ5UldRzH0TkbwG9XECKjBzHsAewntC5oDaI/FwKzEUog==
+ts-loader@9.3.1:
+  version "9.3.1"
+  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.1.tgz#fe25cca56e3e71c1087fe48dc67f4df8c59b22d4"
+  integrity sha512-OkyShkcZTsTwyS3Kt7a4rsT/t2qvEVQuKCTg4LJmpj9fhFR7ukGdZwV6Qq3tRUkqcXtfGpPR7+hFKHCG/0d3Lw==
   dependencies:
     chalk "^4.1.0"
     enhanced-resolve "^5.0.0"
@@ -6659,10 +6708,10 @@ ts-node@10.8.1:
     v8-compile-cache-lib "^3.0.1"
     yn "3.1.1"
 
-tsc-alias@1.6.9:
-  version "1.6.9"
-  resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.6.9.tgz#d04d95124b95ad8eea55e52d45cf65a744c26baa"
-  integrity sha512-5lv5uAHn0cgxY1XfpXIdquUSz2xXq3ryQyNtxC6DYH7YT5rt/W+9Gsft2uyLFTh+ozk4qU8iCSP3VemjT69xlQ==
+tsc-alias@1.6.11:
+  version "1.6.11"
+  resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.6.11.tgz#d6d83f030ad11f48e4ead8ec5729929e5e60519b"
+  integrity sha512-mXEM21WriTJMQyo07B4Kc2nNFFk/1qOjU+jZ0ymXOyLz/A8J+dIBkimqZrh3s/x1qLGoJ1cNZQxa8GGoWOGX1Q==
   dependencies:
     chokidar "^3.5.3"
     commander "^9.0.0"
@@ -6783,10 +6832,10 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typeorm@0.3.6:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.6.tgz#65203443a1b684bb746785913fe2b0877aa991c0"
-  integrity sha512-DRqgfqcelMiGgWSMbBmVoJNFN2nPNA3EeY2gC324ndr2DZoGRTb9ILtp2oGVGnlA+cu5zgQ6it5oqKFNkte7Aw==
+typeorm@0.3.7:
+  version "0.3.7"
+  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.7.tgz#5776ed5058f0acb75d64723b39ff458d21de64c1"
+  integrity sha512-MsPJeP6Zuwfe64c++l80+VRqpGEGxf0CkztIEnehQ+CMmQPSHjOnFbFxwBuZ2jiLqZTjLk2ZqQdVF0RmvxNF3Q==
   dependencies:
     "@sqltools/formatter" "^1.2.2"
     app-root-path "^3.0.0"
@@ -6806,10 +6855,10 @@ typeorm@0.3.6:
     xml2js "^0.4.23"
     yargs "^17.3.1"
 
-typescript@4.7.3:
-  version "4.7.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
-  integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==
+typescript@4.7.4:
+  version "4.7.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
 
 ulid@2.3.0:
   version "2.3.0"
@@ -7012,10 +7061,10 @@ whatwg-mimetype@^3.0.0:
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
   integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
 
-whatwg-url@^10.0.0:
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-10.0.0.tgz#37264f720b575b4a311bd4094ed8c760caaa05da"
-  integrity sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==
+whatwg-url@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
+  integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
   dependencies:
     tr46 "^3.0.0"
     webidl-conversions "^7.0.0"
@@ -7115,16 +7164,11 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-ws@8.8.0:
+ws@8.8.0, ws@^8.8.0:
   version "8.8.0"
   resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769"
   integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==
 
-ws@^8.2.3:
-  version "8.4.2"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b"
-  integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==
-
 xev@3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/xev/-/xev-3.0.2.tgz#3f4080bd8bed0d3479c674050e3696da98d22a4d"
diff --git a/packages/client/package.json b/packages/client/package.json
index 54ee7dee5..5a087a297 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -35,7 +35,7 @@
 		"escape-regexp": "0.0.1",
 		"eventemitter3": "4.0.7",
 		"feed": "4.2.2",
-		"idb-keyval": "6.1.0",
+		"idb-keyval": "6.2.0",
 		"insert-text-at-cursor": "0.3.0",
 		"json5": "2.2.1",
 		"katex": "0.15.6",
@@ -45,7 +45,7 @@
 		"mocha": "10.0.0",
 		"ms": "2.1.3",
 		"nested-property": "4.0.0",
-		"photoswipe": "5.2.7",
+		"photoswipe": "5.2.8",
 		"prismjs": "1.28.0",
 		"private-ip": "2.3.3",
 		"promise-limit": "2.7.0",
@@ -56,21 +56,21 @@
 		"random-seed": "0.3.0",
 		"reflect-metadata": "0.1.13",
 		"rndstr": "1.0.0",
-		"rollup": "2.75.6",
+		"rollup": "2.75.7",
 		"s-age": "1.1.2",
-		"sass": "1.52.3",
+		"sass": "1.53.0",
 		"seedrandom": "3.0.5",
 		"strict-event-emitter-types": "2.0.0",
 		"stringz": "2.1.0",
 		"syuilo-password-strength": "0.0.1",
 		"textarea-caret": "3.1.0",
-		"three": "0.141.0",
+		"three": "0.142.0",
 		"throttle-debounce": "5.0.0",
 		"tinycolor2": "1.4.2",
-		"tsc-alias": "1.6.9",
+		"tsc-alias": "1.6.11",
 		"tsconfig-paths": "4.0.0",
 		"twemoji-parser": "14.0.0",
-		"typescript": "4.7.3",
+		"typescript": "4.7.4",
 		"uuid": "8.3.2",
 		"v-debounce": "0.1.2",
 		"vanilla-tilt": "1.7.2",
@@ -101,13 +101,13 @@
 		"@types/uuid": "8.3.4",
 		"@types/websocket": "1.0.5",
 		"@types/ws": "8.5.3",
-		"@typescript-eslint/eslint-plugin": "5.27.1",
-		"@typescript-eslint/parser": "5.27.1",
+		"@typescript-eslint/eslint-plugin": "5.30.0",
+		"@typescript-eslint/parser": "5.30.0",
 		"cross-env": "7.0.3",
-		"cypress": "10.0.3",
-		"eslint": "8.17.0",
+		"cypress": "10.3.0",
+		"eslint": "8.18.0",
 		"eslint-plugin-import": "2.26.0",
-		"eslint-plugin-vue": "9.1.0",
+		"eslint-plugin-vue": "9.1.1",
 		"start-server-and-test": "1.14.0"
 	}
 }
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index 58df1576f..e5f2e31d7 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -515,14 +515,14 @@
   dependencies:
     "@types/node" "*"
 
-"@typescript-eslint/eslint-plugin@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.1.tgz#fdf59c905354139046b41b3ed95d1609913d0758"
-  integrity sha512-6dM5NKT57ZduNnJfpY81Phe9nc9wolnMCnknb1im6brWi1RYv84nbMS3olJa27B6+irUVV1X/Wb+Am0FjJdGFw==
+"@typescript-eslint/eslint-plugin@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.0.tgz#524a11e15c09701733033c96943ecf33f55d9ca1"
+  integrity sha512-lvhRJ2pGe2V9MEU46ELTdiHgiAFZPKtLhiU5wlnaYpMc2+c1R8fh8i80ZAa665drvjHKUJyRRGg3gEm1If54ow==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.27.1"
-    "@typescript-eslint/type-utils" "5.27.1"
-    "@typescript-eslint/utils" "5.27.1"
+    "@typescript-eslint/scope-manager" "5.30.0"
+    "@typescript-eslint/type-utils" "5.30.0"
+    "@typescript-eslint/utils" "5.30.0"
     debug "^4.3.4"
     functional-red-black-tree "^1.0.1"
     ignore "^5.2.0"
@@ -530,69 +530,69 @@
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/parser@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.27.1.tgz#3a4dcaa67e45e0427b6ca7bb7165122c8b569639"
-  integrity sha512-7Va2ZOkHi5NP+AZwb5ReLgNF6nWLGTeUJfxdkVUAPPSaAdbWNnFZzLZ4EGGmmiCTg+AwlbE1KyUYTBglosSLHQ==
+"@typescript-eslint/parser@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.30.0.tgz#a2184fb5f8ef2bf1db0ae61a43907e2e32aa1b8f"
+  integrity sha512-2oYYUws5o2liX6SrFQ5RB88+PuRymaM2EU02/9Ppoyu70vllPnHVO7ioxDdq/ypXHA277R04SVjxvwI8HmZpzA==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.27.1"
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/typescript-estree" "5.27.1"
+    "@typescript-eslint/scope-manager" "5.30.0"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/typescript-estree" "5.30.0"
     debug "^4.3.4"
 
-"@typescript-eslint/scope-manager@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.1.tgz#4d1504392d01fe5f76f4a5825991ec78b7b7894d"
-  integrity sha512-fQEOSa/QroWE6fAEg+bJxtRZJTH8NTskggybogHt4H9Da8zd4cJji76gA5SBlR0MgtwF7rebxTbDKB49YUCpAg==
+"@typescript-eslint/scope-manager@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.30.0.tgz#bf585ee801ab4ad84db2f840174e171a6bb002c7"
+  integrity sha512-3TZxvlQcK5fhTBw5solQucWSJvonXf5yua5nx8OqK94hxdrT7/6W3/CS42MLd/f1BmlmmbGEgQcTHHCktUX5bQ==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/visitor-keys" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/visitor-keys" "5.30.0"
 
-"@typescript-eslint/type-utils@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.27.1.tgz#369f695199f74c1876e395ebea202582eb1d4166"
-  integrity sha512-+UC1vVUWaDHRnC2cQrCJ4QtVjpjjCgjNFpg8b03nERmkHv9JV9X5M19D7UFMd+/G7T/sgFwX2pGmWK38rqyvXw==
+"@typescript-eslint/type-utils@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.30.0.tgz#98f3af926a5099153f092d4dad87148df21fbaae"
+  integrity sha512-GF8JZbZqSS+azehzlv/lmQQ3EU3VfWYzCczdZjJRxSEeXDQkqFhCBgFhallLDbPwQOEQ4MHpiPfkjKk7zlmeNg==
   dependencies:
-    "@typescript-eslint/utils" "5.27.1"
+    "@typescript-eslint/utils" "5.30.0"
     debug "^4.3.4"
     tsutils "^3.21.0"
 
-"@typescript-eslint/types@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.1.tgz#34e3e629501349d38be6ae97841298c03a6ffbf1"
-  integrity sha512-LgogNVkBhCTZU/m8XgEYIWICD6m4dmEDbKXESCbqOXfKZxRKeqpiJXQIErv66sdopRKZPo5l32ymNqibYEH/xg==
+"@typescript-eslint/types@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.30.0.tgz#db7d81d585a3da3801432a9c1d2fafbff125e110"
+  integrity sha512-vfqcBrsRNWw/LBXyncMF/KrUTYYzzygCSsVqlZ1qGu1QtGs6vMkt3US0VNSQ05grXi5Yadp3qv5XZdYLjpp8ag==
 
-"@typescript-eslint/typescript-estree@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.1.tgz#7621ee78607331821c16fffc21fc7a452d7bc808"
-  integrity sha512-DnZvvq3TAJ5ke+hk0LklvxwYsnXpRdqUY5gaVS0D4raKtbznPz71UJGnPTHEFo0GDxqLOLdMkkmVZjSpET1hFw==
+"@typescript-eslint/typescript-estree@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.0.tgz#4565ee8a6d2ac368996e20b2344ea0eab1a8f0bb"
+  integrity sha512-hDEawogreZB4n1zoqcrrtg/wPyyiCxmhPLpZ6kmWfKF5M5G0clRLaEexpuWr31fZ42F96SlD/5xCt1bT5Qm4Nw==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/visitor-keys" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/visitor-keys" "5.30.0"
     debug "^4.3.4"
     globby "^11.1.0"
     is-glob "^4.0.3"
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/utils@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.27.1.tgz#b4678b68a94bc3b85bf08f243812a6868ac5128f"
-  integrity sha512-mZ9WEn1ZLDaVrhRaYgzbkXBkTPghPFsup8zDbbsYTxC5OmqrFE7skkKS/sraVsLP3TcT3Ki5CSyEFBRkLH/H/w==
+"@typescript-eslint/utils@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.30.0.tgz#1dac771fead5eab40d31860716de219356f5f754"
+  integrity sha512-0bIgOgZflLKIcZsWvfklsaQTM3ZUbmtH0rJ1hKyV3raoUYyeZwcjQ8ZUJTzS7KnhNcsVT1Rxs7zeeMHEhGlltw==
   dependencies:
     "@types/json-schema" "^7.0.9"
-    "@typescript-eslint/scope-manager" "5.27.1"
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/typescript-estree" "5.27.1"
+    "@typescript-eslint/scope-manager" "5.30.0"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/typescript-estree" "5.30.0"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
-"@typescript-eslint/visitor-keys@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.1.tgz#05a62666f2a89769dac2e6baa48f74e8472983af"
-  integrity sha512-xYs6ffo01nhdJgPieyk7HAOpjhTsx7r/oB9LWEhwAXgwn33tkr+W8DI2ChboqhZlC4q3TC6geDYPoiX8ROqyOQ==
+"@typescript-eslint/visitor-keys@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.0.tgz#07721d23daca2ec4c2da7f1e660d41cd78bacac3"
+  integrity sha512-6WcIeRk2DQ3pHKxU1Ni0qMXJkjO/zLjBymlYBy/53qxe7yjEFSvzKLDToJjURUhSl2Fzhkl4SMXQoETauF74cw==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
     eslint-visitor-keys "^3.3.0"
 
 "@ungap/promise-all-settled@1.1.2":
@@ -1265,10 +1265,10 @@ csstype@^2.6.8:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f"
   integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==
 
-cypress@10.0.3:
-  version "10.0.3"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.0.3.tgz#889b4bef863b7d1ef1b608b85b964394ad350c5f"
-  integrity sha512-8C82XTybsEmJC9POYSNITGUhMLCRwB9LadP0x33H+52QVoBjhsWvIzrI+ybCe0+TyxaF0D5/9IL2kSTgjqCB9A==
+cypress@10.3.0:
+  version "10.3.0"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.3.0.tgz#fae8d32f0822fcfb938e79c7c31ef344794336ae"
+  integrity sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg==
   dependencies:
     "@cypress/request" "^2.88.10"
     "@cypress/xvfb" "^1.2.4"
@@ -1720,10 +1720,10 @@ eslint-plugin-import@2.26.0:
     resolve "^1.22.0"
     tsconfig-paths "^3.14.1"
 
-eslint-plugin-vue@9.1.0:
-  version "9.1.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.1.0.tgz#b528941325e26a24bc5d5c5030c0a8996c36659c"
-  integrity sha512-EPCeInPicQ/YyfOWJDr1yfEeSNoFCMzUus107lZyYi37xejdOolNzS5MXGXp8+9bkoKZMdv/1AcZzQebME6r+g==
+eslint-plugin-vue@9.1.1:
+  version "9.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.1.1.tgz#341f7533cb041958455138834341d5be01f9f327"
+  integrity sha512-W9n5PB1X2jzC7CK6riG0oAcxjmKrjTF6+keL1rni8n57DZeilx/Fulz+IRJK3lYseLNAygN0I62L7DvioW40Tw==
   dependencies:
     eslint-utils "^3.0.0"
     natural-compare "^1.4.0"
@@ -1766,10 +1766,10 @@ eslint-visitor-keys@^3.3.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
   integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
 
-eslint@8.17.0:
-  version "8.17.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.17.0.tgz#1cfc4b6b6912f77d24b874ca1506b0fe09328c21"
-  integrity sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==
+eslint@8.18.0:
+  version "8.18.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.18.0.tgz#78d565d16c993d0b73968c523c0446b13da784fd"
+  integrity sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==
   dependencies:
     "@eslint/eslintrc" "^1.3.0"
     "@humanwhocodes/config-array" "^0.9.2"
@@ -2349,10 +2349,10 @@ human-signals@^2.1.0:
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
   integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
 
-idb-keyval@6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.1.0.tgz#e659cff41188e6097d7fadd69926f6adbbe70041"
-  integrity sha512-u/qHZ75rlD3gH+Zah8dAJVJcGW/RfCnfNrFkElC5RpRCnpsCXXhqjVk+6MoVKJ3WhmNbRYdI6IIVP88e+5sxGw==
+idb-keyval@6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.0.tgz#3af94a3cc0689d6ee0bc9e045d2a3340ea897173"
+  integrity sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==
   dependencies:
     safari-14-idb-fix "^3.0.0"
 
@@ -3274,10 +3274,10 @@ performance-now@^2.1.0:
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
-photoswipe@5.2.7:
-  version "5.2.7"
-  resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.2.7.tgz#9ff2aaf2a3e03c817ac2835dc6dee0f901e8159d"
-  integrity sha512-AogMba7W/O5gOtDIZ8cQuou1ltwxlaLNoZY1qi1s+kbYXpZk9D6rXxnNGAfDppl+bfe+sKLW2w2sx+3uQ8oPzg==
+photoswipe@5.2.8:
+  version "5.2.8"
+  resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.2.8.tgz#c276a17dac575c746262472ceb594fc390786176"
+  integrity sha512-tsbG+1ILcli4mR3Tzp4xdxCUSSJaz14wct4dSznk3suVst9tBdt6vDlSASOw/VFqSkcDjbRUA1tC1LoF2DCkzg==
 
 picocolors@^1.0.0:
   version "1.0.0"
@@ -3654,14 +3654,7 @@ rndstr@1.0.0:
     rangestr "0.0.1"
     seedrandom "2.4.2"
 
-rollup@2.75.6:
-  version "2.75.6"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.75.6.tgz#ac4dc8600f95942a0180f61c7c9d6200e374b439"
-  integrity sha512-OEf0TgpC9vU6WGROJIk1JA3LR5vk/yvqlzxqdrE2CzzXnqKXNzbAwlWUXis8RS3ZPe7LAq+YUxsRa0l3r27MLA==
-  optionalDependencies:
-    fsevents "~2.3.2"
-
-rollup@^2.75.6:
+rollup@2.75.7, rollup@^2.75.6:
   version "2.75.7"
   resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.75.7.tgz#221ff11887ae271e37dcc649ba32ce1590aaa0b9"
   integrity sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ==
@@ -3712,10 +3705,10 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-sass@1.52.3:
-  version "1.52.3"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.52.3.tgz#b7cc7ffea2341ccc9a0c4fd372bf1b3f9be1b6cb"
-  integrity sha512-LNNPJ9lafx+j1ArtA7GyEJm9eawXN8KlA1+5dF6IZyoONg1Tyo/g+muOsENWJH/2Q1FHbbV4UwliU0cXMa/VIA==
+sass@1.53.0:
+  version "1.53.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.53.0.tgz#eab73a7baac045cc57ddc1d1ff501ad2659952eb"
+  integrity sha512-zb/oMirbKhUgRQ0/GFz8TSAwRq2IlR29vOUJZOx0l8sV+CkHUfHa4u5nqrG+1VceZp7Jfj59SVW9ogdhTvJDcQ==
   dependencies:
     chokidar ">=3.0.0 <4.0.0"
     immutable "^4.0.0"
@@ -3980,10 +3973,10 @@ textarea-caret@3.1.0:
   resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.1.0.tgz#5d5a35bb035fd06b2ff0e25d5359e97f2655087f"
   integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==
 
-three@0.141.0:
-  version "0.141.0"
-  resolved "https://registry.yarnpkg.com/three/-/three-0.141.0.tgz#16677a12b9dd0c3e1568ebad0fd09de15d5a8216"
-  integrity sha512-JaSDAPWuk4RTzG5BYRQm8YZbERUxTfTDVouWgHMisS2to4E5fotMS9F2zPFNOIJyEFTTQDDKPpsgZVThKU3pXA==
+three@0.142.0:
+  version "0.142.0"
+  resolved "https://registry.yarnpkg.com/three/-/three-0.142.0.tgz#89e226a16221f212eb1d40f0786604b711f28aed"
+  integrity sha512-ESjPO+3geFr+ZUfVMpMnF/eVU2uJPOh0e2ZpMFqjNca1wApS9lJb7E4MjwGIczgt9iuKd8PEm6Pfgp2bJ92Xtg==
 
 throttle-debounce@5.0.0:
   version "5.0.0"
@@ -4037,10 +4030,10 @@ tough-cookie@~2.5.0:
     psl "^1.1.28"
     punycode "^2.1.1"
 
-tsc-alias@1.6.9:
-  version "1.6.9"
-  resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.6.9.tgz#d04d95124b95ad8eea55e52d45cf65a744c26baa"
-  integrity sha512-5lv5uAHn0cgxY1XfpXIdquUSz2xXq3ryQyNtxC6DYH7YT5rt/W+9Gsft2uyLFTh+ozk4qU8iCSP3VemjT69xlQ==
+tsc-alias@1.6.11:
+  version "1.6.11"
+  resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.6.11.tgz#d6d83f030ad11f48e4ead8ec5729929e5e60519b"
+  integrity sha512-mXEM21WriTJMQyo07B4Kc2nNFFk/1qOjU+jZ0ymXOyLz/A8J+dIBkimqZrh3s/x1qLGoJ1cNZQxa8GGoWOGX1Q==
   dependencies:
     chokidar "^3.5.3"
     commander "^9.0.0"
@@ -4136,10 +4129,10 @@ typedarray-to-buffer@^3.1.5:
   dependencies:
     is-typedarray "^1.0.0"
 
-typescript@4.7.3:
-  version "4.7.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
-  integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==
+typescript@4.7.4:
+  version "4.7.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
 
 unbox-primitive@^1.0.1:
   version "1.0.1"
diff --git a/yarn.lock b/yarn.lock
index 327eac017..97d548d0d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -194,48 +194,48 @@
   dependencies:
     "@types/node" "*"
 
-"@typescript-eslint/parser@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.27.1.tgz#3a4dcaa67e45e0427b6ca7bb7165122c8b569639"
-  integrity sha512-7Va2ZOkHi5NP+AZwb5ReLgNF6nWLGTeUJfxdkVUAPPSaAdbWNnFZzLZ4EGGmmiCTg+AwlbE1KyUYTBglosSLHQ==
+"@typescript-eslint/parser@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.30.0.tgz#a2184fb5f8ef2bf1db0ae61a43907e2e32aa1b8f"
+  integrity sha512-2oYYUws5o2liX6SrFQ5RB88+PuRymaM2EU02/9Ppoyu70vllPnHVO7ioxDdq/ypXHA277R04SVjxvwI8HmZpzA==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.27.1"
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/typescript-estree" "5.27.1"
+    "@typescript-eslint/scope-manager" "5.30.0"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/typescript-estree" "5.30.0"
     debug "^4.3.4"
 
-"@typescript-eslint/scope-manager@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.1.tgz#4d1504392d01fe5f76f4a5825991ec78b7b7894d"
-  integrity sha512-fQEOSa/QroWE6fAEg+bJxtRZJTH8NTskggybogHt4H9Da8zd4cJji76gA5SBlR0MgtwF7rebxTbDKB49YUCpAg==
+"@typescript-eslint/scope-manager@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.30.0.tgz#bf585ee801ab4ad84db2f840174e171a6bb002c7"
+  integrity sha512-3TZxvlQcK5fhTBw5solQucWSJvonXf5yua5nx8OqK94hxdrT7/6W3/CS42MLd/f1BmlmmbGEgQcTHHCktUX5bQ==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/visitor-keys" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/visitor-keys" "5.30.0"
 
-"@typescript-eslint/types@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.1.tgz#34e3e629501349d38be6ae97841298c03a6ffbf1"
-  integrity sha512-LgogNVkBhCTZU/m8XgEYIWICD6m4dmEDbKXESCbqOXfKZxRKeqpiJXQIErv66sdopRKZPo5l32ymNqibYEH/xg==
+"@typescript-eslint/types@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.30.0.tgz#db7d81d585a3da3801432a9c1d2fafbff125e110"
+  integrity sha512-vfqcBrsRNWw/LBXyncMF/KrUTYYzzygCSsVqlZ1qGu1QtGs6vMkt3US0VNSQ05grXi5Yadp3qv5XZdYLjpp8ag==
 
-"@typescript-eslint/typescript-estree@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.1.tgz#7621ee78607331821c16fffc21fc7a452d7bc808"
-  integrity sha512-DnZvvq3TAJ5ke+hk0LklvxwYsnXpRdqUY5gaVS0D4raKtbznPz71UJGnPTHEFo0GDxqLOLdMkkmVZjSpET1hFw==
+"@typescript-eslint/typescript-estree@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.0.tgz#4565ee8a6d2ac368996e20b2344ea0eab1a8f0bb"
+  integrity sha512-hDEawogreZB4n1zoqcrrtg/wPyyiCxmhPLpZ6kmWfKF5M5G0clRLaEexpuWr31fZ42F96SlD/5xCt1bT5Qm4Nw==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
-    "@typescript-eslint/visitor-keys" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
+    "@typescript-eslint/visitor-keys" "5.30.0"
     debug "^4.3.4"
     globby "^11.1.0"
     is-glob "^4.0.3"
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/visitor-keys@5.27.1":
-  version "5.27.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.1.tgz#05a62666f2a89769dac2e6baa48f74e8472983af"
-  integrity sha512-xYs6ffo01nhdJgPieyk7HAOpjhTsx7r/oB9LWEhwAXgwn33tkr+W8DI2ChboqhZlC4q3TC6geDYPoiX8ROqyOQ==
+"@typescript-eslint/visitor-keys@5.30.0":
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.0.tgz#07721d23daca2ec4c2da7f1e660d41cd78bacac3"
+  integrity sha512-6WcIeRk2DQ3pHKxU1Ni0qMXJkjO/zLjBymlYBy/53qxe7yjEFSvzKLDToJjURUhSl2Fzhkl4SMXQoETauF74cw==
   dependencies:
-    "@typescript-eslint/types" "5.27.1"
+    "@typescript-eslint/types" "5.30.0"
     eslint-visitor-keys "^3.3.0"
 
 aggregate-error@^3.0.0:
@@ -1084,10 +1084,10 @@ csso@~2.3.1:
     clap "^1.0.9"
     source-map "^0.5.3"
 
-cypress@10.0.3:
-  version "10.0.3"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.0.3.tgz#889b4bef863b7d1ef1b608b85b964394ad350c5f"
-  integrity sha512-8C82XTybsEmJC9POYSNITGUhMLCRwB9LadP0x33H+52QVoBjhsWvIzrI+ybCe0+TyxaF0D5/9IL2kSTgjqCB9A==
+cypress@10.3.0:
+  version "10.3.0"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.3.0.tgz#fae8d32f0822fcfb938e79c7c31ef344794336ae"
+  integrity sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg==
   dependencies:
     "@cypress/request" "^2.88.10"
     "@cypress/xvfb" "^1.2.4"
@@ -4201,10 +4201,10 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@4.7.3:
-  version "4.7.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
-  integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==
+typescript@4.7.4:
+  version "4.7.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
 
 unc-path-regex@^0.1.2:
   version "0.1.2"

From bd092f29b1f1c029bb6cd7a63c6120e1dfe60e6d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 00:21:38 +0900
Subject: [PATCH 034/100] 12.112.0-beta.10

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

diff --git a/package.json b/package.json
index d3fdcc8c5..08c539044 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.9",
+	"version": "12.112.0-beta.10",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From f4b4b9fdfca8fa148662ac545563f016aea4fea3 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 00:38:20 +0900
Subject: [PATCH 035/100] chore(client): fix pie rendering

---
 packages/client/src/pages/admin/overview.vue | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index d2fa4e0e4..f44bba27f 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -413,22 +413,22 @@ onMounted(async () => {
 	});
 
 	os.apiGet('federation/stats', { limit: 10 }).then(res => {
-		topSubInstancesForPie = fedStats.topSubInstances.map(x => ({
+		topSubInstancesForPie = res.topSubInstances.map(x => ({
 			name: x.host,
 			color: x.themeColor,
 			value: x.followersCount,
 			onClick: () => {
 				os.pageWindow(`/instance-info/${x.host}`);
 			},
-		})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]);
-		topPubInstancesForPie = fedStats.topPubInstances.map(x => ({
+		})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
+		topPubInstancesForPie = res.topPubInstances.map(x => ({
 			name: x.host,
 			color: x.themeColor,
 			value: x.followingCount,
 			onClick: () => {
 				os.pageWindow(`/instance-info/${x.host}`);
 			},
-		})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]);
+		})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
 	});
 
 	os.api('admin/server-info').then(serverInfoResponse => {

From 95290ad08513c1fb5bc1a6e35782ba77d1bf15bc Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Thu, 30 Jun 2022 22:03:04 +0200
Subject: [PATCH 036/100] fix typo

Co-authored-by: mei23 <m@m544.net>
---
 .../backend/src/server/api/stream/channels/local-timeline.ts    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 8bb927987..f01f47723 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -51,7 +51,7 @@ export default class extends Channel {
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (iUserRelated(note, this.muting)) return;
+		if (isUserRelated(note, this.muting)) return;
 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 		if (isUserRelated(note, this.blocking)) return;
 

From 23c1109fb0fffe35eb9dd1bf78fde43aaf27078c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 11:07:14 +0900
Subject: [PATCH 037/100] use parse5 6.0.1

Fix #8914
---
 packages/backend/package.json         |  3 +-
 packages/backend/src/mfm/from-html.ts |  2 +-
 packages/backend/yarn.lock            | 40 +++++++--------------------
 3 files changed, 12 insertions(+), 33 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index c5ab587cf..7576a203d 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -75,8 +75,7 @@
 		"node-fetch": "3.2.6",
 		"nodemailer": "6.7.6",
 		"os-utils": "0.0.14",
-		"parse5": "7.0.0",
-		"parse5-htmlparser2-tree-adapter": "7.0.0",
+		"parse5": "6.0.1",
 		"pg": "8.7.3",
 		"private-ip": "2.3.3",
 		"probe-image-size": "7.2.3",
diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts
index 710c18f44..e94dbb800 100644
--- a/packages/backend/src/mfm/from-html.ts
+++ b/packages/backend/src/mfm/from-html.ts
@@ -1,6 +1,6 @@
 import { URL } from 'node:url';
 import * as parse5 from 'parse5';
-import { adapter as treeAdapter } from 'parse5-htmlparser2-tree-adapter';
+import treeAdapter from 'parse5/lib/tree-adapters/default.js';
 
 const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
 const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock
index a9c20c3c0..cc8292a25 100644
--- a/packages/backend/yarn.lock
+++ b/packages/backend/yarn.lock
@@ -2331,11 +2331,6 @@ domelementtype@^2.2.0:
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
   integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
 
-domelementtype@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
-  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
-
 domexception@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
@@ -2364,13 +2359,6 @@ domhandler@^4.2.0:
   dependencies:
     domelementtype "^2.2.0"
 
-domhandler@^5.0.2:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
-  integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
-  dependencies:
-    domelementtype "^2.3.0"
-
 domutils@1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
@@ -5248,14 +5236,6 @@ parse-srcset@^1.0.2:
   resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
   integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=
 
-parse5-htmlparser2-tree-adapter@7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
-  integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==
-  dependencies:
-    domhandler "^5.0.2"
-    parse5 "^7.0.0"
-
 parse5-htmlparser2-tree-adapter@^6.0.0:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
@@ -5263,22 +5243,22 @@ parse5-htmlparser2-tree-adapter@^6.0.0:
   dependencies:
     parse5 "^6.0.1"
 
-parse5@7.0.0, parse5@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a"
-  integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==
-  dependencies:
-    entities "^4.3.0"
+parse5@6.0.1, parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
 
 parse5@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
   integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
 
-parse5@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
-  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+parse5@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a"
+  integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==
+  dependencies:
+    entities "^4.3.0"
 
 parseurl@^1.3.2:
   version "1.3.3"

From 62d6d106a6074c53b9119e1fd7edfeaf6335c8df Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Fri, 1 Jul 2022 13:48:03 +0900
Subject: [PATCH 038/100] migrate parse5 to 7.0.0 (#8916)

* migrate parse5 to 7.0.0

* fix
---
 packages/backend/package.json         |  2 +-
 packages/backend/src/mfm/from-html.ts | 10 ++++++----
 packages/backend/yarn.lock            | 20 ++++++++++----------
 3 files changed, 17 insertions(+), 15 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index 7576a203d..0bf47888e 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -75,7 +75,7 @@
 		"node-fetch": "3.2.6",
 		"nodemailer": "6.7.6",
 		"os-utils": "0.0.14",
-		"parse5": "6.0.1",
+		"parse5": "7.0.0",
 		"pg": "8.7.3",
 		"private-ip": "2.3.3",
 		"probe-image-size": "7.2.3",
diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts
index e94dbb800..7751bac56 100644
--- a/packages/backend/src/mfm/from-html.ts
+++ b/packages/backend/src/mfm/from-html.ts
@@ -1,6 +1,8 @@
 import { URL } from 'node:url';
 import * as parse5 from 'parse5';
-import treeAdapter from 'parse5/lib/tree-adapters/default.js';
+import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
+
+const treeAdapter = TreeAdapter.defaultTreeAdapter;
 
 const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
 const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@@ -19,7 +21,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
 
 	return text.trim();
 
-	function getText(node: parse5.Node): string {
+	function getText(node: TreeAdapter.Node): string {
 		if (treeAdapter.isTextNode(node)) return node.value;
 		if (!treeAdapter.isElementNode(node)) return '';
 		if (node.nodeName === 'br') return '\n';
@@ -31,7 +33,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
 		return '';
 	}
 
-	function appendChildren(childNodes: parse5.ChildNode[]): void {
+	function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
 		if (childNodes) {
 			for (const n of childNodes) {
 				analyze(n);
@@ -39,7 +41,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
 		}
 	}
 
-	function analyze(node: parse5.Node) {
+	function analyze(node: TreeAdapter.Node) {
 		if (treeAdapter.isTextNode(node)) {
 			text += node.value;
 			return;
diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock
index cc8292a25..880bbf7d1 100644
--- a/packages/backend/yarn.lock
+++ b/packages/backend/yarn.lock
@@ -5243,22 +5243,22 @@ parse5-htmlparser2-tree-adapter@^6.0.0:
   dependencies:
     parse5 "^6.0.1"
 
-parse5@6.0.1, parse5@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
-  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+parse5@7.0.0, parse5@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a"
+  integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==
+  dependencies:
+    entities "^4.3.0"
 
 parse5@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
   integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
 
-parse5@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a"
-  integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==
-  dependencies:
-    entities "^4.3.0"
+parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
 
 parseurl@^1.3.2:
   version "1.3.3"

From 0c252532be74cd7f9d380565ab0af081acc3cede Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 15:06:17 +0900
Subject: [PATCH 039/100] chore(client): tweak ui

---
 .../client/src/pages/welcome.entrance.a.vue   | 290 +++++++++---------
 1 file changed, 139 insertions(+), 151 deletions(-)

diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue
index 47e1f1234..b78a37eab 100644
--- a/packages/client/src/pages/welcome.entrance.a.vue
+++ b/packages/client/src/pages/welcome.entrance.a.vue
@@ -13,10 +13,9 @@
 			<MkEmoji :normal="true" :no-style="true" emoji="🎉"/>
 			<MkEmoji :normal="true" :no-style="true" emoji="🍮"/>
 		</div>
-		<div class="main _panel">
-			<div class="bg">
-				<div class="fade"></div>
-			</div>
+		<div class="main">
+			<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
+			<button class="_button _acrylic menu" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
 			<div class="fg">
 				<h1>
 					<!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に -->
@@ -24,123 +23,107 @@
 					<span class="text">{{ instanceName }}</span>
 				</h1>
 				<div class="about">
-					<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
+					<div class="desc" v-html="meta.description || i18n.ts.headlineMisskey"></div>
 				</div>
 				<div class="action">
-					<MkButton inline gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ $ts.signup }}</MkButton>
-					<MkButton inline data-cy-signin @click="signin()">{{ $ts.login }}</MkButton>
+					<MkButton inline rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.signup }}</MkButton>
+					<MkButton inline rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
 				</div>
-				<div v-if="onlineUsersCount && stats" class="status">
-					<div>
-						<I18n :src="$ts.nUsers" text-tag="span" class="users">
-							<template #n><b>{{ number(stats.originalUsersCount) }}</b></template>
-						</I18n>
-						<I18n :src="$ts.nNotes" text-tag="span" class="notes">
-							<template #n><b>{{ number(stats.originalNotesCount) }}</b></template>
-						</I18n>
-					</div>
-					<I18n :src="$ts.onlineUsersCount" text-tag="span" class="online">
-						<template #n><b>{{ onlineUsersCount }}</b></template>
-					</I18n>
-				</div>
-				<button class="_button _acrylic menu" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
 			</div>
 		</div>
+		<div v-if="instances" class="federation">
+			<MarqueeText :duration="40">
+				<MkA v-for="instance in instances" :key="instance.id" :class="$style.federationInstance" :to="`/instance-info/${instance.host}`" behavior="window">
+					<!--<MkInstanceCardMini :instance="instance"/>-->
+					<img v-if="instance.iconUrl" class="icon" :src="instance.iconUrl" alt=""/>
+					<span class="name _monospace">{{ instance.host }}</span>
+				</MkA>
+			</MarqueeText>
+		</div>
 	</div>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import { toUnicode } from 'punycode/';
+import MarqueeText from 'vue-marquee-text-component';
+import XTimeline from './welcome.timeline.vue';
 import XSigninDialog from '@/components/signin-dialog.vue';
 import XSignupDialog from '@/components/signup-dialog.vue';
 import MkButton from '@/components/ui/button.vue';
 import XNote from '@/components/note.vue';
 import MkFeaturedPhotos from '@/components/featured-photos.vue';
-import XTimeline from './welcome.timeline.vue';
 import { host, instanceName } from '@/config';
 import * as os from '@/os';
 import number from '@/filters/number';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkButton,
-		XNote,
-		MkFeaturedPhotos,
-		XTimeline,
-	},
+let meta = $ref();
+let stats = $ref();
+let tags = $ref();
+let onlineUsersCount = $ref();
+let instances = $ref();
 
-	data() {
-		return {
-			host: toUnicode(host),
-			instanceName,
-			meta: null,
-			stats: null,
-			tags: [],
-			onlineUsersCount: null,
-		};
-	},
-
-	created() {
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-
-		os.api('stats').then(stats => {
-			this.stats = stats;
-		});
-
-		os.api('get-online-users-count').then(res => {
-			this.onlineUsersCount = res.count;
-		});
-
-		os.api('hashtags/list', {
-			sort: '+mentionedLocalUsers',
-			limit: 8
-		}).then(tags => {
-			this.tags = tags;
-		});
-	},
-
-	methods: {
-		signin() {
-			os.popup(XSigninDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		signup() {
-			os.popup(XSignupDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		showMenu(ev) {
-			os.popupMenu([{
-				text: this.$t('aboutX', { x: instanceName }),
-				icon: 'fas fa-info-circle',
-				action: () => {
-					os.pageWindow('/about');
-				}
-			}, {
-				text: this.$ts.aboutMisskey,
-				icon: 'fas fa-info-circle',
-				action: () => {
-					os.pageWindow('/about-misskey');
-				}
-			}, null, {
-				text: this.$ts.help,
-				icon: 'fas fa-question-circle',
-				action: () => {
-					window.open(`https://misskey-hub.net/help.md`, '_blank');
-				}
-			}], ev.currentTarget ?? ev.target);
-		},
-
-		number
-	}
+os.api('meta', { detail: true }).then(_meta => {
+	meta = _meta;
 });
+
+os.api('stats').then(_stats => {
+	stats = _stats;
+});
+
+os.api('get-online-users-count').then(res => {
+	onlineUsersCount = res.count;
+});
+
+os.api('hashtags/list', {
+	sort: '+mentionedLocalUsers',
+	limit: 8,
+}).then(_tags => {
+	tags = _tags;
+});
+
+os.api('federation/instances', {
+	sort: '+pubSub',
+	limit: 20,
+}).then(_instances => {
+	instances = _instances;
+});
+
+function signin() {
+	os.popup(XSigninDialog, {
+		autoSet: true,
+	}, {}, 'closed');
+}
+
+function signup() {
+	os.popup(XSignupDialog, {
+		autoSet: true,
+	}, {}, 'closed');
+}
+
+function showMenu(ev) {
+	os.popupMenu([{
+		text: i18n.ts.instanceInfo,
+		icon: 'fas fa-info-circle',
+		action: () => {
+			os.pageWindow('/about');
+		},
+	}, {
+		text: i18n.ts.aboutMisskey,
+		icon: 'fas fa-info-circle',
+		action: () => {
+			os.pageWindow('/about-misskey');
+		},
+	}, null, {
+		text: i18n.ts.help,
+		icon: 'fas fa-question-circle',
+		action: () => {
+			window.open('https://misskey-hub.net/help.md', '_blank');
+		},
+	}], ev.currentTarget ?? ev.target);
+}
 </script>
 
 <style lang="scss" scoped>
@@ -201,7 +184,7 @@ export default defineComponent({
 			position: absolute;
 			top: 42px;
 			left: 42px;
-			width: 160px;
+			width: 140px;
 
 			@media (max-width: 450px) {
 				width: 130px;
@@ -226,30 +209,29 @@ export default defineComponent({
 			position: relative;
 			width: min(480px, 100%);
 			margin: auto auto auto 128px;
+			background: var(--panel);
+			border-radius: var(--radius);
 			box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
 
 			@media (max-width: 1200px) {
 				margin: auto;
 			}
 
-			> .bg {
-				position: absolute;
-				top: 0;
-				left: 0;
-				width: 100%;
-				height: 128px;
-				background-position: center;
-				background-size: cover;
-				opacity: 0.75;
+			> .icon {
+				width: 85px;
+				margin-top: -47px;
+				border-radius: 100%;
+				vertical-align: bottom;
+			}
 
-				> .fade {
-					position: absolute;
-					bottom: 0;
-					left: 0;
-					width: 100%;
-					height: 128px;
-					background: linear-gradient(0deg, var(--panel), var(--X15));
-				}
+			> .menu {
+				position: absolute;
+				top: 16px;
+				right: 16px;
+				width: 32px;
+				height: 32px;
+				border-radius: 8px;
+				font-size: 18px;
 			}
 
 			> .fg {
@@ -259,8 +241,8 @@ export default defineComponent({
 				> h1 {
 					display: block;
 					margin: 0;
-					padding: 32px 32px 24px 32px;
-					font-size: 1.5em;
+					padding: 16px 32px 24px 32px;
+					font-size: 1.4em;
 
 					> .logo {
 						vertical-align: bottom;
@@ -280,41 +262,47 @@ export default defineComponent({
 						line-height: 28px;
 					}
 				}
+			}
+		}
 
-				> .status {
-					border-top: solid 0.5px var(--divider);
-					padding: 32px;
-					font-size: 90%;
+		> .federation {
+			position: absolute;
+			bottom: 16px;
+			left: 0;
+			right: 0;
+			margin: auto;
+			background: var(--acrylicPanel);
+			-webkit-backdrop-filter: var(--blur, blur(15px));
+			backdrop-filter: var(--blur, blur(15px));
+			border-radius: 999px;
+			overflow: clip;
+			width: 800px;
+			padding: 8px 0;
 
-					> div {
-						> span:not(:last-child) {
-							padding-right: 1em;
-							margin-right: 1em;
-							border-right: solid 0.5px var(--divider);
-						}
-					}
-
-					> .online {
-						::v-deep(b) {
-							color: #41b781;
-						}
-
-						::v-deep(span) {
-							opacity: 0.7;
-						}
-					}
-				}
-
-				> .menu {
-					position: absolute;
-					top: 16px;
-					right: 16px;
-					width: 32px;
-					height: 32px;
-					border-radius: 8px;
-				}
+			@media (max-width: 900px) {
+				display: none;
 			}
 		}
 	}
 }
 </style>
+
+<style lang="scss" module>
+.federationInstance {
+	display: inline-flex;
+	align-items: center;
+	vertical-align: bottom;
+	padding: 6px 12px 6px 6px;
+	margin: 0 10px 0 0;
+	background: var(--panel);
+	border-radius: 999px;
+
+	> :global(.icon) {
+		display: inline-block;
+		width: 20px;
+		height: 20px;
+		margin-right: 5px;
+		border-radius: 999px;
+	}
+}
+</style>

From 829425d73c9d1dd8aca476d273d5ae63a7f9ba31 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 15:11:39 +0900
Subject: [PATCH 040/100] 12.112.0-beta.11

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

diff --git a/package.json b/package.json
index 08c539044..aa5a04927 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.10",
+	"version": "12.112.0-beta.11",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From a10b4eb0327c00c5c2d7f161e78eb0229816e6ee Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 15:23:49 +0900
Subject: [PATCH 041/100] chore(client): tweak style

---
 packages/client/src/widgets/rss-marquee.vue | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/widgets/rss-marquee.vue b/packages/client/src/widgets/rss-marquee.vue
index e7516476a..6d910a2fb 100644
--- a/packages/client/src/widgets/rss-marquee.vue
+++ b/packages/client/src/widgets/rss-marquee.vue
@@ -7,7 +7,9 @@
 		<MkLoading v-if="fetching"/>
 		<div v-else class="feed">
 			<MarqueeText :key="key" :duration="widgetProps.speed" :reverse="widgetProps.reverse">
-				<a v-for="item in items" class="item" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
+				<span v-for="item in items" class="item">
+					<a class="link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span class="divider"></span>
+				</span>
 			</MarqueeText>
 		</div>
 	</div>
@@ -108,9 +110,19 @@ defineExpose<WidgetComponentExpose>({
 		font-size: 0.9em;
 
 		::v-deep(.item) {
-			display: inline-block;
+			display: inline-flex;
+			align-items: center;
+			vertical-align: bottom;
 			color: var(--fg);
-			margin: 12px 3em 12px 0;
+			margin: 12px 0;
+
+			> .divider {
+				display: inline-block;
+				width: 0.5px;
+				height: 16px;
+				margin: 0 1em;
+				background: var(--divider);
+			}
 		}
 	}
 }

From 206f323de18d9b67fd12ed498450b481e666c59a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 16:43:38 +0900
Subject: [PATCH 042/100] chore(client): tweak style

---
 packages/client/src/components/form-dialog.vue  | 17 +++++++++--------
 .../client/src/components/modal-page-window.vue |  1 -
 .../client/src/components/ui/modal-window.vue   |  1 -
 3 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue
index 11459f593..345001c43 100644
--- a/packages/client/src/components/form-dialog.vue
+++ b/packages/client/src/components/form-dialog.vue
@@ -1,5 +1,6 @@
 <template>
-<XModalWindow ref="dialog"
+<XModalWindow
+	ref="dialog"
 	:width="450"
 	:can-close="false"
 	:with-ok-button="true"
@@ -37,7 +38,7 @@
 					<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
 				</FormSelect>
 				<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
-					<template #caption><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
+					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
 				</FormRadios>
 				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
@@ -55,7 +56,6 @@
 
 <script lang="ts">
 import { defineComponent } from 'vue';
-import XModalWindow from '@/components/ui/modal-window.vue';
 import FormInput from './form/input.vue';
 import FormTextarea from './form/textarea.vue';
 import FormSwitch from './form/switch.vue';
@@ -63,6 +63,7 @@ import FormSelect from './form/select.vue';
 import FormRange from './form/range.vue';
 import MkButton from './ui/button.vue';
 import FormRadios from './form/radios.vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
 
 export default defineComponent({
 	components: {
@@ -91,7 +92,7 @@ export default defineComponent({
 
 	data() {
 		return {
-			values: {}
+			values: {},
 		};
 	},
 
@@ -104,18 +105,18 @@ export default defineComponent({
 	methods: {
 		ok() {
 			this.$emit('done', {
-				result: this.values
+				result: this.values,
 			});
 			this.$refs.dialog.close();
 		},
 
 		cancel() {
 			this.$emit('done', {
-				canceled: true
+				canceled: true,
 			});
 			this.$refs.dialog.close();
-		}
-	}
+		},
+	},
 });
 </script>
 
diff --git a/packages/client/src/components/modal-page-window.vue b/packages/client/src/components/modal-page-window.vue
index a810bd214..2fed0d35e 100644
--- a/packages/client/src/components/modal-page-window.vue
+++ b/packages/client/src/components/modal-page-window.vue
@@ -143,7 +143,6 @@ function onContextmenu(ev: MouseEvent) {
 		background: var(--windowHeader);
 		-webkit-backdrop-filter: var(--blur, blur(15px));
 		backdrop-filter: var(--blur, blur(15px));
-		box-shadow: 0px 1px var(--divider);
 
 		> button {
 			height: $height;
diff --git a/packages/client/src/components/ui/modal-window.vue b/packages/client/src/components/ui/modal-window.vue
index bf9be971f..b7faea736 100644
--- a/packages/client/src/components/ui/modal-window.vue
+++ b/packages/client/src/components/ui/modal-window.vue
@@ -105,7 +105,6 @@ defineExpose({
 		background: var(--windowHeader);
 		-webkit-backdrop-filter: var(--blur, blur(15px));
 		backdrop-filter: var(--blur, blur(15px));
-		box-shadow: 0px 1px var(--divider);
 
 		> button {
 			height: $height;

From 2b472ee78d97fb6025c835c17ee04e492f369549 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 17:08:45 +0900
Subject: [PATCH 043/100] chore(client): tweak style

---
 packages/client/src/components/instance-stats.vue | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue
index c210371c6..a03993752 100644
--- a/packages/client/src/components/instance-stats.vue
+++ b/packages/client/src/components/instance-stats.vue
@@ -119,6 +119,7 @@ function createDoughnut(chartEl, tooltip, data) {
 			}],
 		},
 		options: {
+			maintainAspectRatio: false,
 			layout: {
 				padding: {
 					left: 16,
@@ -195,10 +196,13 @@ onMounted(() => {
 		gap: 16px;
 
 		> .sub, > .pub {
+			flex: 1;
+			min-width: 0;
 			position: relative;
 			background: var(--panel);
 			border-radius: var(--radius);
 			padding: 24px;
+			max-height: 300px;
 
 			> .title {
 				position: absolute;
@@ -206,6 +210,10 @@ onMounted(() => {
 				left: 24px;
 			}
 		}
+
+		@media (max-width: 600px) {
+			flex-direction: column;
+		}
 	}
 }
 </style>

From 8c3f155a79c259d86575b3f69b7392f5b1a1f352 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 18:55:45 +0900
Subject: [PATCH 044/100] chore(client): tweak ui

---
 .../src/components/global/page-header.vue     |  24 +-
 packages/client/src/pages/admin/_header_.vue  |  24 +-
 packages/client/src/pages/user/followers.vue  |  62 +++
 packages/client/src/pages/user/following.vue  |  62 +++
 packages/client/src/pages/user/home.vue       | 478 +++++++++++++++++
 packages/client/src/pages/user/index.vue      | 489 +-----------------
 packages/client/src/router.ts                 |  14 +-
 7 files changed, 655 insertions(+), 498 deletions(-)
 create mode 100644 packages/client/src/pages/user/followers.vue
 create mode 100644 packages/client/src/pages/user/following.vue
 create mode 100644 packages/client/src/pages/user/home.vue

diff --git a/packages/client/src/components/global/page-header.vue b/packages/client/src/components/global/page-header.vue
index a080c39dd..5395a8796 100644
--- a/packages/client/src/components/global/page-header.vue
+++ b/packages/client/src/components/global/page-header.vue
@@ -34,7 +34,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue';
+import { computed, onMounted, onUnmounted, ref, inject, watch, shallowReactive, nextTick, reactive } from 'vue';
 import tinycolor from 'tinycolor2';
 import { popupMenu } from '@/os';
 import { scrollToTop } from '@/scripts/scroll';
@@ -137,16 +137,18 @@ onMounted(() => {
 	calcBg();
 	globalEvents.on('themeChanged', calcBg);
 
-	watch(() => props.tab, () => {
-		const tabEl = tabRefs[props.tab];
-		if (tabEl && tabHighlightEl) {
-			// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
-			// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
-			const parentRect = tabEl.parentElement.getBoundingClientRect();
-			const rect = tabEl.getBoundingClientRect();
-			tabHighlightEl.style.width = rect.width + 'px';
-			tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
-		}
+	watch(() => [props.tab, props.tabs], () => {
+		nextTick(() => {
+			const tabEl = tabRefs[props.tab];
+			if (tabEl && tabHighlightEl) {
+				// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
+				// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
+				const parentRect = tabEl.parentElement.getBoundingClientRect();
+				const rect = tabEl.getBoundingClientRect();
+				tabHighlightEl.style.width = rect.width + 'px';
+				tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
+			}
+		});
 	}, {
 		immediate: true,
 	});
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index 1883b4abe..1c3cdcb51 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -28,7 +28,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue';
+import { computed, onMounted, onUnmounted, ref, inject, watch, nextTick } from 'vue';
 import tinycolor from 'tinycolor2';
 import { popupMenu } from '@/os';
 import { url } from '@/config';
@@ -126,16 +126,18 @@ onMounted(() => {
 	calcBg();
 	globalEvents.on('themeChanged', calcBg);
 
-	watch(() => props.tab, () => {
-		const tabEl = tabRefs[props.tab];
-		if (tabEl && tabHighlightEl) {
-			// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
-			// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
-			const parentRect = tabEl.parentElement.getBoundingClientRect();
-			const rect = tabEl.getBoundingClientRect();
-			tabHighlightEl.style.width = rect.width + 'px';
-			tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
-		}
+	watch(() => [props.tab, props.tabs], () => {
+		nextTick(() => {
+			const tabEl = tabRefs[props.tab];
+			if (tabEl && tabHighlightEl) {
+				// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
+				// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
+				const parentRect = tabEl.parentElement.getBoundingClientRect();
+				const rect = tabEl.getBoundingClientRect();
+				tabHighlightEl.style.width = rect.width + 'px';
+				tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
+			}
+		});
 	}, {
 		immediate: true,
 	});
diff --git a/packages/client/src/pages/user/followers.vue b/packages/client/src/pages/user/followers.vue
new file mode 100644
index 000000000..3feec15e4
--- /dev/null
+++ b/packages/client/src/pages/user/followers.vue
@@ -0,0 +1,62 @@
+<template>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="1000">
+		<transition name="fade" mode="out-in">
+			<div v-if="user">
+				<XFollowList :user="user" type="following"/>
+			</div>
+			<MkError v-else-if="error" @retry="fetch()"/>
+			<MkLoading v-else/>
+		</transition>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
+import * as misskey from 'misskey-js';
+import XFollowList from './follow-list.vue';
+import * as os from '@/os';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import { i18n } from '@/i18n';
+
+const props = withDefaults(defineProps<{
+	acct: string;
+}>(), {
+});
+
+let user = $ref<null | misskey.entities.UserDetailed>(null);
+let error = $ref(null);
+
+function fetchUser(): void {
+	if (props.acct == null) return;
+	user = null;
+	os.api('users/show', Acct.parse(props.acct)).then(u => {
+		user = u;
+	}).catch(err => {
+		error = err;
+	});
+}
+
+watch(() => props.acct, fetchUser, {
+	immediate: true,
+});
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => user ? {
+	icon: 'fas fa-user',
+	title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`,
+	subtitle: i18n.ts.followers,
+	userName: user,
+	avatar: user,
+	bg: 'var(--bg)',
+} : null));
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/packages/client/src/pages/user/following.vue b/packages/client/src/pages/user/following.vue
new file mode 100644
index 000000000..0c6bb1c9f
--- /dev/null
+++ b/packages/client/src/pages/user/following.vue
@@ -0,0 +1,62 @@
+<template>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="1000">
+		<transition name="fade" mode="out-in">
+			<div v-if="user">
+				<XFollowList :user="user" type="following"/>
+			</div>
+			<MkError v-else-if="error" @retry="fetch()"/>
+			<MkLoading v-else/>
+		</transition>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
+import * as misskey from 'misskey-js';
+import XFollowList from './follow-list.vue';
+import * as os from '@/os';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import { i18n } from '@/i18n';
+
+const props = withDefaults(defineProps<{
+	acct: string;
+}>(), {
+});
+
+let user = $ref<null | misskey.entities.UserDetailed>(null);
+let error = $ref(null);
+
+function fetchUser(): void {
+	if (props.acct == null) return;
+	user = null;
+	os.api('users/show', Acct.parse(props.acct)).then(u => {
+		user = u;
+	}).catch(err => {
+		error = err;
+	});
+}
+
+watch(() => props.acct, fetchUser, {
+	immediate: true,
+});
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => user ? {
+	icon: 'fas fa-user',
+	title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`,
+	subtitle: i18n.ts.following,
+	userName: user,
+	avatar: user,
+	bg: 'var(--bg)',
+} : null));
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/packages/client/src/pages/user/home.vue b/packages/client/src/pages/user/home.vue
new file mode 100644
index 000000000..f7c25f077
--- /dev/null
+++ b/packages/client/src/pages/user/home.vue
@@ -0,0 +1,478 @@
+<template>
+<MkSpacer :content-max="narrow ? 800 : 1100">
+	<div ref="rootEl" v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }">
+		<div class="main">
+			<!-- TODO -->
+			<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
+			<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
+
+			<div class="profile">
+				<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
+
+				<div :key="user.id" class="_block main">
+					<div class="banner-container" :style="style">
+						<div ref="bannerEl" class="banner" :style="style"></div>
+						<div class="fade"></div>
+						<div class="title">
+							<MkUserName class="name" :user="user" :nowrap="true"/>
+							<div class="bottom">
+								<span class="username"><MkAcct :user="user" :detail="true"/></span>
+								<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+								<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+								<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+								<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+							</div>
+						</div>
+						<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
+						<div v-if="$i" class="actions">
+							<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
+							<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+						</div>
+					</div>
+					<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
+					<div class="title">
+						<MkUserName :user="user" :nowrap="false" class="name"/>
+						<div class="bottom">
+							<span class="username"><MkAcct :user="user" :detail="true"/></span>
+							<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+							<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+							<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+							<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+						</div>
+					</div>
+					<div class="description">
+						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+						<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
+					</div>
+					<div class="fields system">
+						<dl v-if="user.location" class="field">
+							<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
+							<dd class="value">{{ user.location }}</dd>
+						</dl>
+						<dl v-if="user.birthday" class="field">
+							<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
+							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+						</dl>
+						<dl class="field">
+							<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
+							<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
+						</dl>
+					</div>
+					<div v-if="user.fields.length > 0" class="fields">
+						<dl v-for="(field, i) in user.fields" :key="i" class="field">
+							<dt class="name">
+								<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+							</dt>
+							<dd class="value">
+								<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
+							</dd>
+						</dl>
+					</div>
+					<div class="status">
+						<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
+							<b>{{ number(user.notesCount) }}</b>
+							<span>{{ $ts.notes }}</span>
+						</MkA>
+						<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
+							<b>{{ number(user.followingCount) }}</b>
+							<span>{{ $ts.following }}</span>
+						</MkA>
+						<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
+							<b>{{ number(user.followersCount) }}</b>
+							<span>{{ $ts.followers }}</span>
+						</MkA>
+					</div>
+				</div>
+			</div>
+
+			<div class="contents">
+				<div v-if="user.pinnedNotes.length > 0" class="_gap">
+					<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true"/>
+				</div>
+				<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
+				<template v-if="narrow">
+					<XPhotos :key="user.id" :user="user"/>
+					<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+				</template>
+			</div>
+			<div>
+				<XUserTimeline :user="user"/>
+			</div>
+		</div>
+		<div v-if="!narrow" class="sub">
+			<XPhotos :key="user.id" :user="user"/>
+			<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+		</div>
+	</div>
+</MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue';
+import calcAge from 's-age';
+import * as misskey from 'misskey-js';
+import XUserTimeline from './index.timeline.vue';
+import XNote from '@/components/note.vue';
+import MkFollowButton from '@/components/follow-button.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkRemoteCaution from '@/components/remote-caution.vue';
+import MkTab from '@/components/tab.vue';
+import MkInfo from '@/components/ui/info.vue';
+import { getScrollPosition } from '@/scripts/scroll';
+import { getUserMenu } from '@/scripts/get-user-menu';
+import number from '@/filters/number';
+import { userPage, acct as getAcct } from '@/filters/user';
+import * as os from '@/os';
+import { useRouter } from '@/router';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
+
+const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
+const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
+
+const props = withDefaults(defineProps<{
+	user: misskey.entities.UserDetailed;
+}>(), {
+});
+
+const router = useRouter();
+
+let parallaxAnimationId = $ref<null | number>(null);
+let narrow = $ref<null | boolean>(null);
+let rootEl = $ref<null | HTMLElement>(null);
+let bannerEl = $ref<null | HTMLElement>(null);
+
+const style = $computed(() => {
+	if (props.user.bannerUrl == null) return {};
+	return {
+		backgroundImage: `url(${ props.user.bannerUrl })`,
+	};
+});
+
+const age = $computed(() => {
+	return calcAge(props.user.birthday);
+});
+
+function menu(ev) {
+	os.popupMenu(getUserMenu(props.user), ev.currentTarget ?? ev.target);
+}
+
+function parallaxLoop() {
+	parallaxAnimationId = window.requestAnimationFrame(parallaxLoop);
+	parallax();
+}
+
+function parallax() {
+	const banner = bannerEl as any;
+	if (banner == null) return;
+
+	const top = getScrollPosition(rootEl);
+
+	if (top < 0) return;
+
+	const z = 1.75; // 奥行き(小さいほど奥)
+	const pos = -(top / z);
+	banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
+}
+
+onMounted(() => {
+	window.requestAnimationFrame(parallaxLoop);
+	narrow = rootEl!.clientWidth < 1000;
+});
+
+onUnmounted(() => {
+	if (parallaxAnimationId) {
+		window.cancelAnimationFrame(parallaxAnimationId);
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ftskorzw {
+
+	> .main {
+
+		> .punished {
+			font-size: 0.8em;
+			padding: 16px;
+		}
+
+		> .profile {
+
+			> .main {
+				position: relative;
+				overflow: hidden;
+
+				> .banner-container {
+					position: relative;
+					height: 250px;
+					overflow: hidden;
+					background-size: cover;
+					background-position: center;
+
+					> .banner {
+						height: 100%;
+						background-color: #4c5e6d;
+						background-size: cover;
+						background-position: center;
+						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
+						will-change: background-position;
+					}
+
+					> .fade {
+						position: absolute;
+						bottom: 0;
+						left: 0;
+						width: 100%;
+						height: 78px;
+						background: linear-gradient(transparent, rgba(#000, 0.7));
+					}
+
+					> .followed {
+						position: absolute;
+						top: 12px;
+						left: 12px;
+						padding: 4px 8px;
+						color: #fff;
+						background: rgba(0, 0, 0, 0.7);
+						font-size: 0.7em;
+						border-radius: 6px;
+					}
+
+					> .actions {
+						position: absolute;
+						top: 12px;
+						right: 12px;
+						-webkit-backdrop-filter: var(--blur, blur(8px));
+						backdrop-filter: var(--blur, blur(8px));
+						background: rgba(0, 0, 0, 0.2);
+						padding: 8px;
+						border-radius: 24px;
+
+						> .menu {
+							vertical-align: bottom;
+							height: 31px;
+							width: 31px;
+							color: #fff;
+							text-shadow: 0 0 8px #000;
+							font-size: 16px;
+						}
+
+						> .koudoku {
+							margin-left: 4px;
+							vertical-align: bottom;
+						}
+					}
+
+					> .title {
+						position: absolute;
+						bottom: 0;
+						left: 0;
+						width: 100%;
+						padding: 0 0 8px 154px;
+						box-sizing: border-box;
+						color: #fff;
+
+						> .name {
+							display: block;
+							margin: 0;
+							line-height: 32px;
+							font-weight: bold;
+							font-size: 1.8em;
+							text-shadow: 0 0 8px #000;
+						}
+
+						> .bottom {
+							> * {
+								display: inline-block;
+								margin-right: 16px;
+								line-height: 20px;
+								opacity: 0.8;
+
+								&.username {
+									font-weight: bold;
+								}
+							}
+						}
+					}
+				}
+
+				> .title {
+					display: none;
+					text-align: center;
+					padding: 50px 8px 16px 8px;
+					font-weight: bold;
+					border-bottom: solid 0.5px var(--divider);
+
+					> .bottom {
+						> * {
+							display: inline-block;
+							margin-right: 8px;
+							opacity: 0.8;
+						}
+					}
+				}
+
+				> .avatar {
+					display: block;
+					position: absolute;
+					top: 170px;
+					left: 16px;
+					z-index: 2;
+					width: 120px;
+					height: 120px;
+					box-shadow: 1px 1px 3px rgba(#000, 0.2);
+				}
+
+				> .description {
+					padding: 24px 24px 24px 154px;
+					font-size: 0.95em;
+
+					> .empty {
+						margin: 0;
+						opacity: 0.5;
+					}
+				}
+
+				> .fields {
+					padding: 24px;
+					font-size: 0.9em;
+					border-top: solid 0.5px var(--divider);
+
+					> .field {
+						display: flex;
+						padding: 0;
+						margin: 0;
+						align-items: center;
+
+						&:not(:last-child) {
+							margin-bottom: 8px;
+						}
+
+						> .name {
+							width: 30%;
+							overflow: hidden;
+							white-space: nowrap;
+							text-overflow: ellipsis;
+							font-weight: bold;
+							text-align: center;
+						}
+
+						> .value {
+							width: 70%;
+							overflow: hidden;
+							white-space: nowrap;
+							text-overflow: ellipsis;
+							margin: 0;
+						}
+					}
+
+					&.system > .field > .name {
+					}
+				}
+
+				> .status {
+					display: flex;
+					padding: 24px;
+					border-top: solid 0.5px var(--divider);
+
+					> a {
+						flex: 1;
+						text-align: center;
+
+						&.active {
+							color: var(--accent);
+						}
+
+						&:hover {
+							text-decoration: none;
+						}
+
+						> b {
+							display: block;
+							line-height: 16px;
+						}
+
+						> span {
+							font-size: 70%;
+						}
+					}
+				}
+			}
+		}
+
+		> .contents {
+			> .content {
+				margin-bottom: var(--margin);
+			}
+		}
+	}
+
+	&.max-width_500px {
+		> .main {
+			> .profile > .main {
+				> .banner-container {
+					height: 140px;
+
+					> .fade {
+						display: none;
+					}
+
+					> .title {
+						display: none;
+					}
+				}
+
+				> .title {
+					display: block;
+				}
+
+				> .avatar {
+					top: 90px;
+					left: 0;
+					right: 0;
+					width: 92px;
+					height: 92px;
+					margin: auto;
+				}
+
+				> .description {
+					padding: 16px;
+					text-align: center;
+				}
+
+				> .fields {
+					padding: 16px;
+				}
+
+				> .status {
+					padding: 16px;
+				}
+			}
+
+			> .contents {
+				> .nav {
+					font-size: 80%;
+				}
+			}
+		}
+	}
+
+	&.wide {
+		display: flex;
+		width: 100%;
+
+		> .main {
+			width: 100%;
+			min-width: 0;
+		}
+
+		> .sub {
+			max-width: 350px;
+			min-width: 350px;
+			margin-left: var(--margin);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
index 7b2e2cde1..bd1bb11a5 100644
--- a/packages/client/src/pages/user/index.vue
+++ b/packages/client/src/pages/user/index.vue
@@ -1,124 +1,15 @@
 <template>
 <MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<div ref="rootEl">
+	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
+	<div>
 		<transition name="fade" mode="out-in">
-			<MkSpacer v-if="user" :content-max="narrow ? 800 : 1100">
-				<div v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }">
-					<div class="main">
-						<!-- TODO -->
-						<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
-						<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
-
-						<div class="profile">
-							<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
-
-							<div :key="user.id" class="_block main">
-								<div class="banner-container" :style="style">
-									<div ref="bannerEl" class="banner" :style="style"></div>
-									<div class="fade"></div>
-									<div class="title">
-										<MkUserName class="name" :user="user" :nowrap="true"/>
-										<div class="bottom">
-											<span class="username"><MkAcct :user="user" :detail="true"/></span>
-											<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
-											<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
-											<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
-											<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
-										</div>
-									</div>
-									<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
-									<div v-if="$i" class="actions">
-										<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
-										<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
-									</div>
-								</div>
-								<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
-								<div class="title">
-									<MkUserName :user="user" :nowrap="false" class="name"/>
-									<div class="bottom">
-										<span class="username"><MkAcct :user="user" :detail="true"/></span>
-										<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
-										<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
-										<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
-										<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
-									</div>
-								</div>
-								<div class="description">
-									<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
-									<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
-								</div>
-								<div class="fields system">
-									<dl v-if="user.location" class="field">
-										<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
-										<dd class="value">{{ user.location }}</dd>
-									</dl>
-									<dl v-if="user.birthday" class="field">
-										<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
-										<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
-									</dl>
-									<dl class="field">
-										<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
-										<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
-									</dl>
-								</div>
-								<div v-if="user.fields.length > 0" class="fields">
-									<dl v-for="(field, i) in user.fields" :key="i" class="field">
-										<dt class="name">
-											<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
-										</dt>
-										<dd class="value">
-											<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
-										</dd>
-									</dl>
-								</div>
-								<div class="status">
-									<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
-										<b>{{ number(user.notesCount) }}</b>
-										<span>{{ $ts.notes }}</span>
-									</MkA>
-									<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
-										<b>{{ number(user.followingCount) }}</b>
-										<span>{{ $ts.following }}</span>
-									</MkA>
-									<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
-										<b>{{ number(user.followersCount) }}</b>
-										<span>{{ $ts.followers }}</span>
-									</MkA>
-								</div>
-							</div>
-						</div>
-
-						<div class="contents">
-							<template v-if="page === 'index'">
-								<div>
-									<div v-if="user.pinnedNotes.length > 0" class="_gap">
-										<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true"/>
-									</div>
-									<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
-									<template v-if="narrow">
-										<XPhotos :key="user.id" :user="user"/>
-										<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
-									</template>
-								</div>
-								<div>
-									<XUserTimeline :user="user"/>
-								</div>
-							</template>
-							<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
-							<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
-							<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
-							<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
-							<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
-							<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
-						</div>
-					</div>
-					<div v-if="!narrow" class="sub">
-						<XPhotos :key="user.id" :user="user"/>
-						<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
-					</div>
-				</div>
-			</MkSpacer>
+			<div v-if="user">
+				<XHome v-if="tab === 'home'" :user="user"/>
+				<XReactions v-else-if="tab === 'reactions'" :user="user"/>
+				<XClips v-else-if="tab === 'clips'" :user="user"/>
+				<XPages v-else-if="tab === 'pages'" :user="user"/>
+				<XGallery v-else-if="tab === 'gallery'" :user="user"/>
+			</div>
 			<MkError v-else-if="error" @retry="fetch()"/>
 			<MkLoading v-else/>
 		</transition>
@@ -131,14 +22,6 @@ import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch }
 import calcAge from 's-age';
 import * as Acct from 'misskey-js/built/acct';
 import * as misskey from 'misskey-js';
-import XUserTimeline from './index.timeline.vue';
-import XNote from '@/components/note.vue';
-import MkFollowButton from '@/components/follow-button.vue';
-import MkContainer from '@/components/ui/container.vue';
-import MkFolder from '@/components/ui/folder.vue';
-import MkRemoteCaution from '@/components/remote-caution.vue';
-import MkTab from '@/components/tab.vue';
-import MkInfo from '@/components/ui/info.vue';
 import { getScrollPosition } from '@/scripts/scroll';
 import { getUserMenu } from '@/scripts/get-user-menu';
 import number from '@/filters/number';
@@ -149,41 +32,24 @@ import { definePageMetadata } from '@/scripts/page-metadata';
 import { i18n } from '@/i18n';
 import { $i } from '@/account';
 
-const XFollowList = defineAsyncComponent(() => import('./follow-list.vue'));
+const XHome = defineAsyncComponent(() => import('./home.vue'));
 const XReactions = defineAsyncComponent(() => import('./reactions.vue'));
 const XClips = defineAsyncComponent(() => import('./clips.vue'));
 const XPages = defineAsyncComponent(() => import('./pages.vue'));
 const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
-const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
-const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
 
 const props = withDefaults(defineProps<{
 	acct: string;
 	page?: string;
 }>(), {
-	page: 'index',
+	page: 'home',
 });
 
 const router = useRouter();
 
+let tab = $ref(props.page);
 let user = $ref<null | misskey.entities.UserDetailed>(null);
 let error = $ref(null);
-let parallaxAnimationId = $ref<null | number>(null);
-let narrow = $ref<null | boolean>(null);
-let rootEl = $ref<null | HTMLElement>(null);
-let bannerEl = $ref<null | HTMLElement>(null);
-
-const style = $computed(() => {
-	if (user?.bannerUrl == null) return {};
-	return {
-		backgroundImage: `url(${ user.bannerUrl })`,
-	};
-});
-
-const age = $computed(() => {
-	if (user == null) return null;
-	return calcAge(user.birthday);
-});
 
 function fetchUser(): void {
 	if (props.acct == null) return;
@@ -203,62 +69,28 @@ function menu(ev) {
 	os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
 }
 
-function parallaxLoop() {
-	parallaxAnimationId = window.requestAnimationFrame(parallaxLoop);
-	parallax();
-}
-
-function parallax() {
-	const banner = bannerEl as any;
-	if (banner == null) return;
-
-	const top = getScrollPosition(rootEl);
-
-	if (top < 0) return;
-
-	const z = 1.75; // 奥行き(小さいほど奥)
-	const pos = -(top / z);
-	banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
-}
-
-onMounted(() => {
-	window.requestAnimationFrame(parallaxLoop);
-	narrow = rootEl!.clientWidth < 1000;
-});
-
-onUnmounted(() => {
-	if (parallaxAnimationId) {
-		window.cancelAnimationFrame(parallaxAnimationId);
-	}
-});
-
 const headerActions = $computed(() => []);
 
 const headerTabs = $computed(() => user ? [{
-	active: props.page === 'index',
+	key: 'home',
 	title: i18n.ts.overview,
 	icon: 'fas fa-home',
-	onClick: () => { router.push('/@' + getAcct(user)); },
 }, ...($i && ($i.id === user.id)) || user.publicReactions ? [{
-	active: props.page === 'reactions',
+	key: 'reactions',
 	title: i18n.ts.reaction,
 	icon: 'fas fa-laugh',
-	onClick: () => { router.push('/@' + getAcct(user) + '/reactions'); },
 }] : [], {
-	active: props.page === 'clips',
+	key: 'clips',
 	title: i18n.ts.clips,
 	icon: 'fas fa-paperclip',
-	onClick: () => { router.push('/@' + getAcct(user) + '/clips'); },
 }, {
-	active: props.page === 'pages',
+	key: 'pages',
 	title: i18n.ts.pages,
 	icon: 'fas fa-file-alt',
-	onClick: () => { router.push('/@' + getAcct(user) + '/pages'); },
 }, {
-	active: props.page === 'gallery',
+	key: 'gallery',
 	title: i18n.ts.gallery,
 	icon: 'fas fa-icons',
-	onClick: () => { router.push('/@' + getAcct(user) + '/gallery'); },
 }] : null);
 
 definePageMetadata(computed(() => user ? {
@@ -284,291 +116,4 @@ definePageMetadata(computed(() => user ? {
 .fade-leave-to {
 	opacity: 0;
 }
-
-.ftskorzw {
-
-	> .main {
-
-		> .punished {
-			font-size: 0.8em;
-			padding: 16px;
-		}
-
-		> .profile {
-
-			> .main {
-				position: relative;
-				overflow: hidden;
-
-				> .banner-container {
-					position: relative;
-					height: 250px;
-					overflow: hidden;
-					background-size: cover;
-					background-position: center;
-
-					> .banner {
-						height: 100%;
-						background-color: #4c5e6d;
-						background-size: cover;
-						background-position: center;
-						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
-						will-change: background-position;
-					}
-
-					> .fade {
-						position: absolute;
-						bottom: 0;
-						left: 0;
-						width: 100%;
-						height: 78px;
-						background: linear-gradient(transparent, rgba(#000, 0.7));
-					}
-
-					> .followed {
-						position: absolute;
-						top: 12px;
-						left: 12px;
-						padding: 4px 8px;
-						color: #fff;
-						background: rgba(0, 0, 0, 0.7);
-						font-size: 0.7em;
-						border-radius: 6px;
-					}
-
-					> .actions {
-						position: absolute;
-						top: 12px;
-						right: 12px;
-						-webkit-backdrop-filter: var(--blur, blur(8px));
-						backdrop-filter: var(--blur, blur(8px));
-						background: rgba(0, 0, 0, 0.2);
-						padding: 8px;
-						border-radius: 24px;
-
-						> .menu {
-							vertical-align: bottom;
-							height: 31px;
-							width: 31px;
-							color: #fff;
-							text-shadow: 0 0 8px #000;
-							font-size: 16px;
-						}
-
-						> .koudoku {
-							margin-left: 4px;
-							vertical-align: bottom;
-						}
-					}
-
-					> .title {
-						position: absolute;
-						bottom: 0;
-						left: 0;
-						width: 100%;
-						padding: 0 0 8px 154px;
-						box-sizing: border-box;
-						color: #fff;
-
-						> .name {
-							display: block;
-							margin: 0;
-							line-height: 32px;
-							font-weight: bold;
-							font-size: 1.8em;
-							text-shadow: 0 0 8px #000;
-						}
-
-						> .bottom {
-							> * {
-								display: inline-block;
-								margin-right: 16px;
-								line-height: 20px;
-								opacity: 0.8;
-
-								&.username {
-									font-weight: bold;
-								}
-							}
-						}
-					}
-				}
-
-				> .title {
-					display: none;
-					text-align: center;
-					padding: 50px 8px 16px 8px;
-					font-weight: bold;
-					border-bottom: solid 0.5px var(--divider);
-
-					> .bottom {
-						> * {
-							display: inline-block;
-							margin-right: 8px;
-							opacity: 0.8;
-						}
-					}
-				}
-
-				> .avatar {
-					display: block;
-					position: absolute;
-					top: 170px;
-					left: 16px;
-					z-index: 2;
-					width: 120px;
-					height: 120px;
-					box-shadow: 1px 1px 3px rgba(#000, 0.2);
-				}
-
-				> .description {
-					padding: 24px 24px 24px 154px;
-					font-size: 0.95em;
-
-					> .empty {
-						margin: 0;
-						opacity: 0.5;
-					}
-				}
-
-				> .fields {
-					padding: 24px;
-					font-size: 0.9em;
-					border-top: solid 0.5px var(--divider);
-
-					> .field {
-						display: flex;
-						padding: 0;
-						margin: 0;
-						align-items: center;
-
-						&:not(:last-child) {
-							margin-bottom: 8px;
-						}
-
-						> .name {
-							width: 30%;
-							overflow: hidden;
-							white-space: nowrap;
-							text-overflow: ellipsis;
-							font-weight: bold;
-							text-align: center;
-						}
-
-						> .value {
-							width: 70%;
-							overflow: hidden;
-							white-space: nowrap;
-							text-overflow: ellipsis;
-							margin: 0;
-						}
-					}
-
-					&.system > .field > .name {
-					}
-				}
-
-				> .status {
-					display: flex;
-					padding: 24px;
-					border-top: solid 0.5px var(--divider);
-
-					> a {
-						flex: 1;
-						text-align: center;
-
-						&.active {
-							color: var(--accent);
-						}
-
-						&:hover {
-							text-decoration: none;
-						}
-
-						> b {
-							display: block;
-							line-height: 16px;
-						}
-
-						> span {
-							font-size: 70%;
-						}
-					}
-				}
-			}
-		}
-
-		> .contents {
-			> .content {
-				margin-bottom: var(--margin);
-			}
-		}
-	}
-
-	&.max-width_500px {
-		> .main {
-			> .profile > .main {
-				> .banner-container {
-					height: 140px;
-
-					> .fade {
-						display: none;
-					}
-
-					> .title {
-						display: none;
-					}
-				}
-
-				> .title {
-					display: block;
-				}
-
-				> .avatar {
-					top: 90px;
-					left: 0;
-					right: 0;
-					width: 92px;
-					height: 92px;
-					margin: auto;
-				}
-
-				> .description {
-					padding: 16px;
-					text-align: center;
-				}
-
-				> .fields {
-					padding: 16px;
-				}
-
-				> .status {
-					padding: 16px;
-				}
-			}
-
-			> .contents {
-				> .nav {
-					font-size: 80%;
-				}
-			}
-		}
-	}
-
-	&.wide {
-		display: flex;
-		width: 100%;
-
-		> .main {
-			width: 100%;
-			min-width: 0;
-		}
-
-		> .sub {
-			max-width: 350px;
-			min-width: 350px;
-			margin-left: var(--margin);
-		}
-	}
-}
 </style>
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index 1197976d5..828708309 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -12,15 +12,21 @@ const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
 });
 
 export const routes = [{
-	name: 'user',
-	path: '/@:acct/:page?',
-	component: page(() => import('./pages/user/index.vue')),
-}, {
 	path: '/@:initUser/pages/:initPageName/view-source',
 	component: page(() => import('./pages/page-editor/page-editor.vue')),
 }, {
 	path: '/@:username/pages/:pageName',
 	component: page(() => import('./pages/page.vue')),
+}, {
+	path: '/@:acct/following',
+	component: page(() => import('./pages/user/following.vue')),
+}, {
+	path: '/@:acct/followers',
+	component: page(() => import('./pages/user/followers.vue')),
+}, {
+	name: 'user',
+	path: '/@:acct/:page?',
+	component: page(() => import('./pages/user/index.vue')),
 }, {
 	name: 'note',
 	path: '/notes/:noteId',

From 307533c29df036d9d1a209ca1b94c6a545c347dd Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 23:33:47 +0900
Subject: [PATCH 045/100] =?UTF-8?q?enhance(client):=20=E3=83=8F=E3=82=A4?=
 =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=88=E3=82=92=E3=81=BF=E3=81=A4=E3=81=91?=
 =?UTF-8?q?=E3=82=8B=E3=81=AB=E7=B5=B1=E5=90=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                  |   5 +
 packages/client/src/menu.ts                   |   5 -
 .../client/src/pages/explore.featured.vue     |  16 ++
 packages/client/src/pages/explore.users.vue   | 143 +++++++++++
 packages/client/src/pages/explore.vue         | 223 +++---------------
 packages/client/src/pages/featured.vue        |  26 --
 packages/client/src/router.ts                 |   3 -
 7 files changed, 202 insertions(+), 219 deletions(-)
 create mode 100644 packages/client/src/pages/explore.featured.vue
 create mode 100644 packages/client/src/pages/explore.users.vue
 delete mode 100644 packages/client/src/pages/featured.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c08133aaf..df3f477ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,11 @@ You should also include the user name that made the change.
 
 ## 12.x.x (unreleased)
 
+### Changes
+- ハイライトがみつけるに統合されました
+- カスタム絵文字ページはインスタンス情報ページに統合されました
+- 連合ページはインスタンス情報ページに統合されました
+
 ### Improvements
 - Server: Allow GET method for some endpoints @syuilo
 - Server: Add rate limit to i/notifications @tamaina
diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
index 72e395160..677296a6f 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -35,11 +35,6 @@ export const menuDef = reactive({
 		indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
 		to: '/my/follow-requests',
 	},
-	featured: {
-		title: 'featured',
-		icon: 'fas fa-fire-alt',
-		to: '/featured',
-	},
 	explore: {
 		title: 'explore',
 		icon: 'fas fa-hashtag',
diff --git a/packages/client/src/pages/explore.featured.vue b/packages/client/src/pages/explore.featured.vue
new file mode 100644
index 000000000..12de9e412
--- /dev/null
+++ b/packages/client/src/pages/explore.featured.vue
@@ -0,0 +1,16 @@
+<template>
+<MkSpacer :content-max="800">
+	<XNotes ref="notes" :pagination="pagination"/>
+</MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import XNotes from '@/components/notes.vue';
+import { i18n } from '@/i18n';
+
+const pagination = {
+	endpoint: 'notes/featured' as const,
+	limit: 10,
+	offsetMode: true,
+};
+</script>
diff --git a/packages/client/src/pages/explore.users.vue b/packages/client/src/pages/explore.users.vue
new file mode 100644
index 000000000..bdc96b33a
--- /dev/null
+++ b/packages/client/src/pages/explore.users.vue
@@ -0,0 +1,143 @@
+<template>
+<MkSpacer :content-max="1200">
+	<div v-if="origin === 'local'">
+		<template v-if="tag == null">
+			<MkFolder class="_gap" persist-key="explore-pinned-users">
+				<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
+				<XUserList :pagination="pinnedUsers"/>
+			</MkFolder>
+			<MkFolder class="_gap" persist-key="explore-popular-users">
+				<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
+				<XUserList :pagination="popularUsers"/>
+			</MkFolder>
+			<MkFolder class="_gap" persist-key="explore-recently-updated-users">
+				<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
+				<XUserList :pagination="recentlyUpdatedUsers"/>
+			</MkFolder>
+			<MkFolder class="_gap" persist-key="explore-recently-registered-users">
+				<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
+				<XUserList :pagination="recentlyRegisteredUsers"/>
+			</MkFolder>
+		</template>
+	</div>
+	<div v-else>
+		<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
+			<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
+
+			<div class="vxjfqztj">
+				<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
+				<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA>
+			</div>
+		</MkFolder>
+
+		<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
+			<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
+			<XUserList :pagination="tagUsers"/>
+		</MkFolder>
+
+		<template v-if="tag == null">
+			<MkFolder class="_gap">
+				<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
+				<XUserList :pagination="popularUsersF"/>
+			</MkFolder>
+			<MkFolder class="_gap">
+				<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
+				<XUserList :pagination="recentlyUpdatedUsersF"/>
+			</MkFolder>
+			<MkFolder class="_gap">
+				<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
+				<XUserList :pagination="recentlyRegisteredUsersF"/>
+			</MkFolder>
+		</template>
+	</div>
+</MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import { computed, watch } from 'vue';
+import XUserList from '@/components/user-list.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import number from '@/filters/number';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+
+const props = defineProps<{
+	origin: 'local' | 'remote';
+	tag?: string;
+}>();
+
+let tagsEl = $ref<InstanceType<typeof MkFolder>>();
+let tagsLocal = $ref([]);
+let tagsRemote = $ref([]);
+
+watch(() => props.tag, () => {
+	if (tagsEl) tagsEl.toggleContent(props.tag == null);
+});
+
+const tagUsers = $computed(() => ({
+	endpoint: 'hashtags/users' as const,
+	limit: 30,
+	params: {
+		tag: props.tag,
+		origin: 'combined',
+		sort: '+follower',
+	},
+}));
+
+const pinnedUsers = { endpoint: 'pinned-users' };
+const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
+	state: 'alive',
+	origin: 'local',
+	sort: '+follower',
+} };
+const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
+	origin: 'local',
+	sort: '+updatedAt',
+} };
+const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
+	origin: 'local',
+	state: 'alive',
+	sort: '+createdAt',
+} };
+const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
+	state: 'alive',
+	origin: 'remote',
+	sort: '+follower',
+} };
+const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
+	origin: 'combined',
+	sort: '+updatedAt',
+} };
+const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
+	origin: 'combined',
+	sort: '+createdAt',
+} };
+
+os.api('hashtags/list', {
+	sort: '+attachedLocalUsers',
+	attachedToLocalUserOnly: true,
+	limit: 30,
+}).then(tags => {
+	tagsLocal = tags;
+});
+os.api('hashtags/list', {
+	sort: '+attachedRemoteUsers',
+	attachedToRemoteUserOnly: true,
+	limit: 30,
+}).then(tags => {
+	tagsRemote = tags;
+});
+</script>
+
+<style lang="scss" scoped>
+.vxjfqztj {
+	> * {
+		margin-right: 16px;
+
+		&.local {
+			font-weight: bold;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue
index cd0dba781..c59fb639d 100644
--- a/packages/client/src/pages/explore.vue
+++ b/packages/client/src/pages/explore.vue
@@ -1,90 +1,39 @@
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
-	<MkSpacer :content-max="1200">
-		<div class="lznhrdub">
-			<div v-if="tab === 'local'">
-				<div v-if="instance && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: instance.bannerUrl ? `url(${instance.bannerUrl})` : null }">
-					<header><span>{{ $t('explore', { host: instance.name || 'Misskey' }) }}</span></header>
-					<div><span>{{ $t('exploreUsersCount', { count: number(stats.originalUsersCount) }) }}</span></div>
-				</div>
-
-				<template v-if="tag == null">
-					<MkFolder class="_gap" persist-key="explore-pinned-users">
-						<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
-						<XUserList :pagination="pinnedUsers"/>
-					</MkFolder>
-					<MkFolder class="_gap" persist-key="explore-popular-users">
-						<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
-						<XUserList :pagination="popularUsers"/>
-					</MkFolder>
-					<MkFolder class="_gap" persist-key="explore-recently-updated-users">
-						<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
-						<XUserList :pagination="recentlyUpdatedUsers"/>
-					</MkFolder>
-					<MkFolder class="_gap" persist-key="explore-recently-registered-users">
-						<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
-						<XUserList :pagination="recentlyRegisteredUsers"/>
-					</MkFolder>
-				</template>
-			</div>
-			<div v-else-if="tab === 'remote'">
-				<div v-if="tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: `url(/client-assets/fedi.jpg)` }">
-					<header><span>{{ $ts.exploreFediverse }}</span></header>
-				</div>
-
-				<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
-					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
-
-					<div class="vxjfqztj">
-						<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
-						<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA>
-					</div>
-				</MkFolder>
-
-				<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
-					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
-					<XUserList :pagination="tagUsers"/>
-				</MkFolder>
-
-				<template v-if="tag == null">
-					<MkFolder class="_gap">
-						<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
-						<XUserList :pagination="popularUsersF"/>
-					</MkFolder>
-					<MkFolder class="_gap">
-						<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
-						<XUserList :pagination="recentlyUpdatedUsersF"/>
-					</MkFolder>
-					<MkFolder class="_gap">
-						<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
-						<XUserList :pagination="recentlyRegisteredUsersF"/>
-					</MkFolder>
-				</template>
-			</div>
-			<div v-else-if="tab === 'search'">
-				<div class="_isolated">
-					<MkInput v-model="searchQuery" :debounce="true" type="search">
-						<template #prefix><i class="fas fa-search"></i></template>
-						<template #label>{{ $ts.searchUser }}</template>
-					</MkInput>
-					<MkRadios v-model="searchOrigin">
-						<option value="combined">{{ $ts.all }}</option>
-						<option value="local">{{ $ts.local }}</option>
-						<option value="remote">{{ $ts.remote }}</option>
-					</MkRadios>
-				</div>
-
-				<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
-			</div>
+	<div class="lznhrdub">
+		<div v-if="tab === 'featured'">
+			<XFeatured/>
 		</div>
-	</MkSpacer>
+		<div v-else-if="tab === 'localUsers'">
+			<XUsers origin="local"/>
+		</div>
+		<div v-else-if="tab === 'remoteUsers'">
+			<XUsers origin="remote"/>
+		</div>
+		<div v-else-if="tab === 'search'">
+			<div class="_isolated">
+				<MkInput v-model="searchQuery" :debounce="true" type="search">
+					<template #prefix><i class="fas fa-search"></i></template>
+					<template #label>{{ $ts.searchUser }}</template>
+				</MkInput>
+				<MkRadios v-model="searchOrigin">
+					<option value="combined">{{ $ts.all }}</option>
+					<option value="local">{{ $ts.local }}</option>
+					<option value="remote">{{ $ts.remote }}</option>
+				</MkRadios>
+			</div>
+
+			<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
+		</div>
+	</div>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { computed, defineComponent, watch } from 'vue';
-import XUserList from '@/components/user-list.vue';
+import { computed, watch } from 'vue';
+import XFeatured from './explore.featured.vue';
+import XUsers from './explore.users.vue';
 import MkFolder from '@/components/ui/folder.vue';
 import MkInput from '@/components/form/input.vue';
 import MkRadios from '@/components/form/radios.vue';
@@ -98,11 +47,8 @@ const props = defineProps<{
 	tag?: string;
 }>();
 
-let tab = $ref('local');
+let tab = $ref('featured');
 let tagsEl = $ref<InstanceType<typeof MkFolder>>();
-let tagsLocal = $ref([]);
-let tagsRemote = $ref([]);
-let stats = $ref(null);
 let searchQuery = $ref(null);
 let searchOrigin = $ref('combined');
 
@@ -110,44 +56,6 @@ watch(() => props.tag, () => {
 	if (tagsEl) tagsEl.toggleContent(props.tag == null);
 });
 
-const tagUsers = $computed(() => ({
-	endpoint: 'hashtags/users' as const,
-	limit: 30,
-	params: {
-		tag: props.tag,
-		origin: 'combined',
-		sort: '+follower',
-	},
-}));
-
-const pinnedUsers = { endpoint: 'pinned-users' };
-const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
-	state: 'alive',
-	origin: 'local',
-	sort: '+follower',
-} };
-const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
-	origin: 'local',
-	sort: '+updatedAt',
-} };
-const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
-	origin: 'local',
-	state: 'alive',
-	sort: '+createdAt',
-} };
-const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
-	state: 'alive',
-	origin: 'remote',
-	sort: '+follower',
-} };
-const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
-	origin: 'combined',
-	sort: '+updatedAt',
-} };
-const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
-	origin: 'combined',
-	sort: '+createdAt',
-} };
 const searchPagination = {
 	endpoint: 'users/search' as const,
 	limit: 10,
@@ -157,31 +65,19 @@ const searchPagination = {
 	} : null),
 };
 
-os.api('hashtags/list', {
-	sort: '+attachedLocalUsers',
-	attachedToLocalUserOnly: true,
-	limit: 30,
-}).then(tags => {
-	tagsLocal = tags;
-});
-os.api('hashtags/list', {
-	sort: '+attachedRemoteUsers',
-	attachedToRemoteUserOnly: true,
-	limit: 30,
-}).then(tags => {
-	tagsRemote = tags;
-});
-os.api('stats').then(_stats => {
-	stats = _stats;
-});
-
 const headerActions = $computed(() => []);
 
 const headerTabs = $computed(() => [{
-	key: 'local',
-	title: i18n.ts.local,
+	key: 'featured',
+	icon: 'fas fa-bolt',
+	title: i18n.ts.featured,
 }, {
-	key: 'remote',
+	key: 'localUsers',
+	icon: 'fas fa-users',
+	title: i18n.ts.users,
+}, {
+	key: 'remoteUsers',
+	icon: 'fas fa-users',
 	title: i18n.ts.remote,
 }, {
 	key: 'search',
@@ -194,46 +90,3 @@ definePageMetadata(computed(() => ({
 	bg: 'var(--bg)',
 })));
 </script>
-
-<style lang="scss" scoped>
-.localfedi7 {
-	color: #fff;
-	padding: 16px;
-	height: 80px;
-	background-position: 50%;
-	background-size: cover;
-	margin-bottom: var(--margin);
-
-	> * {
-		&:not(:last-child) {
-			margin-bottom: 8px;
-		}
-
-		> span {
-			display: inline-block;
-			padding: 6px 8px;
-			background: rgba(0, 0, 0, 0.7);
-		}
-	}
-
-	> header {
-		font-size: 20px;
-		font-weight: bold;
-	}
-
-	> div {
-		font-size: 14px;
-		opacity: 0.8;
-	}
-}
-
-.vxjfqztj {
-	> * {
-		margin-right: 16px;
-
-		&.local {
-			font-weight: bold;
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue
deleted file mode 100644
index 4e3f67c76..000000000
--- a/packages/client/src/pages/featured.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-<MkStickyContainer>
-	<template #header><MkPageHeader/></template>
-	<MkSpacer :content-max="800">
-		<XNotes ref="notes" :pagination="pagination"/>
-	</MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import XNotes from '@/components/notes.vue';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-const pagination = {
-	endpoint: 'notes/featured' as const,
-	limit: 10,
-	offsetMode: true,
-};
-
-definePageMetadata({
-	title: i18n.ts.featured,
-	icon: 'fas fa-fire-alt',
-	bg: 'var(--bg)',
-});
-</script>
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index 828708309..b3baad188 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -61,9 +61,6 @@ export const routes = [{
 }, {
 	path: '/about-misskey',
 	component: page(() => import('./pages/about-misskey.vue')),
-}, {
-	path: '/featured',
-	component: page(() => import('./pages/featured.vue')),
 }, {
 	path: '/theme-editor',
 	component: page(() => import('./pages/theme-editor.vue')),

From aee19d16c65275c66d8f5e144c6e809f5b6bc657 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 1 Jul 2022 23:42:03 +0900
Subject: [PATCH 046/100] feat(client): poll highlights in explore page

---
 CHANGELOG.md                                   |  1 +
 packages/client/src/pages/explore.featured.vue | 18 ++++++++++++++++--
 2 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index df3f477ca..7ddc12399 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ You should also include the user name that made the change.
 - Client: Add instance-cloud widget @syuilo
 - Client: Add rss-marquee widget @syuilo
 - Client: Removing entries from a clip @futchitwo
+- Client: Poll highlights in explore page @syuilo
 - Make possible to delete an account by admin @syuilo
 - Improve player detection in URL preview @mei23
 - Add Badge Image to Push Notification #8012 @tamaina
diff --git a/packages/client/src/pages/explore.featured.vue b/packages/client/src/pages/explore.featured.vue
index 12de9e412..ecb68928a 100644
--- a/packages/client/src/pages/explore.featured.vue
+++ b/packages/client/src/pages/explore.featured.vue
@@ -1,16 +1,30 @@
 <template>
 <MkSpacer :content-max="800">
-	<XNotes ref="notes" :pagination="pagination"/>
+	<MkTab v-model="tab">
+		<option value="notes">{{ i18n.ts.notes }}</option>
+		<option value="polls">{{ i18n.ts.poll }}</option>
+	</MkTab>
+	<XNotes v-if="tab === 'notes'" :pagination="paginationForNotes"/>
+	<XNotes v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
 </MkSpacer>
 </template>
 
 <script lang="ts" setup>
 import XNotes from '@/components/notes.vue';
+import MkTab from '@/components/tab.vue';
 import { i18n } from '@/i18n';
 
-const pagination = {
+const paginationForNotes = {
 	endpoint: 'notes/featured' as const,
 	limit: 10,
 	offsetMode: true,
 };
+
+const paginationForPolls = {
+	endpoint: 'notes/polls/recommendation' as const,
+	limit: 10,
+	offsetMode: true,
+};
+
+let tab = $ref('notes');
 </script>

From d2e72307f68667763d2305f02c3cf5cb29068998 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 12:12:10 +0900
Subject: [PATCH 047/100] enhance(client): cache pages in page-window

---
 packages/client/src/components/page-window.vue | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue
index 7de09d3be..886f480bf 100644
--- a/packages/client/src/components/page-window.vue
+++ b/packages/client/src/components/page-window.vue
@@ -48,7 +48,10 @@ const router = new Router(routes, props.initialPath);
 
 let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
 let windowEl = $ref<InstanceType<typeof XWindow>>();
-const history = $ref<string[]>([props.initialPath]);
+const history = $ref<{ path: string; key: any; }[]>([{
+	path: router.getCurrentPath(),
+	key: router.getCurrentKey(),
+}]);
 const buttonsLeft = $computed(() => {
 	const buttons = [];
 
@@ -72,7 +75,7 @@ const buttonsRight = $computed(() => {
 });
 
 router.addListener('push', ctx => {
-	history.push(router.getCurrentPath());
+	history.push({ path: ctx.path, key: ctx.key });
 });
 
 provide('router', router);
@@ -111,7 +114,7 @@ function menu(ev) {
 
 function back() {
 	history.pop();
-	router.change(history[history.length - 1]);
+	router.change(history[history.length - 1].path, history[history.length - 1].key);
 }
 
 function close() {

From c90225ea025e55278888ede0711351e0cc104048 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 12:22:52 +0900
Subject: [PATCH 048/100] chore(client): tweak ui

---
 locales/ja-JP.yml                                      |  1 +
 .../src/server/api/endpoints/admin/show-user.ts        |  3 ++-
 packages/client/src/pages/user-info.vue                | 10 +++++++++-
 3 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1f52c2c25..b97b64dc5 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -861,6 +861,7 @@ document: "ドキュメント"
 numberOfPageCache: "ページキャッシュ数"
 numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
 logoutConfirm: "ログアウトしますか?"
+lastActiveDate: "最終利用日時"
 
 _emailUnavailable:
   used: "既に使用されています"
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 78033aed5..36384c2b3 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -25,7 +25,7 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, me) => {
 	const [user, profile] = await Promise.all([
 		Users.findOneBy({ id: ps.userId }),
-		UserProfiles.findOneBy({ userId: ps.userId })
+		UserProfiles.findOneBy({ userId: ps.userId }),
 	]);
 
 	if (user == null || profile == null) {
@@ -68,6 +68,7 @@ export default define(meta, paramDef, async (ps, me) => {
 		isModerator: user.isModerator,
 		isSilenced: user.isSilenced,
 		isSuspended: user.isSuspended,
+		lastActiveDate: user.lastActiveDate,
 		signins,
 	};
 });
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index 76b772ece..b3292290e 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -1,7 +1,7 @@
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
-	<MkSpacer :content-max="500" :margin-min="16" :margin-max="32">
+	<MkSpacer :content-max="600" :margin-min="16" :margin-max="32">
 		<FormSuspense :p="init">
 			<div v-if="tab === 'overview'" class="_formRoot">
 				<div class="_formBlock aeakzknw">
@@ -27,6 +27,14 @@
 						<template #key>ID</template>
 						<template #value><span class="_monospace">{{ user.id }}</span></template>
 					</MkKeyValue>
+					<MkKeyValue oneline style="margin: 1em 0;">
+						<template #key>{{ i18n.ts.createdAt }}</template>
+						<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
+					</MkKeyValue>
+					<MkKeyValue v-if="info" oneline style="margin: 1em 0;">
+						<template #key>{{ i18n.ts.lastActiveDate }}</template>
+						<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
+					</MkKeyValue>
 				</div>
 
 				<FormSection v-if="iAmModerator">

From cf36949256d41cdae442d4923f3c5fc63f61f3f1 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 12:34:22 +0900
Subject: [PATCH 049/100] =?UTF-8?q?enhance(server):=20=E3=82=A2=E3=83=B3?=
 =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=83=88=E3=82=92=E6=96=B0=E3=81=97=E3=81=84?=
 =?UTF-8?q?=E9=A0=86=E3=81=AB=E3=82=BD=E3=83=BC=E3=83=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../api/endpoints/notes/polls/recommendation.ts   | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
index 2150efaaf..5a04d68f3 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -60,12 +60,21 @@ export default define(meta, paramDef, async (ps, user) => {
 	query.setParameters(mutingQuery.getParameters());
 	//#endregion
 
-	const polls = await query.take(ps.limit).skip(ps.offset).getMany();
+	const polls = await query
+		.orderBy('poll.noteId', 'DESC')
+		.take(ps.limit)
+		.skip(ps.offset)
+		.getMany();
 
 	if (polls.length === 0) return [];
 
-	const notes = await Notes.findBy({
-		id: In(polls.map(poll => poll.noteId)),
+	const notes = await Notes.find({
+		where: {
+			id: In(polls.map(poll => poll.noteId)),
+		},
+		order: {
+			createdAt: 'DESC',
+		},
 	});
 
 	return await Notes.packMany(notes, user, {

From c8c7d10348075a92b37958ea868d9f9dad76c358 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 14:00:37 +0900
Subject: [PATCH 050/100] refactor(client): refactoring

---
 .../client/src/components/page-window.vue     |  1 +
 packages/client/src/pages/about-misskey.vue   |  1 -
 packages/client/src/pages/about.vue           |  1 -
 packages/client/src/pages/admin-file.vue      |  1 -
 packages/client/src/pages/admin/abuses.vue    |  1 -
 packages/client/src/pages/admin/ads.vue       |  1 -
 .../client/src/pages/admin/announcements.vue  |  1 -
 packages/client/src/pages/admin/database.vue  |  1 -
 .../client/src/pages/admin/email-settings.vue |  1 -
 packages/client/src/pages/admin/emojis.vue    |  1 -
 packages/client/src/pages/admin/files.vue     |  1 -
 packages/client/src/pages/admin/index.vue     |  1 -
 .../client/src/pages/admin/instance-block.vue |  1 -
 .../client/src/pages/admin/integrations.vue   |  1 -
 .../client/src/pages/admin/object-storage.vue |  1 -
 .../client/src/pages/admin/other-settings.vue |  1 -
 packages/client/src/pages/admin/overview.vue  |  1 -
 .../client/src/pages/admin/proxy-account.vue  |  1 -
 packages/client/src/pages/admin/queue.vue     |  1 -
 packages/client/src/pages/admin/relays.vue    |  1 -
 packages/client/src/pages/admin/security.vue  |  1 -
 packages/client/src/pages/admin/settings.vue  |  1 -
 packages/client/src/pages/admin/users.vue     |  1 -
 packages/client/src/pages/announcements.vue   |  1 -
 .../client/src/pages/antenna-timeline.vue     | 47 ++++++++++---------
 packages/client/src/pages/channel-editor.vue  |  2 -
 packages/client/src/pages/channel.vue         |  1 -
 packages/client/src/pages/channels.vue        |  1 -
 packages/client/src/pages/clip.vue            |  1 -
 packages/client/src/pages/drive.vue           |  1 -
 packages/client/src/pages/explore.vue         |  1 -
 packages/client/src/pages/favorites.vue       |  1 -
 packages/client/src/pages/follow-requests.vue |  1 -
 packages/client/src/pages/gallery/edit.vue    |  2 -
 packages/client/src/pages/gallery/index.vue   |  1 -
 packages/client/src/pages/gallery/post.vue    |  1 -
 packages/client/src/pages/instance-info.vue   |  1 -
 packages/client/src/pages/messaging/index.vue |  1 -
 .../client/src/pages/my-antennas/create.vue   |  1 -
 .../client/src/pages/my-antennas/index.vue    |  1 -
 packages/client/src/pages/my-clips/index.vue  |  1 -
 packages/client/src/pages/my-lists/index.vue  |  1 -
 packages/client/src/pages/my-lists/list.vue   |  1 -
 packages/client/src/pages/not-found.vue       |  1 -
 packages/client/src/pages/note.vue            |  1 -
 packages/client/src/pages/notifications.vue   |  1 -
 .../src/pages/page-editor/page-editor.vue     |  3 +-
 packages/client/src/pages/pages.vue           |  1 -
 packages/client/src/pages/preview.vue         |  1 -
 packages/client/src/pages/reset-password.vue  |  1 -
 packages/client/src/pages/search.vue          |  1 -
 .../client/src/pages/settings/accounts.vue    |  1 -
 packages/client/src/pages/settings/api.vue    |  1 -
 packages/client/src/pages/settings/apps.vue   |  1 -
 .../client/src/pages/settings/custom-css.vue  |  1 -
 packages/client/src/pages/settings/deck.vue   |  1 -
 .../src/pages/settings/delete-account.vue     |  1 -
 packages/client/src/pages/settings/drive.vue  |  1 -
 packages/client/src/pages/settings/email.vue  |  1 -
 .../client/src/pages/settings/general.vue     |  1 -
 .../src/pages/settings/import-export.vue      |  1 -
 packages/client/src/pages/settings/index.vue  |  1 -
 .../client/src/pages/settings/integration.vue |  1 -
 packages/client/src/pages/settings/menu.vue   |  1 -
 .../client/src/pages/settings/mute-block.vue  |  1 -
 .../src/pages/settings/notifications.vue      |  1 -
 packages/client/src/pages/settings/other.vue  |  1 -
 .../src/pages/settings/plugin.install.vue     |  1 -
 packages/client/src/pages/settings/plugin.vue |  1 -
 .../client/src/pages/settings/privacy.vue     |  1 -
 .../client/src/pages/settings/profile.vue     |  1 -
 .../client/src/pages/settings/reaction.vue    |  1 -
 .../client/src/pages/settings/security.vue    |  1 -
 packages/client/src/pages/settings/sounds.vue |  1 -
 .../src/pages/settings/theme.install.vue      |  1 -
 .../src/pages/settings/theme.manage.vue       |  1 -
 packages/client/src/pages/settings/theme.vue  |  1 -
 .../src/pages/settings/webhook.edit.vue       |  1 -
 .../client/src/pages/settings/webhook.new.vue |  1 -
 .../client/src/pages/settings/webhook.vue     |  1 -
 .../client/src/pages/settings/word-mute.vue   |  1 -
 packages/client/src/pages/tag.vue             |  1 -
 packages/client/src/pages/theme-editor.vue    |  1 -
 packages/client/src/pages/timeline.vue        |  1 -
 packages/client/src/pages/user-info.vue       |  1 -
 .../client/src/pages/user-list-timeline.vue   |  1 -
 packages/client/src/pages/user/followers.vue  |  1 -
 packages/client/src/pages/user/following.vue  |  1 -
 packages/client/src/pages/user/index.vue      |  1 -
 packages/client/src/ui/universal.vue          |  2 +-
 90 files changed, 27 insertions(+), 114 deletions(-)

diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue
index 886f480bf..5b06c7718 100644
--- a/packages/client/src/components/page-window.vue
+++ b/packages/client/src/components/page-window.vue
@@ -139,5 +139,6 @@ defineExpose({
 <style lang="scss" scoped>
 .yrolvcoq {
 	min-height: 100%;
+	background: var(--bg);
 }
 </style>
diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue
index ba85860cd..fd7b5f936 100644
--- a/packages/client/src/pages/about-misskey.vue
+++ b/packages/client/src/pages/about-misskey.vue
@@ -204,7 +204,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.aboutMisskey,
 	icon: null,
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index 683f7446b..e482c4ca6 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -131,7 +131,6 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: i18n.ts.instanceInfo,
 	icon: 'fas fa-info-circle',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/admin-file.vue b/packages/client/src/pages/admin-file.vue
index 402e9502d..7bfbed35f 100644
--- a/packages/client/src/pages/admin-file.vue
+++ b/packages/client/src/pages/admin-file.vue
@@ -117,7 +117,6 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
 	icon: 'fas fa-file',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index 2b6dadf7c..59d457dde 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -87,7 +87,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.abuseReports,
 	icon: 'fas fa-exclamation-circle',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue
index 05557469e..21feafc0b 100644
--- a/packages/client/src/pages/admin/ads.vue
+++ b/packages/client/src/pages/admin/ads.vue
@@ -116,7 +116,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.ads,
 	icon: 'fas fa-audio-description',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue
index 025897d09..5107c2f30 100644
--- a/packages/client/src/pages/admin/announcements.vue
+++ b/packages/client/src/pages/admin/announcements.vue
@@ -102,7 +102,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.announcements,
 	icon: 'fas fa-broadcast-tower',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue
index b9c5f9e39..ca8718ef6 100644
--- a/packages/client/src/pages/admin/database.vue
+++ b/packages/client/src/pages/admin/database.vue
@@ -29,6 +29,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.database,
 	icon: 'fas fa-database',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue
index c0ff94fad..46cfd3db7 100644
--- a/packages/client/src/pages/admin/email-settings.vue
+++ b/packages/client/src/pages/admin/email-settings.vue
@@ -122,6 +122,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.emailServer,
 	icon: 'fas fa-envelope',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index 868472ac9..4d847daa5 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -292,7 +292,6 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: i18n.ts.customEmojis,
 	icon: 'fas fa-laugh',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue
index 1d037fc4e..dd309180a 100644
--- a/packages/client/src/pages/admin/files.vue
+++ b/packages/client/src/pages/admin/files.vue
@@ -110,7 +110,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => ({
 	title: i18n.ts.files,
 	icon: 'fas fa-cloud',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index d967a081b..f0ac5b3fc 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -41,7 +41,6 @@ const router = useRouter();
 const indexInfo = {
 	title: i18n.ts.controlPanel,
 	icon: 'fas fa-cog',
-	bg: 'var(--bg)',
 	hideHeader: true,
 };
 
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
index 1aec151ab..6d479e8f0 100644
--- a/packages/client/src/pages/admin/instance-block.vue
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -47,6 +47,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.instanceBlocking,
 	icon: 'fas fa-ban',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue
index d407d440b..9964426a6 100644
--- a/packages/client/src/pages/admin/integrations.vue
+++ b/packages/client/src/pages/admin/integrations.vue
@@ -53,6 +53,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.integration,
 	icon: 'fas fa-share-alt',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue
index 450fd134e..5cc301853 100644
--- a/packages/client/src/pages/admin/object-storage.vue
+++ b/packages/client/src/pages/admin/object-storage.vue
@@ -144,6 +144,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.objectStorage,
 	icon: 'fas fa-cloud',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue
index 59b3503c3..ee4e8edba 100644
--- a/packages/client/src/pages/admin/other-settings.vue
+++ b/packages/client/src/pages/admin/other-settings.vue
@@ -40,6 +40,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.other,
 	icon: 'fas fa-cogs',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index f44bba27f..316cf04f1 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -468,7 +468,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.dashboard,
 	icon: 'fas fa-tachometer-alt',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue
index 0c5bb1bc9..0951d26c2 100644
--- a/packages/client/src/pages/admin/proxy-account.vue
+++ b/packages/client/src/pages/admin/proxy-account.vue
@@ -58,6 +58,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.proxyAccount,
 	icon: 'fas fa-ghost',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue
index d091fe647..6ccb464d1 100644
--- a/packages/client/src/pages/admin/queue.vue
+++ b/packages/client/src/pages/admin/queue.vue
@@ -52,6 +52,5 @@ const headerTabs = $computed(() => [{
 definePageMetadata({
 	title: i18n.ts.jobQueue,
 	icon: 'fas fa-clipboard-list',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue
index 1ca4f2df0..42347c0e7 100644
--- a/packages/client/src/pages/admin/relays.vue
+++ b/packages/client/src/pages/admin/relays.vue
@@ -78,7 +78,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.relays,
 	icon: 'fas fa-globe',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
index 65b08565c..ec11e42a7 100644
--- a/packages/client/src/pages/admin/security.vue
+++ b/packages/client/src/pages/admin/security.vue
@@ -74,6 +74,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.security,
 	icon: 'fas fa-lock',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index a5767cc2c..496eb46ea 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -258,6 +258,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.general,
 	icon: 'fas fa-cog',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
index 3b70679a4..fc2ab3ea0 100644
--- a/packages/client/src/pages/admin/users.vue
+++ b/packages/client/src/pages/admin/users.vue
@@ -135,7 +135,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => ({
 	title: i18n.ts.users,
 	icon: 'fas fa-users',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue
index 9afaa7fd1..aeb85b655 100644
--- a/packages/client/src/pages/announcements.vue
+++ b/packages/client/src/pages/announcements.vue
@@ -47,7 +47,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.announcements,
 	icon: 'fas fa-broadcast-tower',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue
index 29b6066fc..831ced4ea 100644
--- a/packages/client/src/pages/antenna-timeline.vue
+++ b/packages/client/src/pages/antenna-timeline.vue
@@ -1,17 +1,20 @@
 <template>
-<div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks">
-	<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
-	<div class="tl _block">
-		<XTimeline
-			ref="tlEl" :key="antennaId"
-			class="tl"
-			src="antenna"
-			:antenna="antennaId"
-			:sound="true"
-			@queue="queueUpdated"
-		/>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks">
+		<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
+		<div class="tl _block">
+			<XTimeline
+				ref="tlEl" :key="antennaId"
+				class="tl"
+				src="antenna"
+				:antenna="antennaId"
+				:sound="true"
+				@queue="queueUpdated"
+			/>
+		</div>
 	</div>
-</div>
+</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
@@ -68,23 +71,21 @@ watch(() => props.antennaId, async () => {
 	});
 }, { immediate: true });
 
-const headerActions = $computed(() => []);
+const headerActions = $computed(() => antenna ? [{
+	icon: 'fas fa-calendar-alt',
+	text: i18n.ts.jumpToSpecifiedDate,
+	handler: timetravel,
+}, {
+	icon: 'fas fa-cog',
+	text: i18n.ts.settings,
+	handler: settings,
+}] : []);
 
 const headerTabs = $computed(() => []);
 
 definePageMetadata(computed(() => antenna ? {
 	title: antenna.name,
 	icon: 'fas fa-satellite',
-	bg: 'var(--bg)',
-	actions: [{
-		icon: 'fas fa-calendar-alt',
-		text: i18n.ts.jumpToSpecifiedDate,
-		handler: timetravel,
-	}, {
-		icon: 'fas fa-cog',
-		text: i18n.ts.settings,
-		handler: settings,
-	}],
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue
index 2065bd668..0fa1f6951 100644
--- a/packages/client/src/pages/channel-editor.vue
+++ b/packages/client/src/pages/channel-editor.vue
@@ -111,11 +111,9 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => props.channelId ? {
 	title: i18n.ts._channel.edit,
 	icon: 'fas fa-satellite-dish',
-	bg: 'var(--bg)',
 } : {
 	title: i18n.ts._channel.create,
 	icon: 'fas fa-satellite-dish',
-	bg: 'var(--bg)',
 }));
 </script>
 
diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue
index 003ad5cce..1443a9b64 100644
--- a/packages/client/src/pages/channel.vue
+++ b/packages/client/src/pages/channel.vue
@@ -80,7 +80,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => channel ? {
 	title: channel.name,
 	icon: 'fas fa-satellite-dish',
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue
index c48a64a1e..63612bc57 100644
--- a/packages/client/src/pages/channels.vue
+++ b/packages/client/src/pages/channels.vue
@@ -75,6 +75,5 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: i18n.ts.channel,
 	icon: 'fas fa-satellite-dish',
-	bg: 'var(--bg)',
 })));
 </script>
diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue
index ce21b4c80..608e4ba7e 100644
--- a/packages/client/src/pages/clip.vue
+++ b/packages/client/src/pages/clip.vue
@@ -102,7 +102,6 @@ const headerActions = $computed(() => clip && isOwned ? [{
 definePageMetadata(computed(() => clip ? {
 	title: clip.name,
 	icon: 'fas fa-paperclip',
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue
index c7bc31135..988a1bf3d 100644
--- a/packages/client/src/pages/drive.vue
+++ b/packages/client/src/pages/drive.vue
@@ -20,7 +20,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => ({
 	title: folder ? folder.name : i18n.ts.drive,
 	icon: 'fas fa-cloud',
-	bg: 'var(--bg)',
 	hideHeader: true,
 })));
 </script>
diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue
index c59fb639d..981660cbf 100644
--- a/packages/client/src/pages/explore.vue
+++ b/packages/client/src/pages/explore.vue
@@ -87,6 +87,5 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: i18n.ts.explore,
 	icon: 'fas fa-hashtag',
-	bg: 'var(--bg)',
 })));
 </script>
diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue
index 6efca4c22..6f75d68de 100644
--- a/packages/client/src/pages/favorites.vue
+++ b/packages/client/src/pages/favorites.vue
@@ -38,7 +38,6 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 definePageMetadata({
 	title: i18n.ts.favorites,
 	icon: 'fas fa-star',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index e6f9a9a5d..f21c68c4c 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -65,7 +65,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => ({
 	title: i18n.ts.followRequests,
 	icon: 'fas fa-user-clock',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue
index 1de8328fe..f8a5d54f7 100644
--- a/packages/client/src/pages/gallery/edit.vue
+++ b/packages/client/src/pages/gallery/edit.vue
@@ -116,11 +116,9 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => props.postId ? {
 	title: i18n.ts.edit,
 	icon: 'fas fa-pencil-alt',
-	bg: 'var(--bg)',
 } : {
 	title: i18n.ts.postToGallery,
 	icon: 'fas fa-pencil-alt',
-	bg: 'var(--bg)',
 }));
 </script>
 
diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue
index 1eb6ce22f..6b406af74 100644
--- a/packages/client/src/pages/gallery/index.vue
+++ b/packages/client/src/pages/gallery/index.vue
@@ -122,7 +122,6 @@ const headerTabs = $computed(() => [{
 definePageMetadata({
 	title: i18n.ts.gallery,
 	icon: 'fas fa-icons',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index 6651c3e28..e16ccc315 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -149,7 +149,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => post ? {
 	title: post.title,
 	avatar: post.user,
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
index 83f3354df..b97ebb3e3 100644
--- a/packages/client/src/pages/instance-info.vue
+++ b/packages/client/src/pages/instance-info.vue
@@ -215,7 +215,6 @@ const headerTabs = $computed(() => [{
 definePageMetadata({
 	title: props.host,
 	icon: 'fas fa-server',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue
index bf9ac056c..7df4c846f 100644
--- a/packages/client/src/pages/messaging/index.vue
+++ b/packages/client/src/pages/messaging/index.vue
@@ -159,7 +159,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.messaging,
 	icon: 'fas fa-comments',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue
index e792834a8..dc10bece8 100644
--- a/packages/client/src/pages/my-antennas/create.vue
+++ b/packages/client/src/pages/my-antennas/create.vue
@@ -38,7 +38,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.manageAntennas,
 	icon: 'fas fa-satellite',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue
index 2cdb26031..70e444da5 100644
--- a/packages/client/src/pages/my-antennas/index.vue
+++ b/packages/client/src/pages/my-antennas/index.vue
@@ -34,7 +34,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.manageAntennas,
 	icon: 'fas fa-satellite',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
index 6434b0c00..ac5a3578f 100644
--- a/packages/client/src/pages/my-clips/index.vue
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -69,7 +69,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.clip,
 	icon: 'fas fa-paperclip',
-	bg: 'var(--bg)',
 	action: {
 		icon: 'fas fa-plus',
 		handler: create,
diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue
index 411826a95..03b638151 100644
--- a/packages/client/src/pages/my-lists/index.vue
+++ b/packages/client/src/pages/my-lists/index.vue
@@ -46,7 +46,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.manageLists,
 	icon: 'fas fa-list-ul',
-	bg: 'var(--bg)',
 	action: {
 		icon: 'fas fa-plus',
 		handler: create,
diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue
index 6e76c4a7d..5bc0bf41d 100644
--- a/packages/client/src/pages/my-lists/list.vue
+++ b/packages/client/src/pages/my-lists/list.vue
@@ -120,7 +120,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => list ? {
 	title: list.name,
 	icon: 'fas fa-list-ul',
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/not-found.vue b/packages/client/src/pages/not-found.vue
index 955fbbccf..a819cce96 100644
--- a/packages/client/src/pages/not-found.vue
+++ b/packages/client/src/pages/not-found.vue
@@ -18,6 +18,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.notFound,
 	icon: 'fas fa-exclamation-triangle',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue
index 852b82157..7c936cc62 100644
--- a/packages/client/src/pages/note.vue
+++ b/packages/client/src/pages/note.vue
@@ -132,7 +132,6 @@ definePageMetadata(computed(() => note ? {
 		title: i18n.t('noteOf', { user: note.user.name }),
 		text: note.text,
 	},
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue
index 3df1a3f17..acf338c2c 100644
--- a/packages/client/src/pages/notifications.vue
+++ b/packages/client/src/pages/notifications.vue
@@ -91,6 +91,5 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: i18n.ts.notifications,
 	icon: 'fas fa-bell',
-	bg: 'var(--bg)',
 })));
 </script>
diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue
index c38286c1d..3ce48e89f 100644
--- a/packages/client/src/pages/page-editor/page-editor.vue
+++ b/packages/client/src/pages/page-editor/page-editor.vue
@@ -439,8 +439,7 @@ definePageMetadata(computed(() => {
 	return {
 		title: title,
 		icon: 'fas fa-pencil-alt',
-		bg: 'var(--bg)',
-	};
+		};
 }));
 </script>
 
diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue
index 16aeae2f5..62c675e41 100644
--- a/packages/client/src/pages/pages.vue
+++ b/packages/client/src/pages/pages.vue
@@ -77,7 +77,6 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: i18n.ts.pages,
 	icon: 'fas fa-sticky-note',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue
index cba7589a3..8f211081d 100644
--- a/packages/client/src/pages/preview.vue
+++ b/packages/client/src/pages/preview.vue
@@ -17,7 +17,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => ({
 	title: i18n.ts.preview,
 	icon: 'fas fa-eye',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue
index 39a1191ca..10c41f2d2 100644
--- a/packages/client/src/pages/reset-password.vue
+++ b/packages/client/src/pages/reset-password.vue
@@ -51,7 +51,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.resetPassword,
 	icon: 'fas fa-lock',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue
index 25fef7af5..404b9e3db 100644
--- a/packages/client/src/pages/search.vue
+++ b/packages/client/src/pages/search.vue
@@ -34,6 +34,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => ({
 	title: i18n.t('searchWith', { q: props.query }),
 	icon: 'fas fa-search',
-	bg: 'var(--bg)',
 })));
 </script>
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index 47b816243..d1e71c454 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -109,7 +109,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.accounts,
 	icon: 'fas fa-users',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue
index d94862712..b8a2dedb5 100644
--- a/packages/client/src/pages/settings/api.vue
+++ b/packages/client/src/pages/settings/api.vue
@@ -42,6 +42,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: 'API',
 	icon: 'fas fa-key',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue
index 673e91fe6..10ecbc795 100644
--- a/packages/client/src/pages/settings/apps.vue
+++ b/packages/client/src/pages/settings/apps.vue
@@ -67,7 +67,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.installedApps,
 	icon: 'fas fa-plug',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue
index 3e032be25..d5000d397 100644
--- a/packages/client/src/pages/settings/custom-css.vue
+++ b/packages/client/src/pages/settings/custom-css.vue
@@ -42,6 +42,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.customCss,
 	icon: 'fas fa-code',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue
index 295357377..b1cf8a8cc 100644
--- a/packages/client/src/pages/settings/deck.vue
+++ b/packages/client/src/pages/settings/deck.vue
@@ -73,6 +73,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.deck,
 	icon: 'fas fa-columns',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue
index a587c3299..3c4ea716c 100644
--- a/packages/client/src/pages/settings/delete-account.vue
+++ b/packages/client/src/pages/settings/delete-account.vue
@@ -48,6 +48,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts._accountDelete.accountDelete,
 	icon: 'fas fa-exclamation-triangle',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue
index 73c0384f1..cec2dc4d5 100644
--- a/packages/client/src/pages/settings/drive.vue
+++ b/packages/client/src/pages/settings/drive.vue
@@ -101,7 +101,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.drive,
 	icon: 'fas fa-cloud',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue
index 8b67ff34d..e575af6d6 100644
--- a/packages/client/src/pages/settings/email.vue
+++ b/packages/client/src/pages/settings/email.vue
@@ -107,6 +107,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.email,
 	icon: 'fas fa-envelope',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue
index e7339af14..74fa0bc92 100644
--- a/packages/client/src/pages/settings/general.vue
+++ b/packages/client/src/pages/settings/general.vue
@@ -186,6 +186,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.general,
 	icon: 'fas fa-cogs',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue
index 49d8a80b3..d48dab9f8 100644
--- a/packages/client/src/pages/settings/import-export.vue
+++ b/packages/client/src/pages/settings/import-export.vue
@@ -155,7 +155,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.importAndExport,
 	icon: 'fas fa-boxes',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 8143298cc..8e445a77d 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -42,7 +42,6 @@ const props = withDefaults(defineProps<{
 const indexInfo = {
 	title: i18n.ts.settings,
 	icon: 'fas fa-cog',
-	bg: 'var(--bg)',
 	hideHeader: true,
 };
 const INFO = ref(indexInfo);
diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue
index 7de151040..ccb02e08a 100644
--- a/packages/client/src/pages/settings/integration.vue
+++ b/packages/client/src/pages/settings/integration.vue
@@ -95,6 +95,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.integration,
 	icon: 'fas fa-share-alt',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/menu.vue
index 1b4d8799c..076654c10 100644
--- a/packages/client/src/pages/settings/menu.vue
+++ b/packages/client/src/pages/settings/menu.vue
@@ -83,6 +83,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.menu,
 	icon: 'fas fa-list-ul',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue
index d8cb28662..397a0c815 100644
--- a/packages/client/src/pages/settings/mute-block.vue
+++ b/packages/client/src/pages/settings/mute-block.vue
@@ -57,6 +57,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.muteAndBlock,
 	icon: 'fas fa-ban',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
index 494a3eebe..d2a363965 100644
--- a/packages/client/src/pages/settings/notifications.vue
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -56,6 +56,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.notifications,
 	icon: 'fas fa-bell',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue
index 283d87a06..52ef4d401 100644
--- a/packages/client/src/pages/settings/other.vue
+++ b/packages/client/src/pages/settings/other.vue
@@ -41,6 +41,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.other,
 	icon: 'fas fa-ellipsis-h',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue
index 7ff55e9d8..a4cab4b7a 100644
--- a/packages/client/src/pages/settings/plugin.install.vue
+++ b/packages/client/src/pages/settings/plugin.install.vue
@@ -120,6 +120,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts._plugin.install,
 	icon: 'fas fa-download',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue
index 75cf42bb8..8e773b799 100644
--- a/packages/client/src/pages/settings/plugin.vue
+++ b/packages/client/src/pages/settings/plugin.vue
@@ -90,7 +90,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.plugins,
 	icon: 'fas fa-plug',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue
index 4d509efe2..be9e34cdf 100644
--- a/packages/client/src/pages/settings/privacy.vue
+++ b/packages/client/src/pages/settings/privacy.vue
@@ -96,6 +96,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.privacy,
 	icon: 'fas fa-lock-open',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index b662de9e3..6b60148df 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -183,7 +183,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.profile,
 	icon: 'fas fa-user',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue
index d0fdf835c..382e1b081 100644
--- a/packages/client/src/pages/settings/reaction.vue
+++ b/packages/client/src/pages/settings/reaction.vue
@@ -131,7 +131,6 @@ definePageMetadata({
 		icon: 'fas fa-eye',
 		handler: preview,
 	},
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue
index 57880ef3d..eb3efa9af 100644
--- a/packages/client/src/pages/settings/security.vue
+++ b/packages/client/src/pages/settings/security.vue
@@ -104,7 +104,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.security,
 	icon: 'fas fa-lock',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue
index bb23257d7..f29c9eb04 100644
--- a/packages/client/src/pages/settings/sounds.vue
+++ b/packages/client/src/pages/settings/sounds.vue
@@ -131,6 +131,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.sounds,
 	icon: 'fas fa-music',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue
index 6a863ed9e..2994d8fb1 100644
--- a/packages/client/src/pages/settings/theme.install.vue
+++ b/packages/client/src/pages/settings/theme.install.vue
@@ -76,6 +76,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts._theme.install,
 	icon: 'fas fa-download',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue
index 68cbf3c35..9d28b4a31 100644
--- a/packages/client/src/pages/settings/theme.manage.vue
+++ b/packages/client/src/pages/settings/theme.manage.vue
@@ -74,6 +74,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts._theme.manage,
 	icon: 'fas fa-folder-open',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index d4c23e07b..6e7a5ff3a 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -152,7 +152,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.theme,
 	icon: 'fas fa-palette',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/webhook.edit.vue b/packages/client/src/pages/settings/webhook.edit.vue
index d3cf5d7b7..618250958 100644
--- a/packages/client/src/pages/settings/webhook.edit.vue
+++ b/packages/client/src/pages/settings/webhook.edit.vue
@@ -86,6 +86,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: 'Edit webhook',
 	icon: 'fas fa-bolt',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/webhook.new.vue b/packages/client/src/pages/settings/webhook.new.vue
index 508c0d78b..fa96c5fa4 100644
--- a/packages/client/src/pages/settings/webhook.new.vue
+++ b/packages/client/src/pages/settings/webhook.new.vue
@@ -78,6 +78,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: 'Create new webhook',
 	icon: 'fas fa-bolt',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/webhook.vue b/packages/client/src/pages/settings/webhook.vue
index 50739e2fd..ef9b9b56f 100644
--- a/packages/client/src/pages/settings/webhook.vue
+++ b/packages/client/src/pages/settings/webhook.vue
@@ -49,6 +49,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: 'Webhook',
 	icon: 'fas fa-bolt',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue
index c6af0e766..5fee7cd35 100644
--- a/packages/client/src/pages/settings/word-mute.vue
+++ b/packages/client/src/pages/settings/word-mute.vue
@@ -124,6 +124,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.wordMute,
 	icon: 'fas fa-comment-slash',
-	bg: 'var(--bg)',
 });
 </script>
diff --git a/packages/client/src/pages/tag.vue b/packages/client/src/pages/tag.vue
index d63864ed5..406eb1c98 100644
--- a/packages/client/src/pages/tag.vue
+++ b/packages/client/src/pages/tag.vue
@@ -28,6 +28,5 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => ({
 	title: props.tag,
 	icon: 'fas fa-hashtag',
-	bg: 'var(--bg)',
 })));
 </script>
diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue
index 352554353..cec383359 100644
--- a/packages/client/src/pages/theme-editor.vue
+++ b/packages/client/src/pages/theme-editor.vue
@@ -224,7 +224,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata({
 	title: i18n.ts.themeEditor,
 	icon: 'fas fa-palette',
-	bg: 'var(--bg)',
 });
 </script>
 
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index 004c29c56..40eb85ff4 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -151,7 +151,6 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: i18n.ts.timeline,
 	icon: src === 'local' ? 'fas fa-comments' : src === 'social' ? 'fas fa-share-alt' : src === 'global' ? 'fas fa-globe' : 'fas fa-home',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index b3292290e..cfea2637b 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -309,7 +309,6 @@ const headerTabs = $computed(() => [{
 definePageMetadata(computed(() => ({
 	title: user ? acct(user) : i18n.ts.userInfo,
 	icon: 'fas fa-info-circle',
-	bg: 'var(--bg)',
 })));
 </script>
 
diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue
index 07d5a166e..593db1dea 100644
--- a/packages/client/src/pages/user-list-timeline.vue
+++ b/packages/client/src/pages/user-list-timeline.vue
@@ -79,7 +79,6 @@ const headerTabs = $computed(() => []);
 definePageMetadata(computed(() => list ? {
 	title: list.name,
 	icon: 'fas fa-list-ul',
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/user/followers.vue b/packages/client/src/pages/user/followers.vue
index 3feec15e4..c0c0e01d7 100644
--- a/packages/client/src/pages/user/followers.vue
+++ b/packages/client/src/pages/user/followers.vue
@@ -54,7 +54,6 @@ definePageMetadata(computed(() => user ? {
 	subtitle: i18n.ts.followers,
 	userName: user,
 	avatar: user,
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/user/following.vue b/packages/client/src/pages/user/following.vue
index 0c6bb1c9f..d1753fe7d 100644
--- a/packages/client/src/pages/user/following.vue
+++ b/packages/client/src/pages/user/following.vue
@@ -54,7 +54,6 @@ definePageMetadata(computed(() => user ? {
 	subtitle: i18n.ts.following,
 	userName: user,
 	avatar: user,
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
index bd1bb11a5..99c341388 100644
--- a/packages/client/src/pages/user/index.vue
+++ b/packages/client/src/pages/user/index.vue
@@ -103,7 +103,6 @@ definePageMetadata(computed(() => user ? {
 	share: {
 		title: user.name,
 	},
-	bg: 'var(--bg)',
 } : null));
 </script>
 
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 41d59342b..3614f7de5 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -234,7 +234,7 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
 	> .contents {
 		width: 100%;
 		min-width: 0;
-		background: var(--panel);
+		background: var(--bg);
 
 		> main {
 			min-width: 0;

From e23e7de45377ae2425d10c6e675767d7fae91cc6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 15:12:11 +0900
Subject: [PATCH 051/100] feat: Log user ips (#8872)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* store ip and headers

* Update admin-file.vue

* require admin for view ip/headers

* IP (recent) 消した

* admin必須

* opt in

* clean ips periodically

* respect logging setting in drive/files/create
---
 locales/ja-JP.yml                             |  1 +
 .../migration/1655918165614-user-ip.js        | 17 +++++++
 .../migration/1656122560740-file-ip.js        | 13 +++++
 .../backend/migration/1656328812281-ip-2.js   | 13 +++++
 packages/backend/src/db/postgre.ts            |  4 +-
 .../backend/src/models/entities/drive-file.ts | 13 ++++-
 packages/backend/src/models/entities/meta.ts  |  7 ++-
 .../backend/src/models/entities/user-ip.ts    | 24 +++++++++
 packages/backend/src/models/index.ts          |  2 +
 packages/backend/src/queue/index.ts           | 18 ++++---
 .../src/queue/processors/system/clean.ts      | 18 +++++++
 .../src/queue/processors/system/index.ts      |  2 +
 .../backend/src/server/api/api-handler.ts     | 34 +++++++++++++
 packages/backend/src/server/api/call.ts       |  2 +-
 packages/backend/src/server/api/define.ts     | 34 +++++++------
 packages/backend/src/server/api/endpoints.ts  |  2 +
 .../api/endpoints/admin/drive/show-file.ts    |  7 ++-
 .../api/endpoints/admin/get-user-ips.ts       | 31 +++++++++++
 .../src/server/api/endpoints/admin/meta.ts    |  8 ++-
 .../server/api/endpoints/admin/update-meta.ts |  7 ++-
 .../api/endpoints/drive/files/create.ts       | 21 ++++++--
 .../endpoints/drive/files/upload-from-url.ts  |  6 +--
 .../backend/src/services/drive/add-file.ts    | 51 +++++++++++--------
 .../src/services/drive/upload-from-url.ts     | 10 ++--
 packages/client/src/account.ts                |  1 +
 packages/client/src/pages/admin-file.vue      | 26 ++++++++--
 packages/client/src/pages/admin/security.vue  | 15 ++++++
 packages/client/src/pages/user-info.vue       | 47 +++++++++++++++--
 packages/client/src/scripts/upload.ts         | 10 ++--
 29 files changed, 371 insertions(+), 73 deletions(-)
 create mode 100644 packages/backend/migration/1655918165614-user-ip.js
 create mode 100644 packages/backend/migration/1656122560740-file-ip.js
 create mode 100644 packages/backend/migration/1656328812281-ip-2.js
 create mode 100644 packages/backend/src/models/entities/user-ip.ts
 create mode 100644 packages/backend/src/queue/processors/system/clean.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/get-user-ips.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index b97b64dc5..ec726c821 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -854,6 +854,7 @@ noEmailServerWarning: "メールサーバーの設定がされていません。
 thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。"
 recommended: "推奨"
 check: "チェック"
+requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。"
 isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
 typeToConfirm: "この操作を行うには {x} と入力してください"
 deleteAccount: "アカウント削除"
diff --git a/packages/backend/migration/1655918165614-user-ip.js b/packages/backend/migration/1655918165614-user-ip.js
new file mode 100644
index 000000000..2294fbaf1
--- /dev/null
+++ b/packages/backend/migration/1655918165614-user-ip.js
@@ -0,0 +1,17 @@
+export class userIp1655918165614 {
+    name = 'userIp1655918165614'
+
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "user_ip" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "ip" character varying(128) NOT NULL, CONSTRAINT "PK_2c44ddfbf7c0464d028dcef325e" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_7f7f1c66f48e9a8e18a33bc515" ON "user_ip" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_361b500e06721013c124b7b6c5" ON "user_ip" ("userId", "ip") `);
+        await queryRunner.query(`ALTER TABLE "user_ip" ADD CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_ip" DROP CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_361b500e06721013c124b7b6c5"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_7f7f1c66f48e9a8e18a33bc515"`);
+        await queryRunner.query(`DROP TABLE "user_ip"`);
+    }
+}
diff --git a/packages/backend/migration/1656122560740-file-ip.js b/packages/backend/migration/1656122560740-file-ip.js
new file mode 100644
index 000000000..b59e7a911
--- /dev/null
+++ b/packages/backend/migration/1656122560740-file-ip.js
@@ -0,0 +1,13 @@
+export class fileIp1656122560740 {
+    name = 'fileIp1656122560740'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "drive_file" ADD "requestHeaders" jsonb DEFAULT '{}'`);
+        await queryRunner.query(`ALTER TABLE "drive_file" ADD "requestIp" character varying(128)`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "requestIp"`);
+        await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "requestHeaders"`);
+    }
+}
diff --git a/packages/backend/migration/1656328812281-ip-2.js b/packages/backend/migration/1656328812281-ip-2.js
new file mode 100644
index 000000000..b0ee1ebfc
--- /dev/null
+++ b/packages/backend/migration/1656328812281-ip-2.js
@@ -0,0 +1,13 @@
+export class ip21656328812281 {
+    name = 'ip21656328812281'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_ip" DROP CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150"`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableIpLogging" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIpLogging"`);
+        await queryRunner.query(`ALTER TABLE "user_ip" ADD CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+    }
+}
diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts
index 904bbb8b7..94d55e431 100644
--- a/packages/backend/src/db/postgre.ts
+++ b/packages/backend/src/db/postgre.ts
@@ -68,9 +68,10 @@ import { RegistryItem } from '@/models/entities/registry-item.js';
 import { Ad } from '@/models/entities/ad.js';
 import { PasswordResetRequest } from '@/models/entities/password-reset-request.js';
 import { UserPending } from '@/models/entities/user-pending.js';
+import { Webhook } from '@/models/entities/webhook.js';
+import { UserIp } from '@/models/entities/user-ip.js';
 
 import { entities as charts } from '@/services/chart/entities.js';
-import { Webhook } from '@/models/entities/webhook.js';
 import { envOption } from '../env.js';
 import { dbLogger } from './logger.js';
 import { redisClient } from './redis.js';
@@ -173,6 +174,7 @@ export const entities = [
 	PasswordResetRequest,
 	UserPending,
 	Webhook,
+	UserIp,
 	...charts,
 ];
 
diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
index a636d1d51..32387290d 100644
--- a/packages/backend/src/models/entities/drive-file.ts
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -1,7 +1,7 @@
 import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from '../id.js';
 import { User } from './user.js';
 import { DriveFolder } from './drive-folder.js';
-import { id } from '../id.js';
 
 @Entity()
 @Index(['userId', 'folderId', 'id'])
@@ -165,4 +165,15 @@ export class DriveFile {
 		comment: 'Whether the DriveFile is direct link to remote server.',
 	})
 	public isLink: boolean;
+
+	@Column('jsonb', {
+		default: {},
+		nullable: true,
+	})
+	public requestHeaders: Record<string, string> | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public requestIp: string | null;
 }
diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts
index 80b5228bc..2be43bdd4 100644
--- a/packages/backend/src/models/entities/meta.ts
+++ b/packages/backend/src/models/entities/meta.ts
@@ -1,6 +1,6 @@
 import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
-import { User } from './user.js';
 import { id } from '../id.js';
+import { User } from './user.js';
 import { Clip } from './clip.js';
 
 @Entity()
@@ -427,4 +427,9 @@ export class Meta {
 		default: true,
 	})
 	public objectStorageS3ForcePathStyle: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableIpLogging: boolean;
 }
diff --git a/packages/backend/src/models/entities/user-ip.ts b/packages/backend/src/models/entities/user-ip.ts
new file mode 100644
index 000000000..543e9e728
--- /dev/null
+++ b/packages/backend/src/models/entities/user-ip.ts
@@ -0,0 +1,24 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { id } from '../id.js';
+import { Note } from './note.js';
+import { User } from './user.js';
+
+@Entity()
+@Index(['userId', 'ip'], { unique: true })
+export class UserIp {
+	@PrimaryGeneratedColumn()
+	public id: string;
+
+	@Column('timestamp with time zone', {
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public ip: string;
+}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index 814b37d44..3f7326931 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -65,6 +65,7 @@ import { PasswordResetRequest } from './entities/password-reset-request.js';
 import { UserPending } from './entities/user-pending.js';
 import { InstanceRepository } from './repositories/instance.js';
 import { Webhook } from './entities/webhook.js';
+import { UserIp } from './entities/user-ip.js';
 
 export const Announcements = db.getRepository(Announcement);
 export const AnnouncementReads = db.getRepository(AnnouncementRead);
@@ -90,6 +91,7 @@ export const UserGroups = (UserGroupRepository);
 export const UserGroupJoinings = db.getRepository(UserGroupJoining);
 export const UserGroupInvitations = (UserGroupInvitationRepository);
 export const UserNotePinings = db.getRepository(UserNotePining);
+export const UserIps = db.getRepository(UserIp);
 export const UsedUsernames = db.getRepository(UsedUsername);
 export const Followings = (FollowingRepository);
 export const FollowRequests = (FollowRequestRepository);
diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts
index c5fd7de1c..ebb3a77ca 100644
--- a/packages/backend/src/queue/index.ts
+++ b/packages/backend/src/queue/index.ts
@@ -2,6 +2,9 @@ import httpSignature from '@peertube/http-signature';
 import { v4 as uuid } from 'uuid';
 
 import config from '@/config/index.js';
+import { DriveFile } from '@/models/entities/drive-file.js';
+import { IActivity } from '@/remote/activitypub/type.js';
+import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
 import { envOption } from '../env.js';
 
 import processDeliver from './processors/deliver.js';
@@ -12,18 +15,15 @@ import processSystemQueue from './processors/system/index.js';
 import processWebhookDeliver from './processors/webhook-deliver.js';
 import { endedPollNotification } from './processors/ended-poll-notification.js';
 import { queueLogger } from './logger.js';
-import { DriveFile } from '@/models/entities/drive-file.js';
 import { getJobInfo } from './get-job-info.js';
 import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
 import { ThinUser } from './types.js';
-import { IActivity } from '@/remote/activitypub/type.js';
-import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
 
 function renderError(e: Error): any {
 	return {
-		stack: e?.stack,
-		message: e?.message,
-		name: e?.name,
+		stack: e.stack,
+		message: e.message,
+		name: e.name,
 	};
 }
 
@@ -314,6 +314,12 @@ export default function() {
 		removeOnComplete: true,
 	});
 
+	systemQueue.add('clean', {
+	}, {
+		repeat: { cron: '0 0 * * *' },
+		removeOnComplete: true,
+	});
+
 	systemQueue.add('checkExpiredMutings', {
 	}, {
 		repeat: { cron: '*/5 * * * *' },
diff --git a/packages/backend/src/queue/processors/system/clean.ts b/packages/backend/src/queue/processors/system/clean.ts
new file mode 100644
index 000000000..c4f978d7c
--- /dev/null
+++ b/packages/backend/src/queue/processors/system/clean.ts
@@ -0,0 +1,18 @@
+import Bull from 'bull';
+import { LessThan } from 'typeorm';
+import { UserIps } from '@/models/index.js';
+
+import { queueLogger } from '../../logger.js';
+
+const logger = queueLogger.createSubLogger('clean');
+
+export async function clean(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> {
+	logger.info('Cleaning...');
+
+	UserIps.delete({
+		createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
+	});
+
+	logger.succ('Cleaned.');
+	done();
+}
diff --git a/packages/backend/src/queue/processors/system/index.ts b/packages/backend/src/queue/processors/system/index.ts
index f90f6efaf..9527d40b0 100644
--- a/packages/backend/src/queue/processors/system/index.ts
+++ b/packages/backend/src/queue/processors/system/index.ts
@@ -3,12 +3,14 @@ import { tickCharts } from './tick-charts.js';
 import { resyncCharts } from './resync-charts.js';
 import { cleanCharts } from './clean-charts.js';
 import { checkExpiredMutings } from './check-expired-mutings.js';
+import { clean } from './clean.js';
 
 const jobs = {
 	tickCharts,
 	resyncCharts,
 	cleanCharts,
 	checkExpiredMutings,
+	clean,
 } as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;
 
 export default function(dbQueue: Bull.Queue<Record<string, unknown>>) {
diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts
index c22c868c8..34ff970b4 100644
--- a/packages/backend/src/server/api/api-handler.ts
+++ b/packages/backend/src/server/api/api-handler.ts
@@ -1,10 +1,19 @@
 import Koa from 'koa';
 
+import { User } from '@/models/entities/user.js';
+import { UserIps } from '@/models/index.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
 import { IEndpoint } from './endpoints.js';
 import authenticate, { AuthenticationError } from './authenticate.js';
 import call from './call.js';
 import { ApiError } from './error.js';
 
+const userIpHistories = new Map<User['id'], Set<string>>();
+
+setInterval(() => {
+	userIpHistories.clear();
+}, 1000 * 60 * 60);
+
 export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
 	const body = ctx.is('multipart/form-data')
 		? (ctx.request as any).body
@@ -44,6 +53,31 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
 		}).catch((e: ApiError) => {
 			reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
 		});
+
+		// Log IP
+		if (user) {
+			fetchMeta().then(meta => {
+				if (!meta.enableIpLogging) return;
+				const ip = ctx.ip;
+				const ips = userIpHistories.get(user.id);
+				if (ips == null || !ips.has(ip)) {
+					if (ips == null) {
+						userIpHistories.set(user.id, new Set([ip]));
+					} else {
+						ips.add(ip);
+					}
+
+					try {
+						UserIps.insert({
+							createdAt: new Date(),
+							userId: user.id,
+							ip: ip,
+						});
+					} catch {
+					}
+				}
+			});
+		}
 	}).catch(e => {
 		if (e instanceof AuthenticationError) {
 			reply(403, new ApiError({
diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts
index 75bbc9f90..aa130459a 100644
--- a/packages/backend/src/server/api/call.ts
+++ b/packages/backend/src/server/api/call.ts
@@ -116,7 +116,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
 
 	// API invoking
 	const before = performance.now();
-	return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => {
+	return await ep.exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((e: Error) => {
 		if (e instanceof ApiError) {
 			throw e;
 		} else {
diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts
index 47dcb44ea..c1b56b8a8 100644
--- a/packages/backend/src/server/api/define.ts
+++ b/packages/backend/src/server/api/define.ts
@@ -1,16 +1,16 @@
 import * as fs from 'node:fs';
 import Ajv from 'ajv';
 import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js';
-import { IEndpointMeta } from './endpoints.js';
-import { ApiError } from './error.js';
 import { Schema, SchemaType } from '@/misc/schema.js';
 import { AccessToken } from '@/models/entities/access-token.js';
+import { IEndpointMeta } from './endpoints.js';
+import { ApiError } from './error.js';
 
 export type Response = Record<string, any> | void;
 
 // TODO: paramsの型をT['params']のスキーマ定義から推論する
 type executor<T extends IEndpointMeta, Ps extends Schema> =
-	(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) =>
+	(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
 		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
 
 const ajv = new Ajv({
@@ -20,23 +20,27 @@ const ajv = new Ajv({
 ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
 
 export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
-		: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
+		: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => Promise<any> {
 	const validate = ajv.compile(paramDef);
 
-	return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {
-		function cleanup() {
-			fs.unlink(file.path, () => {});
-		}
+	return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => {
+		let cleanup: undefined | (() => void) = undefined;
 
-		if (meta.requireFile && file == null) return Promise.reject(new ApiError({
-			message: 'File required.',
-			code: 'FILE_REQUIRED',
-			id: '4267801e-70d1-416a-b011-4ee502885d8b',
-		}));
+		if (meta.requireFile) {
+			cleanup = () => {
+				fs.unlink(file.path, () => {});
+			};
+
+			if (file == null) return Promise.reject(new ApiError({
+				message: 'File required.',
+				code: 'FILE_REQUIRED',
+				id: '4267801e-70d1-416a-b011-4ee502885d8b',
+			}));
+		}
 
 		const valid = validate(params);
 		if (!valid) {
-			if (file) cleanup();
+			if (file) cleanup!();
 
 			const errors = validate.errors!;
 			const err = new ApiError({
@@ -50,6 +54,6 @@ export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, pa
 			return Promise.reject(err);
 		}
 
-		return cb(params as SchemaType<Ps>, user, token, file, cleanup);
+		return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
 	};
 }
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 1a3fc199d..f01967754 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -35,6 +35,7 @@ import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/fed
 import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
 import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
 import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
+import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
 import * as ep___admin_invite from './endpoints/admin/invite.js';
 import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js';
 import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js';
@@ -348,6 +349,7 @@ const eps = [
 	['admin/federation/update-instance', ep___admin_federation_updateInstance],
 	['admin/get-index-stats', ep___admin_getIndexStats],
 	['admin/get-table-stats', ep___admin_getTableStats],
+	['admin/get-user-ips', ep___admin_getUserIps],
 	['admin/invite', ep___admin_invite],
 	['admin/moderators/add', ep___admin_moderators_add],
 	['admin/moderators/remove', ep___admin_moderators_remove],
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index 039df74f1..e9117a23c 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -1,6 +1,6 @@
+import { DriveFiles } from '@/models/index.js';
 import define from '../../../define.js';
 import { ApiError } from '../../../error.js';
-import { DriveFiles } from '@/models/index.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -184,5 +184,10 @@ export default define(meta, paramDef, async (ps, me) => {
 		throw new ApiError(meta.errors.noSuchFile);
 	}
 
+	if (!me.isAdmin) {
+		delete file.requestIp;
+		delete file.requestHeaders;
+	}
+
 	return file;
 });
diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
new file mode 100644
index 000000000..e8b9cb3b0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
@@ -0,0 +1,31 @@
+import { UserIps } from '@/models/index.js';
+import define from '../../define.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireAdmin: true,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		userId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, paramDef, async (ps, me) => {
+	const ips = await UserIps.find({
+		where: { userId: ps.userId },
+		order: { createdAt: 'DESC' },
+		take: 30,
+	});
+
+	return ips.map(x => ({
+		ip: x.ip,
+		createdAt: x.createdAt.toISOString(),
+	}));
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 8d50486ef..8b7162895 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -1,7 +1,7 @@
 import config from '@/config/index.js';
-import define from '../../define.js';
 import { fetchMeta } from '@/misc/fetch-meta.js';
 import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
+import define from '../../define.js';
 
 export const meta = {
 	tags: ['meta'],
@@ -304,6 +304,10 @@ export const meta = {
 				type: 'boolean',
 				optional: true, nullable: false,
 			},
+			enableIpLogging: {
+				type: 'boolean',
+				optional: true, nullable: false,
+			},
 		},
 	},
 } as const;
@@ -360,7 +364,6 @@ export default define(meta, paramDef, async (ps, me) => {
 		pinnedPages: instance.pinnedPages,
 		pinnedClipId: instance.pinnedClipId,
 		cacheRemoteFiles: instance.cacheRemoteFiles,
-
 		useStarForReactionFallback: instance.useStarForReactionFallback,
 		pinnedUsers: instance.pinnedUsers,
 		hiddenTags: instance.hiddenTags,
@@ -397,5 +400,6 @@ export default define(meta, paramDef, async (ps, me) => {
 		objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
 		deeplAuthKey: instance.deeplAuthKey,
 		deeplIsPro: instance.deeplIsPro,
+		enableIpLogging: instance.enableIpLogging,
 	};
 });
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 09e43301b..4dc4726a2 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -1,8 +1,8 @@
-import define from '../../define.js';
 import { Meta } from '@/models/entities/meta.js';
 import { insertModerationLog } from '@/services/insert-moderation-log.js';
 import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js';
 import { db } from '@/db/postgre.js';
+import define from '../../define.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -96,6 +96,7 @@ export const paramDef = {
 		objectStorageUseProxy: { type: 'boolean' },
 		objectStorageSetPublicRead: { type: 'boolean' },
 		objectStorageS3ForcePathStyle: { type: 'boolean' },
+		enableIpLogging: { type: 'boolean' },
 	},
 	required: [],
 } as const;
@@ -396,6 +397,10 @@ export default define(meta, paramDef, async (ps, me) => {
 		set.deeplIsPro = ps.deeplIsPro;
 	}
 
+	if (ps.enableIpLogging !== undefined) {
+		set.enableIpLogging = ps.enableIpLogging;
+	}
+
 	await db.transaction(async transactionalEntityManager => {
 		const metas = await transactionalEntityManager.find(Meta, {
 			order: {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index 7397fd9ce..3a76a5d98 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -1,10 +1,11 @@
 import ms from 'ms';
 import { addFile } from '@/services/drive/add-file.js';
+import { DriveFiles } from '@/models/index.js';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
 import define from '../../../define.js';
 import { apiLogger } from '../../../logger.js';
 import { ApiError } from '../../../error.js';
-import { DriveFiles } from '@/models/index.js';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
 
 export const meta = {
 	tags: ['drive'],
@@ -50,7 +51,7 @@ export const paramDef = {
 } as const;
 
 // eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
+export default define(meta, paramDef, async (ps, user, _, file, cleanup, ip, headers) => {
 	// Get 'name' parameter
 	let name = ps.name || file.originalname;
 	if (name !== undefined && name !== null) {
@@ -66,9 +67,21 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
 		name = null;
 	}
 
+	const meta = await fetchMeta();
+
 	try {
 		// Create file
-		const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive });
+		const driveFile = await addFile({
+			user,
+			path: file.path,
+			name,
+			comment: ps.comment,
+			folderId: ps.folderId,
+			force: ps.force,
+			sensitive: ps.isSensitive,
+			requestIp: meta.enableIpLogging ? ip : null,
+			requestHeaders: meta.enableIpLogging ? headers : null,
+		});
 		return await DriveFiles.pack(driveFile, { self: true });
 	} catch (e) {
 		if (e instanceof Error || typeof e === 'string') {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
index 53f2298f2..eb8071c3c 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -1,9 +1,9 @@
 import ms from 'ms';
 import { uploadFromUrl } from '@/services/drive/upload-from-url.js';
-import define from '../../../define.js';
 import { DriveFiles } from '@/models/index.js';
 import { publishMainStream } from '@/services/stream.js';
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
+import define from '../../../define.js';
 
 export const meta = {
 	tags: ['drive'],
@@ -34,8 +34,8 @@ export const paramDef = {
 } as const;
 
 // eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
-	uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => {
+export default define(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
+	uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => {
 		DriveFiles.pack(file, { self: true }).then(packedFile => {
 			publishMainStream(user.id, 'urlUploadFinished', {
 				marker: ps.marker,
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index cfbcb60dd..a25413187 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -2,26 +2,26 @@ import * as fs from 'node:fs';
 
 import { v4 as uuid } from 'uuid';
 
+import S3 from 'aws-sdk/clients/s3.js';
+import sharp from 'sharp';
+import { IsNull } from 'typeorm';
 import { publishMainStream, publishDriveStream } from '@/services/stream.js';
-import { deleteFile } from './delete-file.js';
 import { fetchMeta } from '@/misc/fetch-meta.js';
-import { GenerateVideoThumbnail } from './generate-video-thumbnail.js';
-import { driveLogger } from './logger.js';
-import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js';
 import { contentDisposition } from '@/misc/content-disposition.js';
 import { getFileInfo } from '@/misc/get-file-info.js';
 import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js';
-import { InternalStorage } from './internal-storage.js';
 import { DriveFile } from '@/models/entities/drive-file.js';
 import { IRemoteUser, User } from '@/models/entities/user.js';
 import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js';
 import { genId } from '@/misc/gen-id.js';
 import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
-import S3 from 'aws-sdk/clients/s3.js';
-import { getS3 } from './s3.js';
-import sharp from 'sharp';
 import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
-import { IsNull } from 'typeorm';
+import { getS3 } from './s3.js';
+import { InternalStorage } from './internal-storage.js';
+import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js';
+import { driveLogger } from './logger.js';
+import { GenerateVideoThumbnail } from './generate-video-thumbnail.js';
+import { deleteFile } from './delete-file.js';
 
 const logger = driveLogger.createSubLogger('register', 'yellow');
 
@@ -171,7 +171,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
 	}
 
 	if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) {
-		logger.debug(`web image and thumbnail not created (not an required file)`);
+		logger.debug('web image and thumbnail not created (not an required file)');
 		return {
 			webpublic: null,
 			thumbnail: null,
@@ -212,7 +212,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
 	let webpublic: IImage | null = null;
 
 	if (generateWeb && !satisfyWebpublic) {
-		logger.info(`creating web image`);
+		logger.info('creating web image');
 
 		try {
 			if (['image/jpeg', 'image/webp'].includes(type)) {
@@ -222,14 +222,14 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
 			} else if (['image/svg+xml'].includes(type)) {
 				webpublic = await convertSharpToPng(img, 2048, 2048);
 			} else {
-				logger.debug(`web image not created (not an required image)`);
+				logger.debug('web image not created (not an required image)');
 			}
 		} catch (err) {
-			logger.warn(`web image not created (an error occured)`, err as Error);
+			logger.warn('web image not created (an error occured)', err as Error);
 		}
 	} else {
-		if (satisfyWebpublic) logger.info(`web image not created (original satisfies webpublic)`);
-		else logger.info(`web image not created (from remote)`);
+		if (satisfyWebpublic) logger.info('web image not created (original satisfies webpublic)');
+		else logger.info('web image not created (from remote)');
 	}
 	// #endregion webpublic
 
@@ -240,10 +240,10 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
 		if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) {
 			thumbnail = await convertSharpToWebp(img, 498, 280);
 		} else {
-			logger.debug(`thumbnail not created (not an required file)`);
+			logger.debug('thumbnail not created (not an required file)');
 		}
 	} catch (err) {
-		logger.warn(`thumbnail not created (an error occured)`, err as Error);
+		logger.warn('thumbnail not created (an error occured)', err as Error);
 	}
 	// #endregion thumbnail
 
@@ -276,7 +276,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
 	const s3 = getS3(meta);
 
 	const upload = s3.upload(params, {
-		partSize: s3.endpoint?.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
+		partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
 	});
 
 	const result = await upload.promise();
@@ -326,6 +326,9 @@ type AddFileArgs = {
 	uri?: string | null;
 	/** Mark file as sensitive */
 	sensitive?: boolean | null;
+
+	requestIp?: string | null;
+	requestHeaders?: Record<string, string> | null;
 };
 
 /**
@@ -342,7 +345,9 @@ export async function addFile({
 	isLink = false,
 	url = null,
 	uri = null,
-	sensitive = null
+	sensitive = null,
+	requestIp = null,
+	requestHeaders = null,
 }: AddFileArgs): Promise<DriveFile> {
 	const info = await getFileInfo(path);
 	logger.info(`${JSON.stringify(info)}`);
@@ -427,11 +432,13 @@ export async function addFile({
 	file.properties = properties;
 	file.blurhash = info.blurhash || null;
 	file.isLink = isLink;
+	file.requestIp = requestIp;
+	file.requestHeaders = requestHeaders;
 	file.isSensitive = user
 		? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
-			(sensitive !== null && sensitive !== undefined)
-				? sensitive
-				: false
+		(sensitive !== null && sensitive !== undefined)
+			? sensitive
+			: false
 		: false;
 
 	if (url !== null) {
diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts
index 001fc49ee..3c5e1aa5c 100644
--- a/packages/backend/src/services/drive/upload-from-url.ts
+++ b/packages/backend/src/services/drive/upload-from-url.ts
@@ -1,12 +1,12 @@
 import { URL } from 'node:url';
-import { addFile } from './add-file.js';
 import { User } from '@/models/entities/user.js';
-import { driveLogger } from './logger.js';
 import { createTemp } from '@/misc/create-temp.js';
 import { downloadUrl } from '@/misc/download-url.js';
 import { DriveFolder } from '@/models/entities/drive-folder.js';
 import { DriveFile } from '@/models/entities/drive-file.js';
 import { DriveFiles } from '@/models/index.js';
+import { driveLogger } from './logger.js';
+import { addFile } from './add-file.js';
 
 const logger = driveLogger.createSubLogger('downloader');
 
@@ -19,6 +19,8 @@ type Args = {
 	force?: boolean;
 	isLink?: boolean;
 	comment?: string | null;
+	requestIp?: string | null;
+	requestHeaders?: Record<string, string> | null;
 };
 
 export async function uploadFromUrl({
@@ -30,6 +32,8 @@ export async function uploadFromUrl({
 	force = false,
 	isLink = false,
 	comment = null,
+	requestIp = null,
+	requestHeaders = null,
 }: Args): Promise<DriveFile> {
 	let name = new URL(url).pathname.split('/').pop() || null;
 	if (name == null || !DriveFiles.validateFileName(name)) {
@@ -49,7 +53,7 @@ export async function uploadFromUrl({
 		// write content at URL to temp file
 		await downloadUrl(url, path);
 
-		const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
+		const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders });
 		logger.succ(`Got: ${driveFile.id}`);
 		return driveFile!;
 	} catch (e) {
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
index eb2ba0a1e..38f2ee4b3 100644
--- a/packages/client/src/account.ts
+++ b/packages/client/src/account.ts
@@ -17,6 +17,7 @@ const accountData = localStorage.getItem('account');
 export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
 
 export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
+export const iAmAdmin = $i != null && $i.isAdmin;
 
 export async function signout() {
 	waiting();
diff --git a/packages/client/src/pages/admin-file.vue b/packages/client/src/pages/admin-file.vue
index 7bfbed35f..f96a41a7e 100644
--- a/packages/client/src/pages/admin-file.vue
+++ b/packages/client/src/pages/admin-file.vue
@@ -1,7 +1,7 @@
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
-	<MkSpacer v-if="file" :content-max="500" :margin-min="16" :margin-max="32">
+	<MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32">
 		<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot">
 			<a class="_formBlock thumbnail" :href="file.url" target="_blank">
 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@@ -39,6 +39,20 @@
 				<MkButton danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton>
 			</div>
 		</div>
+		<div v-else-if="tab === 'ip' && info" class="_formRoot">
+			<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
+			<MkKeyValue v-if="info.requestIp" class="_formBlock _monospace" :copy="info.requestIp" oneline>
+				<template #key>IP</template>
+				<template #value>{{ info.requestIp }}</template>
+			</MkKeyValue>
+			<FormSection v-if="info.requestHeaders">
+				<template #label>Headers</template>
+				<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_formBlock _monospace">
+					<template #key>{{ k }}</template>
+					<template #value>{{ v }}</template>
+				</MkKeyValue>
+			</FormSection>
+		</div>
 		<div v-else-if="tab === 'raw'" class="_formRoot">
 			<MkObjectView v-if="info" tall :value="info">
 			</MkObjectView>
@@ -54,13 +68,15 @@ import MkSwitch from '@/components/form/switch.vue';
 import MkObjectView from '@/components/object-view.vue';
 import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
 import MkKeyValue from '@/components/key-value.vue';
-import FormLink from '@/components/form/link.vue';
+import FormSection from '@/components/form/section.vue';
 import MkUserCardMini from '@/components/user-card-mini.vue';
+import MkInfo from '@/components/ui/info.vue';
 import bytes from '@/filters/bytes';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { acct } from '@/filters/user';
+import { iAmAdmin, iAmModerator } from '@/account';
 
 let tab = $ref('overview');
 let file: any = $ref(null);
@@ -108,7 +124,11 @@ const headerTabs = $computed(() => [{
 	key: 'overview',
 	title: i18n.ts.overview,
 	icon: 'fas fa-info-circle',
-}, {
+}, iAmModerator ? {
+	key: 'ip',
+	title: 'IP',
+	icon: 'fas fa-bars-staggered',
+} : null, {
 	key: 'raw',
 	title: 'Raw data',
 	icon: 'fas fa-code',
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
index ec11e42a7..76fa9d21e 100644
--- a/packages/client/src/pages/admin/security.vue
+++ b/packages/client/src/pages/admin/security.vue
@@ -14,6 +14,18 @@
 					<XBotProtection/>
 				</FormFolder>
 
+				<FormFolder class="_formBlock">
+					<template #label>Log IP address</template>
+					<template v-if="enableIpLogging" #suffix>Enabled</template>
+					<template v-else #suffix>Disabled</template>
+
+					<div class="_formRoot">
+						<FormSwitch v-model="enableIpLogging" class="_formBlock" @update:modelValue="save">
+							<template #label>Enable</template>
+						</FormSwitch>
+					</div>
+				</FormFolder>
+
 				<FormFolder class="_formBlock">
 					<template #label>Summaly Proxy</template>
 
@@ -51,17 +63,20 @@ import { definePageMetadata } from '@/scripts/page-metadata';
 let summalyProxy: string = $ref('');
 let enableHcaptcha: boolean = $ref(false);
 let enableRecaptcha: boolean = $ref(false);
+let enableIpLogging: boolean = $ref(false);
 
 async function init() {
 	const meta = await os.api('admin/meta');
 	summalyProxy = meta.summalyProxy;
 	enableHcaptcha = meta.enableHcaptcha;
 	enableRecaptcha = meta.enableRecaptcha;
+	enableIpLogging = meta.enableIpLogging;
 }
 
 function save() {
 	os.apiWithDialog('admin/update-meta', {
 		summalyProxy,
+		enableIpLogging,
 	}).then(() => {
 		fetchInstance();
 	});
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index cfea2637b..f9edd208a 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -27,6 +27,12 @@
 						<template #key>ID</template>
 						<template #value><span class="_monospace">{{ user.id }}</span></template>
 					</MkKeyValue>
+					<!-- 要る?
+					<MkKeyValue v-if="ips.length > 0" :copy="user.id" oneline style="margin: 1em 0;">
+						<template #key>IP (recent)</template>
+						<template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
+					</MkKeyValue>
+					-->
 					<MkKeyValue oneline style="margin: 1em 0;">
 						<template #key>{{ i18n.ts.createdAt }}</template>
 						<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
@@ -92,8 +98,18 @@
 			<div v-else-if="tab === 'files'" class="_formRoot">
 				<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
 			</div>
+			<div v-else-if="tab === 'ip'" class="_formRoot">
+				<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
+				<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
+				<template v-if="iAmAdmin && ips">
+					<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
+						<span class="date">{{ record.createdAt }}</span>
+						<span class="ip">{{ record.ip }}</span>
+					</div>
+				</template>
+			</div>
 			<div v-else-if="tab === 'ap'" class="_formRoot">
-				<MkObjectView v-if="ap" tall :value="user">
+				<MkObjectView v-if="ap" tall :value="ap">
 				</MkObjectView>
 			</div>
 			<div v-else-if="tab === 'raw'" class="_formRoot">
@@ -122,6 +138,7 @@ import MkKeyValue from '@/components/key-value.vue';
 import MkSelect from '@/components/form/select.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import MkFileListForAdmin from '@/components/file-list-for-admin.vue';
+import MkInfo from '@/components/ui/info.vue';
 import * as os from '@/os';
 import number from '@/filters/number';
 import bytes from '@/filters/bytes';
@@ -129,7 +146,7 @@ import { url } from '@/config';
 import { userPage, acct } from '@/filters/user';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { i18n } from '@/i18n';
-import { iAmModerator } from '@/account';
+import { iAmAdmin, iAmModerator } from '@/account';
 
 const props = defineProps<{
 	userId: string;
@@ -140,6 +157,7 @@ let chartSrc = $ref('per-user-notes');
 let user = $ref<null | misskey.entities.UserDetailed>();
 let init = $ref();
 let info = $ref();
+let ips = $ref(null);
 let ap = $ref(null);
 let moderator = $ref(false);
 let silenced = $ref(false);
@@ -158,9 +176,12 @@ function createFetcher() {
 			userId: props.userId,
 		}), os.api('admin/show-user', {
 			userId: props.userId,
-		})]).then(([_user, _info]) => {
+		}), iAmAdmin ? os.api('admin/get-user-ips', {
+			userId: props.userId,
+		}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => {
 			user = _user;
 			info = _info;
+			ips = _ips;
 			moderator = info.isModerator;
 			silenced = info.isSilenced;
 			suspended = info.isSuspended;
@@ -300,7 +321,11 @@ const headerTabs = $computed(() => [{
 	key: 'ap',
 	title: 'AP',
 	icon: 'fas fa-share-alt',
-}, {
+}, iAmModerator ? {
+	key: 'ip',
+	title: 'IP',
+	icon: 'fas fa-bars-staggered',
+} : null, {
 	key: 'raw',
 	title: 'Raw',
 	icon: 'fas fa-code',
@@ -362,3 +387,17 @@ definePageMetadata(computed(() => ({
 	}
 }
 </style>
+
+<style lang="scss" module>
+.ip {
+	display: flex;
+
+	> :global(.date) {
+		opacity: 0.7;
+	}
+
+	> :global(.ip) {
+		margin-left: auto;
+	}
+}
+</style>
diff --git a/packages/client/src/scripts/upload.ts b/packages/client/src/scripts/upload.ts
index 2f7b30b58..2f907e5e8 100644
--- a/packages/client/src/scripts/upload.ts
+++ b/packages/client/src/scripts/upload.ts
@@ -1,9 +1,9 @@
 import { reactive, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { readAndCompressImage } from 'browser-image-resizer';
 import { defaultStore } from '@/store';
 import { apiUrl } from '@/config';
-import * as Misskey from 'misskey-js';
 import { $i } from '@/account';
-import { readAndCompressImage } from 'browser-image-resizer';
 import { alert } from '@/os';
 
 type Uploading = {
@@ -31,7 +31,7 @@ export function uploadFile(
 	file: File,
 	folder?: any,
 	name?: string,
-	keepOriginal: boolean = defaultStore.state.keepOriginalUploading
+	keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
 ): Promise<Misskey.entities.DriveFile> {
 	if (folder && typeof folder === 'object') folder = folder.id;
 
@@ -45,7 +45,7 @@ export function uploadFile(
 				name: name || file.name || 'untitled',
 				progressMax: undefined,
 				progressValue: undefined,
-				img: window.URL.createObjectURL(file)
+				img: window.URL.createObjectURL(file),
 			});
 
 			uploads.value.push(ctx);
@@ -86,7 +86,7 @@ export function uploadFile(
 					alert({
 						type: 'error',
 						title: 'Failed to upload',
-						text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`
+						text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
 					});
 
 					reject();

From 4e5d7259f728c137ade57861834aeeb05071f9be Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 15:17:09 +0900
Subject: [PATCH 052/100] 12.112.0-beta.12

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

diff --git a/package.json b/package.json
index aa5a04927..03165b762 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.11",
+	"version": "12.112.0-beta.12",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From ddcd10db8e0c76a091e8e8859d15e89109e25a9e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 21:26:33 +0900
Subject: [PATCH 053/100] feat(server): add fetch-rss api to reduce dependency
 of external apis

---
 packages/backend/package.json                 |  1 +
 packages/backend/src/server/api/endpoints.ts  |  2 +
 .../src/server/api/endpoints/fetch-rss.ts     | 39 +++++++++++++++++++
 packages/backend/yarn.lock                    | 15 ++++++-
 packages/client/src/widgets/rss-marquee.vue   |  2 +-
 packages/client/src/widgets/rss.vue           |  2 +-
 6 files changed, 58 insertions(+), 3 deletions(-)
 create mode 100644 packages/backend/src/server/api/endpoints/fetch-rss.ts

diff --git a/packages/backend/package.json b/packages/backend/package.json
index 0bf47888e..ef3f55458 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -92,6 +92,7 @@
 		"rename": "1.0.4",
 		"require-all": "3.0.0",
 		"rndstr": "1.0.0",
+		"rss-parser": "3.12.0",
 		"s-age": "1.1.2",
 		"sanitize-html": "2.7.0",
 		"semver": "7.3.7",
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index f01967754..f45876392 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -312,6 +312,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
 import * as ep___users_search from './endpoints/users/search.js';
 import * as ep___users_show from './endpoints/users/show.js';
 import * as ep___users_stats from './endpoints/users/stats.js';
+import * as ep___fetchRss from './endpoints/fetch-rss.js';
 
 const eps = [
 	['admin/meta', ep___admin_meta],
@@ -626,6 +627,7 @@ const eps = [
 	['users/search', ep___users_search],
 	['users/show', ep___users_show],
 	['users/stats', ep___users_stats],
+	['fetch-rss', ep___fetchRss],
 ];
 
 export interface IEndpointMeta {
diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts
new file mode 100644
index 000000000..05fa22a9e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts
@@ -0,0 +1,39 @@
+import Parser from 'rss-parser';
+import { getResponse } from '@/misc/fetch.js';
+import config from '@/config/index.js';
+import define from '../define.js';
+
+const rssParser = new Parser();
+
+export const meta = {
+	tags: ['meta'],
+
+	requireCredential: false,
+	allowGet: true,
+	cacheSec: 60 * 3,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		url: { type: 'string' },
+	},
+	required: ['url'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, paramDef, async (ps) => {
+	const res = await getResponse({
+		url: ps.url,
+		method: 'GET',
+		headers: Object.assign({
+			'User-Agent': config.userAgent,
+			Accept: 'application/rss+xml, */*',
+		}),
+		timeout: 5000,
+	});
+
+	const text = await res.text();
+
+	return rssParser.parseString(text);
+});
diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock
index 880bbf7d1..3c9f2680f 100644
--- a/packages/backend/yarn.lock
+++ b/packages/backend/yarn.lock
@@ -2485,6 +2485,11 @@ entities@^2.0.0:
   resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
   integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
 
+entities@^2.0.3:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
 entities@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.0.tgz#62915f08d67353bb4eb67e3d62641a4059aec656"
@@ -5985,6 +5990,14 @@ rndstr@1.0.0:
     rangestr "0.0.1"
     seedrandom "2.4.2"
 
+rss-parser@3.12.0:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/rss-parser/-/rss-parser-3.12.0.tgz#b8888699ea46304a74363fbd8144671b2997984c"
+  integrity sha512-aqD3E8iavcCdkhVxNDIdg1nkBI17jgqF+9OqPS1orwNaOgySdpvq6B+DoONLhzjzwV8mWg37sb60e4bmLK117A==
+  dependencies:
+    entities "^2.0.3"
+    xml2js "^0.4.19"
+
 run-parallel@^1.1.9:
   version "1.1.9"
   resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
@@ -7174,7 +7187,7 @@ xml2js@0.4.19:
     sax ">=0.6.0"
     xmlbuilder "~9.0.1"
 
-xml2js@^0.4.23:
+xml2js@^0.4.19, xml2js@^0.4.23:
   version "0.4.23"
   resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
   integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
diff --git a/packages/client/src/widgets/rss-marquee.vue b/packages/client/src/widgets/rss-marquee.vue
index 6d910a2fb..d96eadb4e 100644
--- a/packages/client/src/widgets/rss-marquee.vue
+++ b/packages/client/src/widgets/rss-marquee.vue
@@ -80,7 +80,7 @@ const fetching = ref(true);
 let key = $ref(0);
 
 const tick = () => {
-	fetch(`https://api.rss2json.com/v1/api.json?rss_url=${widgetProps.url}`, {}).then(res => {
+	fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
 		res.json().then(feed => {
 			items.value = feed.items;
 			fetching.value = false;
diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue
index ea896478a..72f624982 100644
--- a/packages/client/src/widgets/rss.vue
+++ b/packages/client/src/widgets/rss.vue
@@ -51,7 +51,7 @@ const items = ref([]);
 const fetching = ref(true);
 
 const tick = () => {
-	fetch(`https://api.rss2json.com/v1/api.json?rss_url=${widgetProps.url}`, {}).then(res => {
+	fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
 		res.json().then(feed => {
 			items.value = feed.items;
 			fetching.value = false;

From 89dbc73bef4788c7ec73712e50835a1cbb7c6aec Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 21:28:04 +0900
Subject: [PATCH 054/100] enhance(client): better marquee component

---
 packages/client/package.json                  |  1 -
 packages/client/src/components/marquee.vue    | 97 +++++++++++++++++++
 .../client/src/pages/welcome.entrance.a.vue   |  2 +-
 packages/client/src/widgets/rss-marquee.vue   |  2 +-
 packages/client/yarn.lock                     | 15 +--
 5 files changed, 100 insertions(+), 17 deletions(-)
 create mode 100644 packages/client/src/components/marquee.vue

diff --git a/packages/client/package.json b/packages/client/package.json
index 5a087a297..b810abd08 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -76,7 +76,6 @@
 		"vanilla-tilt": "1.7.2",
 		"vite": "3.0.0-beta.5",
 		"vue": "3.2.37",
-		"vue-marquee-text-component": "2.0.1",
 		"vue-prism-editor": "2.0.0-alpha.2",
 		"vuedraggable": "4.0.1",
 		"websocket": "1.0.34",
diff --git a/packages/client/src/components/marquee.vue b/packages/client/src/components/marquee.vue
new file mode 100644
index 000000000..2fd76a54f
--- /dev/null
+++ b/packages/client/src/components/marquee.vue
@@ -0,0 +1,97 @@
+<script lang="ts">
+import { h, onMounted, onUnmounted, ref } from 'vue';
+
+export default {
+	name: 'MarqueeText',
+	props: {
+		duration: {
+			type: Number,
+			default: 15,
+		},
+		repeat: {
+			type: Number,
+			default: 2,
+		},
+		paused: {
+			type: Boolean,
+			default: false,
+		},
+		reverse: {
+			type: Boolean,
+			default: false,
+		},
+	},
+	setup(props) {
+		const contentEl = ref();
+
+		function calc() {
+			const eachLength = contentEl.value.offsetWidth / props.repeat;
+			const factor = 3000;
+			const duration = props.duration / ((1 / eachLength) * factor);
+
+			contentEl.value.style.animationDuration = `${duration}s`;
+		}
+
+		onMounted(() => {
+			calc();
+		});
+
+		onUnmounted(() => {
+		});
+
+		return {
+			contentEl,
+		};
+	},
+	render({
+		$slots, $style, $props: {
+			duration, repeat, paused, reverse,
+		},
+	}) {
+		return h('div', { class: [$style.wrap] }, [
+			h('span', {
+				ref: 'contentEl',
+				class: [
+					paused
+						? $style.paused
+						: undefined,
+					$style.content,
+				],
+			}, Array(repeat).fill(
+				h('span', {
+					class: $style.text,
+					style: {
+						animationDirection: reverse
+							? 'reverse'
+							: undefined,
+					},
+				}, $slots.default()),
+			)),
+		]);
+	},
+};
+</script>
+
+<style lang="scss" module>
+.wrap {
+	overflow: clip;
+}
+.content {
+	display: inline-block;
+	white-space: nowrap;
+}
+.text {
+	display: inline-block;
+	animation-name: marquee;
+	animation-timing-function: linear;
+	animation-iteration-count: infinite;
+	animation-duration: inherit;
+}
+.paused .text {
+	animation-play-state: paused;
+}
+@keyframes marquee {
+	0% { transform:translateX(0); }
+	100% { transform:translateX(-100%); }
+}
+</style>
diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue
index b78a37eab..8ff7d9057 100644
--- a/packages/client/src/pages/welcome.entrance.a.vue
+++ b/packages/client/src/pages/welcome.entrance.a.vue
@@ -47,8 +47,8 @@
 <script lang="ts" setup>
 import { } from 'vue';
 import { toUnicode } from 'punycode/';
-import MarqueeText from 'vue-marquee-text-component';
 import XTimeline from './welcome.timeline.vue';
+import MarqueeText from '@/components/marquee.vue';
 import XSigninDialog from '@/components/signin-dialog.vue';
 import XSignupDialog from '@/components/signup-dialog.vue';
 import MkButton from '@/components/ui/button.vue';
diff --git a/packages/client/src/widgets/rss-marquee.vue b/packages/client/src/widgets/rss-marquee.vue
index d96eadb4e..2f92c09f3 100644
--- a/packages/client/src/widgets/rss-marquee.vue
+++ b/packages/client/src/widgets/rss-marquee.vue
@@ -18,8 +18,8 @@
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref, watch } from 'vue';
-import MarqueeText from 'vue-marquee-text-component';
 import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import MarqueeText from '@/components/marquee.vue';
 import { GetFormResultType } from '@/scripts/form';
 import * as os from '@/os';
 import MkContainer from '@/components/ui/container.vue';
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index e5f2e31d7..a7b46b70d 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -1221,11 +1221,6 @@ content-disposition@0.5.4:
   dependencies:
     safe-buffer "5.2.1"
 
-core-js@^3.18.0:
-  version "3.23.3"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.3.tgz#3b977612b15da6da0c9cc4aec487e8d24f371112"
-  integrity sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q==
-
 core-util-is@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -4250,20 +4245,12 @@ vue-eslint-parser@^9.0.1:
     lodash "^4.17.21"
     semver "^7.3.6"
 
-vue-marquee-text-component@2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/vue-marquee-text-component/-/vue-marquee-text-component-2.0.1.tgz#62691df195f755471fa9bdc9b1969f836a922b9a"
-  integrity sha512-dbeRwDY5neOJcWZrDFU2tJMhPSsxN25ZpNYeZdt0jkseg1MbyGKzrfEH9nrCFZRkEfqhxG+ukyzwVwR9US5sTQ==
-  dependencies:
-    core-js "^3.18.0"
-    vue "^3.2.19"
-
 vue-prism-editor@2.0.0-alpha.2:
   version "2.0.0-alpha.2"
   resolved "https://registry.yarnpkg.com/vue-prism-editor/-/vue-prism-editor-2.0.0-alpha.2.tgz#aa53a88efaaed628027cbb282c2b1d37fc7c5c69"
   integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
 
-vue@3.2.37, vue@^3.2.19:
+vue@3.2.37:
   version "3.2.37"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
   integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==

From 92aa415ea67bb1d0c48fd0385988bc628c48a37d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 21:28:55 +0900
Subject: [PATCH 055/100] enhance(client): better sticky-container component

---
 .../src/components/global/page-header.vue     |  3 --
 .../components/global/sticky-container.vue    | 52 +++++++++----------
 packages/client/src/pages/admin/_header_.vue  |  3 --
 3 files changed, 24 insertions(+), 34 deletions(-)

diff --git a/packages/client/src/components/global/page-header.vue b/packages/client/src/components/global/page-header.vue
index 5395a8796..0ab15e0ca 100644
--- a/packages/client/src/components/global/page-header.vue
+++ b/packages/client/src/components/global/page-header.vue
@@ -174,9 +174,6 @@ onUnmounted(() => {
 .fdidabkb {
 	--height: 60px;
 	display: flex;
-	position: sticky;
-	top: var(--stickyTop, 0);
-	z-index: 1000;
 	width: 100%;
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
diff --git a/packages/client/src/components/global/sticky-container.vue b/packages/client/src/components/global/sticky-container.vue
index 98a7ee9c3..2603fac55 100644
--- a/packages/client/src/components/global/sticky-container.vue
+++ b/packages/client/src/components/global/sticky-container.vue
@@ -1,6 +1,8 @@
 <template>
 <div ref="rootEl">
-	<slot name="header"></slot>
+	<div ref="headerEl">
+		<slot name="header"></slot>
+	</div>
 	<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
 		<slot></slot>
 	</div>
@@ -8,39 +10,25 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted } from 'vue';
+import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
 
-const props = withDefaults(defineProps<{
-	autoSticky?: boolean;
-}>(), {
-	autoSticky: false,
-});
+const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
 
 const rootEl = $ref<HTMLElement>();
+const headerEl = $ref<HTMLElement>();
 const bodyEl = $ref<HTMLElement>();
 
 let headerHeight = $ref<string | undefined>();
+let childStickyTop = $ref(0);
+const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
+provide(CURRENT_STICKY_TOP, $$(childStickyTop));
 
 const calc = () => {
-	const currentStickyTop = getComputedStyle(rootEl).getPropertyValue('--stickyTop') || '0px';
-
-	const header = rootEl.children[0] as HTMLElement;
-	if (header === bodyEl) {
-		bodyEl.style.setProperty('--stickyTop', currentStickyTop);
-	} else {
-		bodyEl.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
-		headerHeight = header.offsetHeight.toString();
-
-		if (props.autoSticky) {
-			header.style.setProperty('--stickyTop', currentStickyTop);
-			header.style.position = 'sticky';
-			header.style.top = 'var(--stickyTop)';
-			header.style.zIndex = '1';
-		}
-	}
+	childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
+	headerHeight = headerEl.offsetHeight.toString();
 };
 
-const observer = new MutationObserver(() => {
+const observer = new ResizeObserver(() => {
 	window.setTimeout(() => {
 		calc();
 	}, 100);
@@ -49,11 +37,19 @@ const observer = new MutationObserver(() => {
 onMounted(() => {
 	calc();
 
-	observer.observe(rootEl, {
-		attributes: false,
-		childList: true,
-		subtree: false,
+	watch(parentStickyTop, calc);
+
+	watch($$(childStickyTop), () => {
+		bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`);
+	}, {
+		immediate: true,
 	});
+
+	headerEl.style.position = 'sticky';
+	headerEl.style.top = 'var(--stickyTop, 0)';
+	headerEl.style.zIndex = '1000';
+
+	observer.observe(headerEl);
 });
 
 onUnmounted(() => {
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index 1c3cdcb51..73747e116 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -152,9 +152,6 @@ onUnmounted(() => {
 .fdidabkc {
 	--height: 60px;
 	display: flex;
-	position: sticky;
-	top: var(--stickyTop, 0);
-	z-index: 1000;
 	width: 100%;
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));

From 395b61b271d56c8dd9de1bb66bfa12fdcfce7ecd Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 21:29:48 +0900
Subject: [PATCH 056/100] fix(client): use unique class names for root to
 prevent conflicts of style

---
 packages/client/src/components/tag-cloud.vue | 4 ++--
 packages/client/src/pages/settings/theme.vue | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/client/src/components/tag-cloud.vue b/packages/client/src/components/tag-cloud.vue
index 8df8d0b05..13f528ecf 100644
--- a/packages/client/src/components/tag-cloud.vue
+++ b/packages/client/src/components/tag-cloud.vue
@@ -1,5 +1,5 @@
 <template>
-<div ref="rootEl" class="root">
+<div ref="rootEl" class="meijqfqm">
 	<canvas :id="idForCanvas" ref="canvasEl" class="canvas" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
 	<div :id="idForTags" ref="tagsEl" class="tags">
 		<ul>
@@ -71,7 +71,7 @@ defineExpose({
 </script>
 
 <style lang="scss" scoped>
-.root {
+.meijqfqm {
 	position: relative;
 	overflow: clip;
 	display: grid;
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index 6e7a5ff3a..0ed4abc37 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="_formRoot root">
+<div class="_formRoot rsljpzjq">
 	<div v-adaptive-border class="rfqxtzch _panel _formBlock">
 		<div class="toggle">
 			<div class="toggleWrapper">
@@ -384,7 +384,7 @@ definePageMetadata({
 	}
 }
 
-.root {
+.rsljpzjq {
 	> .selects {
 		display: flex;
 		gap: 1.5em var(--margin);

From 31324181e91a9e05a8cc1cc770bebd5b1e7a1390 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 22:06:53 +0900
Subject: [PATCH 057/100] fix(client): fix typo

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

diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue
index 345001c43..5fd9ec460 100644
--- a/packages/client/src/components/form-dialog.vue
+++ b/packages/client/src/components/form-dialog.vue
@@ -41,7 +41,7 @@
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
 				</FormRadios>
-				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
+				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 				</FormRange>

From d15507a090ccde710677069f9f73579c21888aff Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 22:07:04 +0900
Subject: [PATCH 058/100] chore(client): tweak ui

---
 packages/client/src/components/form/range.vue |  3 +-
 packages/client/src/components/marquee.vue    |  4 +-
 packages/client/src/scripts/use-interval.ts   |  2 +
 packages/client/src/widgets/rss-marquee.vue   | 65 +++++++++++--------
 4 files changed, 46 insertions(+), 28 deletions(-)

diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index d46174acc..387ad26f3 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -6,7 +6,7 @@
 			<div class="track">
 				<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
 			</div>
-			<div v-if="steps" class="ticks">
+			<div v-if="steps && showTicks" class="ticks">
 				<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
 			</div>
 			<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
@@ -27,6 +27,7 @@ const props = withDefaults(defineProps<{
 	max: number;
 	step?: number;
 	textConverter?: (value: number) => string,
+	showTicks?: boolean;
 }>(), {
 	step: 1,
 	textConverter: (v) => v.toString(),
diff --git a/packages/client/src/components/marquee.vue b/packages/client/src/components/marquee.vue
index 2fd76a54f..468503351 100644
--- a/packages/client/src/components/marquee.vue
+++ b/packages/client/src/components/marquee.vue
@@ -1,5 +1,5 @@
 <script lang="ts">
-import { h, onMounted, onUnmounted, ref } from 'vue';
+import { h, onMounted, onUnmounted, ref, watch } from 'vue';
 
 export default {
 	name: 'MarqueeText',
@@ -32,6 +32,8 @@ export default {
 			contentEl.value.style.animationDuration = `${duration}s`;
 		}
 
+		watch(() => props.duration, calc);
+
 		onMounted(() => {
 			calc();
 		});
diff --git a/packages/client/src/scripts/use-interval.ts b/packages/client/src/scripts/use-interval.ts
index eb6e44338..201ba417e 100644
--- a/packages/client/src/scripts/use-interval.ts
+++ b/packages/client/src/scripts/use-interval.ts
@@ -4,6 +4,8 @@ export function useInterval(fn: () => void, interval: number, options: {
 	immediate: boolean;
 	afterMounted: boolean;
 }): void {
+	if (Number.isNaN(interval)) return;
+
 	let intervalId: number | null = null;
 
 	if (options.afterMounted) {
diff --git a/packages/client/src/widgets/rss-marquee.vue b/packages/client/src/widgets/rss-marquee.vue
index 2f92c09f3..c20954c1e 100644
--- a/packages/client/src/widgets/rss-marquee.vue
+++ b/packages/client/src/widgets/rss-marquee.vue
@@ -6,11 +6,13 @@
 	<div class="ekmkgxbk">
 		<MkLoading v-if="fetching"/>
 		<div v-else class="feed">
-			<MarqueeText :key="key" :duration="widgetProps.speed" :reverse="widgetProps.reverse">
-				<span v-for="item in items" class="item">
-					<a class="link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span class="divider"></span>
-				</span>
-			</MarqueeText>
+			<transition name="change" mode="default">
+				<MarqueeText :key="key" :duration="widgetProps.duration" :reverse="widgetProps.reverse">
+					<span v-for="item in items" class="item">
+						<a class="link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span class="divider"></span>
+					</span>
+				</MarqueeText>
+			</transition>
 		</div>
 	</div>
 </MkContainer>
@@ -32,6 +34,21 @@ const widgetPropsDef = {
 		type: 'string' as const,
 		default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
 	},
+	refreshIntervalSec: {
+		type: 'number' as const,
+		default: 60,
+	},
+	duration: {
+		type: 'range' as const,
+		default: 70,
+		step: 1,
+		min: 5,
+		max: 200,
+	},
+	reverse: {
+		type: 'boolean' as const,
+		default: false,
+	},
 	showHeader: {
 		type: 'boolean' as const,
 		default: false,
@@ -40,25 +57,6 @@ const widgetPropsDef = {
 		type: 'boolean' as const,
 		default: false,
 	},
-	speed: {
-		type: 'radio' as const,
-		default: 70,
-		options: [{
-			value: 170, label: 'very slow',
-		}, {
-			value: 100, label: 'slow',
-		}, {
-			value: 70, label: 'medium',
-		}, {
-			value: 40, label: 'fast',
-		}, {
-			value: 20, label: 'very fast',
-		}],
-	},
-	reverse: {
-		type: 'boolean' as const,
-		default: false,
-	},
 };
 
 type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@@ -91,7 +89,7 @@ const tick = () => {
 
 watch(() => widgetProps.url, tick);
 
-useInterval(tick, 60000, {
+useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
 	immediate: true,
 	afterMounted: true,
 });
@@ -104,17 +102,32 @@ defineExpose<WidgetComponentExpose>({
 </script>
 
 <style lang="scss" scoped>
+.change-enter-active, .change-leave-active {
+	position: absolute;
+	top: 0;
+  transition: all 1s ease;
+}
+.change-enter-from {
+  opacity: 0;
+	transform: translateY(-100%);
+}
+.change-leave-to {
+  opacity: 0;
+	transform: translateY(100%);
+}
+
 .ekmkgxbk {
 	> .feed {
 		padding: 0;
 		font-size: 0.9em;
+		line-height: 42px;
+		height: 42px;
 
 		::v-deep(.item) {
 			display: inline-flex;
 			align-items: center;
 			vertical-align: bottom;
 			color: var(--fg);
-			margin: 12px 0;
 
 			> .divider {
 				display: inline-block;

From 69272b49a467ae5a8890afbeb8ef2d268a8b3770 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 2 Jul 2022 23:01:13 +0900
Subject: [PATCH 059/100] update eslint rules

---
 packages/client/.eslintrc.js | 3 +--
 packages/shared/.eslintrc.js | 1 +
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js
index 981b08d74..a5a4fd0f4 100644
--- a/packages/client/.eslintrc.js
+++ b/packages/client/.eslintrc.js
@@ -22,9 +22,8 @@ module.exports = {
 			},
 		],
 		// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
-		// data の禁止理由: 抽象的すぎるため
 		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
-		'id-denylist': ['error', 'window', 'data', 'e'],
+		'id-denylist': ['error', 'window', 'e'],
 		'no-shadow': ['warn'],
 		'vue/attributes-order': ['error', {
 			'alphabetical': false,
diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js
index dc5321882..3aef6484e 100644
--- a/packages/shared/.eslintrc.js
+++ b/packages/shared/.eslintrc.js
@@ -21,6 +21,7 @@ module.exports = {
 		}],
 		'eol-last': ['error', 'always'],
 		'semi': ['error', 'always'],
+		'semi-spacing': ['error', { 'before': false, 'after': true }],
 		'quotes': ['warn', 'single'],
 		'comma-dangle': ['warn', 'always-multiline'],
 		'keyword-spacing': ['error', {

From e5ccfa51588e231ee66a17ec119d4282905ad40d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 00:15:03 +0900
Subject: [PATCH 060/100] feat: moderation note

---
 CHANGELOG.md                                  |   1 +
 locales/ja-JP.yml                             |   1 +
 .../1656772790599-user-moderation-note.js     |  11 ++
 .../src/models/entities/user-profile.ts       |   7 +-
 packages/backend/src/server/api/endpoints.ts  |   2 +
 .../server/api/endpoints/admin/show-user.ts   |   1 +
 .../api/endpoints/admin/update-user-note.ts   |  31 ++++
 packages/client/src/pages/user-info.vue       | 136 ++++++++++++------
 8 files changed, 146 insertions(+), 44 deletions(-)
 create mode 100644 packages/backend/migration/1656772790599-user-moderation-note.js
 create mode 100644 packages/backend/src/server/api/endpoints/admin/update-user-note.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ddc12399..5f185f63f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ You should also include the user name that made the change.
 - Client: Add rss-marquee widget @syuilo
 - Client: Removing entries from a clip @futchitwo
 - Client: Poll highlights in explore page @syuilo
+- ユーザーにモデレーションメモを残せる機能 @syuilo
 - Make possible to delete an account by admin @syuilo
 - Improve player detection in URL preview @mei23
 - Add Badge Image to Push Notification #8012 @tamaina
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ec726c821..d333ac29d 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -381,6 +381,7 @@ administrator: "管理者"
 token: "トークン"
 twoStepAuthentication: "二段階認証"
 moderator: "モデレーター"
+moderation: "モデレーション"
 nUsersMentioned: "{n}人が投稿"
 securityKey: "セキュリティキー"
 securityKeyName: "キーの名前"
diff --git a/packages/backend/migration/1656772790599-user-moderation-note.js b/packages/backend/migration/1656772790599-user-moderation-note.js
new file mode 100644
index 000000000..133bcffe1
--- /dev/null
+++ b/packages/backend/migration/1656772790599-user-moderation-note.js
@@ -0,0 +1,11 @@
+export class userModerationNote1656772790599 {
+    name = 'userModerationNote1656772790599'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "moderationNote"`);
+    }
+}
diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts
index 1778742ea..7dfe13fe1 100644
--- a/packages/backend/src/models/entities/user-profile.ts
+++ b/packages/backend/src/models/entities/user-profile.ts
@@ -1,8 +1,8 @@
 import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+import { ffVisibility, notificationTypes } from '@/types.js';
 import { id } from '../id.js';
 import { User } from './user.js';
 import { Page } from './page.js';
-import { ffVisibility, notificationTypes } from '@/types.js';
 
 // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
 //       ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
@@ -117,6 +117,11 @@ export class UserProfile {
 	})
 	public password: string | null;
 
+	@Column('varchar', {
+		length: 8192, default: '',
+	})
+	public moderationNote: string | null;
+
 	// TODO: そのうち消す
 	@Column('jsonb', {
 		default: {},
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index f45876392..4a2ecebd8 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -61,6 +61,7 @@ import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
 import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
 import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
 import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
+import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
 import * as ep___announcements from './endpoints/announcements.js';
 import * as ep___antennas_create from './endpoints/antennas/create.js';
 import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -376,6 +377,7 @@ const eps = [
 	['admin/update-meta', ep___admin_updateMeta],
 	['admin/vacuum', ep___admin_vacuum],
 	['admin/delete-account', ep___admin_deleteAccount],
+	['admin/update-user-note', ep___admin_updateUserNote],
 	['announcements', ep___announcements],
 	['antennas/create', ep___antennas_create],
 	['antennas/delete', ep___antennas_delete],
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 36384c2b3..f04a7a67c 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -69,6 +69,7 @@ export default define(meta, paramDef, async (ps, me) => {
 		isSilenced: user.isSilenced,
 		isSuspended: user.isSuspended,
 		lastActiveDate: user.lastActiveDate,
+		moderationNote: profile.moderationNote,
 		signins,
 	};
 });
diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts
new file mode 100644
index 000000000..fa21ab783
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts
@@ -0,0 +1,31 @@
+import { UserProfiles, Users } from '@/models/index.js';
+import define from '../../define.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		userId: { type: 'string', format: 'misskey:id' },
+		text: { type: 'string' },
+	},
+	required: ['userId', 'text'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, paramDef, async (ps, me) => {
+	const user = await Users.findOneBy({ id: ps.userId });
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	await UserProfiles.update({ userId: user.id }, {
+		moderationNote: ps.text,
+	});
+});
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index f9edd208a..204ece7eb 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -9,6 +9,11 @@
 					<div class="body">
 						<span class="name"><MkUserName class="name" :user="user"/></span>
 						<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
+						<span class="state">
+							<span v-if="suspended" class="suspended">Suspended</span>
+							<span v-if="silenced" class="silenced">Silenced</span>
+							<span v-if="moderator" class="moderator">Moderator</span>
+						</span>
 					</div>
 				</div>
 
@@ -41,20 +46,12 @@
 						<template #key>{{ i18n.ts.lastActiveDate }}</template>
 						<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
 					</MkKeyValue>
+					<MkKeyValue v-if="info" oneline style="margin: 1em 0;">
+						<template #key>{{ i18n.ts.email }}</template>
+						<template #value><span class="_monospace">{{ info.email }}</span></template>
+					</MkKeyValue>
 				</div>
 
-				<FormSection v-if="iAmModerator">
-					<template #label>Moderation</template>
-					<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch>
-					<FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
-					<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch>
-					{{ $ts.reflectMayTakeTime }}
-					<div class="_formBlock">
-						<FormButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
-						<FormButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ $ts.deleteAccount }}</FormButton>
-					</div>
-				</FormSection>
-
 				<FormSection>
 					<template #label>ActivityPub</template>
 
@@ -78,8 +75,44 @@
 					</div>
 
 					<FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
+
+					<FormFolder class="_formBlock">
+						<template #label>Raw</template>
+
+						<MkObjectView v-if="ap" tall :value="ap">
+						</MkObjectView>
+					</FormFolder>
 				</FormSection>
 			</div>
+			<div v-else-if="tab === 'moderation'" class="_formRoot">
+				<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch>
+				<FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
+				<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch>
+				{{ $ts.reflectMayTakeTime }}
+				<div class="_formBlock">
+					<FormButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
+					<FormButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ $ts.deleteAccount }}</FormButton>
+				</div>
+				<FormTextarea v-model="moderationNote" manual-save class="_formBlock">
+					<template #label>Moderation note</template>
+				</FormTextarea>
+				<FormFolder class="_formBlock">
+					<template #label>IP</template>
+					<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
+					<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
+					<template v-if="iAmAdmin && ips">
+						<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
+							<span class="date">{{ record.createdAt }}</span>
+							<span class="ip">{{ record.ip }}</span>
+						</div>
+					</template>
+				</FormFolder>
+				<FormFolder class="_formBlock">
+					<template #label>{{ i18n.ts.files }}</template>
+
+					<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
+				</FormFolder>
+			</div>
 			<div v-else-if="tab === 'chart'" class="_formRoot">
 				<div class="cmhjzshm">
 					<div class="selects">
@@ -95,23 +128,6 @@
 					</div>
 				</div>
 			</div>
-			<div v-else-if="tab === 'files'" class="_formRoot">
-				<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
-			</div>
-			<div v-else-if="tab === 'ip'" class="_formRoot">
-				<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
-				<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
-				<template v-if="iAmAdmin && ips">
-					<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
-						<span class="date">{{ record.createdAt }}</span>
-						<span class="ip">{{ record.ip }}</span>
-					</div>
-				</template>
-			</div>
-			<div v-else-if="tab === 'ap'" class="_formRoot">
-				<MkObjectView v-if="ap" tall :value="ap">
-				</MkObjectView>
-			</div>
 			<div v-else-if="tab === 'raw'" class="_formRoot">
 				<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
 				</MkObjectView>
@@ -134,6 +150,7 @@ import FormSwitch from '@/components/form/switch.vue';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import FormButton from '@/components/ui/button.vue';
+import FormFolder from '@/components/form/folder.vue';
 import MkKeyValue from '@/components/key-value.vue';
 import MkSelect from '@/components/form/select.vue';
 import FormSuspense from '@/components/form/suspense.vue';
@@ -162,6 +179,7 @@ let ap = $ref(null);
 let moderator = $ref(false);
 let silenced = $ref(false);
 let suspended = $ref(false);
+let moderationNote = $ref('');
 const filesPagination = {
 	endpoint: 'admin/drive/files' as const,
 	limit: 10,
@@ -185,6 +203,12 @@ function createFetcher() {
 			moderator = info.isModerator;
 			silenced = info.isSilenced;
 			suspended = info.isSuspended;
+			moderationNote = info.moderationNote;
+
+			watch($$(moderationNote), async () => {
+				await os.api('admin/update-user-note', { userId: user.id, text: moderationNote });
+				await refreshUser();
+			});
 		});
 	} else {
 		return () => os.api('users/show', {
@@ -309,23 +333,15 @@ const headerTabs = $computed(() => [{
 	key: 'overview',
 	title: i18n.ts.overview,
 	icon: 'fas fa-info-circle',
-}, {
+}, iAmModerator ? {
+	key: 'moderation',
+	title: i18n.ts.moderation,
+	icon: 'fas fa-shield-halved',
+} : null, {
 	key: 'chart',
 	title: i18n.ts.charts,
 	icon: 'fas fa-chart-simple',
-}, iAmModerator ? {
-	key: 'files',
-	title: i18n.ts.files,
-	icon: 'fas fa-cloud',
-} : null, {
-	key: 'ap',
-	title: 'AP',
-	icon: 'fas fa-share-alt',
-}, iAmModerator ? {
-	key: 'ip',
-	title: 'IP',
-	icon: 'fas fa-bars-staggered',
-} : null, {
+}, {
 	key: 'raw',
 	title: 'Raw',
 	icon: 'fas fa-code',
@@ -370,6 +386,40 @@ definePageMetadata(computed(() => ({
 			overflow: hidden;
 			text-overflow: ellipsis;
 		}
+
+		> .state {
+			display: flex;
+			gap: 8px;
+			flex-wrap: wrap;
+			margin-top: 4px;
+
+			&:empty {
+				display: none;
+			}
+
+			> .suspended, > .silenced, > .moderator {
+				display: inline-block;
+				border: solid 1px;
+				border-radius: 6px;
+				padding: 2px 6px;
+				font-size: 85%;
+			}
+
+			> .suspended {
+				color: var(--error);
+				border-color: var(--error);
+			}
+
+			> .silenced {
+				color: var(--warn);
+				border-color: var(--warn);
+			}
+
+			> .moderator {
+				color: var(--success);
+				border-color: var(--success);
+			}
+		}
 	}
 }
 

From 110f7af0f5c2a5a4228d86f9f9f7b74b4a83e768 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 00:24:49 +0900
Subject: [PATCH 061/100] 12.112.0-beta.13

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

diff --git a/package.json b/package.json
index 03165b762..a661bc648 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.12",
+	"version": "12.112.0-beta.13",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 58ebe2ed0558a5c1ffdc3c69727196456032eb62 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 14:40:02 +0900
Subject: [PATCH 062/100] feat(client): status bar (experimental)

---
 locales/ja-JP.yml                             |   2 +
 .../components/global/sticky-container.vue    |   8 +-
 packages/client/src/pages/settings/index.vue  |   6 +
 .../pages/settings/statusbars.statusbar.vue   | 122 ++++++++++++++++++
 .../client/src/pages/settings/statusbars.vue  |  61 +++++++++
 packages/client/src/store.ts                  |  13 ++
 .../src/ui/_common_/statusbar-federation.vue  | 103 +++++++++++++++
 .../client/src/ui/_common_/statusbar-rss.vue  |  88 +++++++++++++
 .../src/ui/_common_/statusbar-user-list.vue   | 104 +++++++++++++++
 .../client/src/ui/_common_/statusbars.vue     |  75 +++++++++++
 packages/client/src/ui/deck.vue               |  86 +++++++-----
 packages/client/src/ui/universal.vue          |  41 +++---
 12 files changed, 658 insertions(+), 51 deletions(-)
 create mode 100644 packages/client/src/pages/settings/statusbars.statusbar.vue
 create mode 100644 packages/client/src/pages/settings/statusbars.vue
 create mode 100644 packages/client/src/ui/_common_/statusbar-federation.vue
 create mode 100644 packages/client/src/ui/_common_/statusbar-rss.vue
 create mode 100644 packages/client/src/ui/_common_/statusbar-user-list.vue
 create mode 100644 packages/client/src/ui/_common_/statusbars.vue

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index d333ac29d..01d001688 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -864,6 +864,8 @@ numberOfPageCache: "ページキャッシュ数"
 numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
 logoutConfirm: "ログアウトしますか?"
 lastActiveDate: "最終利用日時"
+statusbar: "ステータスバー"
+pleaseSelect: "選択してください"
 
 _emailUnavailable:
   used: "既に使用されています"
diff --git a/packages/client/src/components/global/sticky-container.vue b/packages/client/src/components/global/sticky-container.vue
index 2603fac55..44f4f065a 100644
--- a/packages/client/src/components/global/sticky-container.vue
+++ b/packages/client/src/components/global/sticky-container.vue
@@ -9,11 +9,15 @@
 </div>
 </template>
 
+<script lang="ts">
+// なんか動かない
+//const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
+const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
+</script>
+
 <script lang="ts" setup>
 import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
 
-const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
-
 const rootEl = $ref<HTMLElement>();
 const headerEl = $ref<HTMLElement>();
 const bodyEl = $ref<HTMLElement>();
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 8e445a77d..76410ec12 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -113,6 +113,11 @@ const menuDef = computed(() => [{
 		text: i18n.ts.theme,
 		to: '/settings/theme',
 		active: props.initialPage === 'theme',
+	}, {
+		icon: 'fas fa-list-ul',
+		text: i18n.ts.statusbar,
+		to: '/settings/statusbars',
+		active: props.initialPage === 'statusbars',
 	}, {
 		icon: 'fas fa-list-ul',
 		text: i18n.ts.menu,
@@ -221,6 +226,7 @@ const component = computed(() => {
 		case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
 		case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
 		case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
+		case 'statusbars': return defineAsyncComponent(() => import('./statusbars.vue'));
 		case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
 		case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
 		case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
diff --git a/packages/client/src/pages/settings/statusbars.statusbar.vue b/packages/client/src/pages/settings/statusbars.statusbar.vue
new file mode 100644
index 000000000..ad2fa557a
--- /dev/null
+++ b/packages/client/src/pages/settings/statusbars.statusbar.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="_formRoot">
+	<FormSelect v-model="statusbar.type" placeholder="Please select" class="_formBlock">
+		<template #label>{{ i18n.ts.type }}</template>
+		<option value="rss">RSS</option>
+		<option value="federation">Federation</option>
+		<option value="userList">User list timeline</option>
+	</FormSelect>
+
+	<MkInput v-model="statusbar.name" class="_formBlock">
+		<template #label>Name</template>
+	</MkInput>
+
+	<MkSwitch v-model="statusbar.black" class="_formBlock">
+		<template #label>Black</template>
+	</MkSwitch>
+
+	<template v-if="statusbar.type === 'rss'">
+		<MkInput v-model="statusbar.props.url" class="_formBlock" type="url">
+			<template #label>URL</template>
+		</MkInput>
+		<MkInput v-model="statusbar.props.refreshIntervalSec" class="_formBlock" type="number">
+			<template #label>Refresh interval</template>
+		</MkInput>
+		<MkInput v-model="statusbar.props.marqueeDuration" class="_formBlock" type="number">
+			<template #label>Duration</template>
+		</MkInput>
+		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+			<template #label>Reverse</template>
+		</MkSwitch>
+	</template>
+	<template v-else-if="statusbar.type === 'federation'">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" class="_formBlock" type="number">
+			<template #label>Refresh interval</template>
+		</MkInput>
+		<MkInput v-model="statusbar.props.marqueeDuration" class="_formBlock" type="number">
+			<template #label>Duration</template>
+		</MkInput>
+		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+			<template #label>Reverse</template>
+		</MkSwitch>
+		<MkSwitch v-model="statusbar.props.colored" class="_formBlock">
+			<template #label>Colored</template>
+		</MkSwitch>
+	</template>
+	<template v-else-if="statusbar.type === 'userList' && userLists != null">
+		<FormSelect v-model="statusbar.props.userListId" class="_formBlock">
+			<template #label>{{ i18n.ts.userList }}</template>
+			<option v-for="list in userLists" :value="list.id">{{ list.name }}</option>
+		</FormSelect>
+		<MkInput v-model="statusbar.props.refreshIntervalSec" class="_formBlock" type="number">
+			<template #label>Refresh interval</template>
+		</MkInput>
+		<MkInput v-model="statusbar.props.marqueeDuration" class="_formBlock" type="number">
+			<template #label>Duration</template>
+		</MkInput>
+		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+			<template #label>Reverse</template>
+		</MkSwitch>
+	</template>
+
+	<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+		<FormButton @click="save">save</FormButton>
+		<FormButton danger @click="del">Delete</FormButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, reactive, ref, watch } from 'vue';
+import FormSelect from '@/components/form/select.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
+
+const props = defineProps<{
+	_id: string;
+	userLists: any[] | null;
+}>();
+
+const statusbar = reactive(JSON.parse(JSON.stringify(defaultStore.state.statusbars.find(x => x.id === props._id))));
+
+watch(() => statusbar.type, () => {
+	if (statusbar.type === 'rss') {
+		statusbar.name = 'NEWS';
+		statusbar.props.url = 'http://feeds.afpbb.com/rss/afpbb/afpbbnews';
+		statusbar.props.refreshIntervalSec = 120;
+		statusbar.props.display = 'marquee';
+		statusbar.props.marqueeDuration = 100;
+		statusbar.props.marqueeReverse = false;
+	} else if (statusbar.type === 'federation') {
+		statusbar.name = 'FEDERATION';
+		statusbar.props.refreshIntervalSec = 120;
+		statusbar.props.display = 'marquee';
+		statusbar.props.marqueeDuration = 100;
+		statusbar.props.marqueeReverse = false;
+		statusbar.props.colored = false;
+	} else if (statusbar.type === 'userList') {
+		statusbar.name = 'LIST TL';
+		statusbar.props.refreshIntervalSec = 120;
+		statusbar.props.display = 'marquee';
+		statusbar.props.marqueeDuration = 100;
+		statusbar.props.marqueeReverse = false;
+	}
+});
+
+async function save() {
+	const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
+	const statusbars = JSON.parse(JSON.stringify(defaultStore.state.statusbars));
+	statusbars[i] = JSON.parse(JSON.stringify(statusbar));
+	defaultStore.set('statusbars', statusbars);
+}
+
+function del() {
+	defaultStore.set('statusbars', defaultStore.state.statusbars.filter(x => x.id !== props._id));
+}
+</script>
diff --git a/packages/client/src/pages/settings/statusbars.vue b/packages/client/src/pages/settings/statusbars.vue
new file mode 100644
index 000000000..dea5e0ffd
--- /dev/null
+++ b/packages/client/src/pages/settings/statusbars.vue
@@ -0,0 +1,61 @@
+<template>
+<div class="_formRoot">
+	<FormFolder v-for="x in statusbars" :key="x.id" class="_formBlock">
+		<template #label>{{ x.type ?? i18n.ts.notSet }}</template>
+		<template #suffix>{{ x.name }}</template>
+		<XStatusbar :_id="x.id" :user-lists="userLists"/>
+	</FormFolder>
+	<FormButton @click="add">add</FormButton>
+	<FormRadios v-model="statusbarSize" class="_formBlock">
+		<template #label>Size</template>
+		<option value="verySmall">{{ i18n.ts.small }}+</option>
+		<option value="small">{{ i18n.ts.small }}</option>
+		<option value="medium">{{ i18n.ts.medium }}</option>
+		<option value="large">{{ i18n.ts.large }}</option>
+	</FormRadios>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, ref, watch } from 'vue';
+import { v4 as uuid } from 'uuid';
+import XStatusbar from './statusbars.statusbar.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormFolder from '@/components/form/folder.vue';
+import FormButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+import { unisonReload } from '@/scripts/unison-reload';
+import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
+
+const statusbarSize = computed(defaultStore.makeGetterSetter('statusbarSize'));
+const statusbars = defaultStore.reactiveState.statusbars;
+
+let userLists = $ref();
+
+onMounted(() => {
+	os.api('users/lists/list').then(res => {
+		userLists = res;
+	});
+});
+
+async function add() {
+	defaultStore.push('statusbars', {
+		id: uuid(),
+		type: null,
+		black: false,
+		props: {},
+	});
+}
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+	title: i18n.ts.statusbar,
+	icon: 'fas fa-list-ul',
+	bg: 'var(--bg)',
+});
+</script>
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index 94d9d9138..cde907017 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -88,6 +88,19 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'deviceAccount',
 		default: false,
 	},
+	statusbars: {
+		where: 'deviceAccount',
+		default: [] as {
+			name: string;
+			id: string;
+			type: string;
+			props: Record<string, any>;
+		}[],
+	},
+	statusbarSize: {
+		where: 'deviceAccount',
+		default: 'medium',
+	},
 	widgets: {
 		where: 'deviceAccount',
 		default: [] as {
diff --git a/packages/client/src/ui/_common_/statusbar-federation.vue b/packages/client/src/ui/_common_/statusbar-federation.vue
new file mode 100644
index 000000000..87b954b90
--- /dev/null
+++ b/packages/client/src/ui/_common_/statusbar-federation.vue
@@ -0,0 +1,103 @@
+<template>
+<span v-if="!fetching" class="nmidsaqw">
+	<template v-if="display === 'marquee'">
+		<transition name="change" mode="default">
+			<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
+				<span v-for="instance in instances" :key="instance.id" class="item" :class="{ colored }" :style="{ background: colored ? instance.themeColor : null }">
+					<img v-if="instance.iconUrl" class="icon" :src="instance.iconUrl" alt=""/>
+					<MkA :to="`/instance-info/${instance.host}`" class="host _monospace">
+						{{ instance.host }}
+					</MkA>
+					<span class="divider"></span>
+				</span>
+			</MarqueeText>
+		</transition>
+	</template>
+	<template v-else-if="display === 'oneByOne'">
+		<!-- TODO -->
+	</template>
+</span>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
+import * as misskey from 'misskey-js';
+import MarqueeText from '@/components/marquee.vue';
+import * as os from '@/os';
+import { useInterval } from '@/scripts/use-interval';
+import { getNoteSummary } from '@/scripts/get-note-summary';
+import { notePage } from '@/filters/note';
+
+const props = defineProps<{
+	display?: 'marquee' | 'oneByOne';
+	colored?: boolean;
+	marqueeDuration?: number;
+	marqueeReverse?: boolean;
+	oneByOneInterval?: number;
+	refreshIntervalSec?: number;
+}>();
+
+const instances = ref<misskey.entities.Instance[]>([]);
+const fetching = ref(true);
+let key = $ref(0);
+
+const tick = () => {
+	os.api('federation/instances', {
+		sort: '+lastCommunicatedAt',
+		limit: 30,
+	}).then(res => {
+		instances.value = res;
+		fetching.value = false;
+		key++;
+	});
+};
+
+useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
+	immediate: true,
+	afterMounted: true,
+});
+</script>
+
+<style lang="scss" scoped>
+.change-enter-active, .change-leave-active {
+	position: absolute;
+	top: 0;
+  transition: all 1s ease;
+}
+.change-enter-from {
+  opacity: 0;
+	transform: translateY(-100%);
+}
+.change-leave-to {
+  opacity: 0;
+	transform: translateY(100%);
+}
+
+.nmidsaqw {
+	display: inline-block;
+	position: relative;
+
+	::v-deep(.item) {
+		display: inline-block;
+		vertical-align: bottom;
+		margin-right: 3em;
+
+		> .icon {
+			display: inline-block;
+			height: var(--height);
+			aspect-ratio: 1;
+			vertical-align: bottom;
+			margin-right: 1em;
+		}
+
+		> .host {
+			vertical-align: bottom;
+		}
+
+		&.colored {
+			padding-right: 1em;
+			color: #fff;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/_common_/statusbar-rss.vue b/packages/client/src/ui/_common_/statusbar-rss.vue
new file mode 100644
index 000000000..ddfc6faaa
--- /dev/null
+++ b/packages/client/src/ui/_common_/statusbar-rss.vue
@@ -0,0 +1,88 @@
+<template>
+<span v-if="!fetching" class="xbhtxfms">
+	<template v-if="display === 'marquee'">
+		<transition name="change" mode="default">
+			<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
+				<span v-for="item in items" class="item">
+					<a class="link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span class="divider"></span>
+				</span>
+			</MarqueeText>
+		</transition>
+	</template>
+	<template v-else-if="display === 'oneByOne'">
+		<!-- TODO -->
+	</template>
+</span>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
+import MarqueeText from '@/components/marquee.vue';
+import * as os from '@/os';
+import { useInterval } from '@/scripts/use-interval';
+
+const props = defineProps<{
+	url?: string;
+	display?: 'marquee' | 'oneByOne';
+	marqueeDuration?: number;
+	marqueeReverse?: boolean;
+	oneByOneInterval?: number;
+	refreshIntervalSec?: number;
+}>();
+
+const items = ref([]);
+const fetching = ref(true);
+let key = $ref(0);
+
+const tick = () => {
+	fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
+		res.json().then(feed => {
+			items.value = feed.items;
+			fetching.value = false;
+			key++;
+		});
+	});
+};
+
+useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
+	immediate: true,
+	afterMounted: true,
+});
+</script>
+
+<style lang="scss" scoped>
+.change-enter-active, .change-leave-active {
+	position: absolute;
+	top: 0;
+  transition: all 1s ease;
+}
+.change-enter-from {
+  opacity: 0;
+	transform: translateY(-100%);
+}
+.change-leave-to {
+  opacity: 0;
+	transform: translateY(100%);
+}
+
+.xbhtxfms {
+	display: inline-block;
+	position: relative;
+
+	::v-deep(.item) {
+		display: inline-flex;
+		align-items: center;
+		vertical-align: bottom;
+		margin: 0;
+
+		> .divider {
+			display: inline-block;
+			width: 0.5px;
+			height: var(--height);
+			margin: 0 1em;
+			background: currentColor;
+			opacity: 0.7;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/_common_/statusbar-user-list.vue b/packages/client/src/ui/_common_/statusbar-user-list.vue
new file mode 100644
index 000000000..01240dc6b
--- /dev/null
+++ b/packages/client/src/ui/_common_/statusbar-user-list.vue
@@ -0,0 +1,104 @@
+<template>
+<span v-if="!fetching" class="osdsvwzy">
+	<template v-if="display === 'marquee'">
+		<transition name="change" mode="default">
+			<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
+				<span v-for="note in notes" :key="note.id" class="item">
+					<img class="avatar" :src="note.user.avatarUrl" decoding="async"/>
+					<MkA class="text" :to="notePage(note)">
+						<Mfm :text="getNoteSummary(note)" :plain="true" :nowrap="true" :custom-emojis="note.emojis"/>
+					</MkA>
+					<span class="divider"></span>
+				</span>
+			</MarqueeText>
+		</transition>
+	</template>
+	<template v-else-if="display === 'oneByOne'">
+		<!-- TODO -->
+	</template>
+</span>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
+import * as misskey from 'misskey-js';
+import MarqueeText from '@/components/marquee.vue';
+import * as os from '@/os';
+import { useInterval } from '@/scripts/use-interval';
+import { getNoteSummary } from '@/scripts/get-note-summary';
+import { notePage } from '@/filters/note';
+
+const props = defineProps<{
+	userListId?: string;
+	display?: 'marquee' | 'oneByOne';
+	marqueeDuration?: number;
+	marqueeReverse?: boolean;
+	oneByOneInterval?: number;
+	refreshIntervalSec?: number;
+}>();
+
+const notes = ref<misskey.entities.Note[]>([]);
+const fetching = ref(true);
+let key = $ref(0);
+
+const tick = () => {
+	if (props.userListId == null) return;
+	os.api('notes/user-list-timeline', {
+		listId: props.userListId,
+	}).then(res => {
+		notes.value = res;
+		fetching.value = false;
+		key++;
+	});
+};
+
+useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
+	immediate: true,
+	afterMounted: true,
+});
+</script>
+
+<style lang="scss" scoped>
+.change-enter-active, .change-leave-active {
+	position: absolute;
+	top: 0;
+  transition: all 1s ease;
+}
+.change-enter-from {
+  opacity: 0;
+	transform: translateY(-100%);
+}
+.change-leave-to {
+  opacity: 0;
+	transform: translateY(100%);
+}
+
+.osdsvwzy {
+	display: inline-block;
+	position: relative;
+
+	::v-deep(.item) {
+		display: inline-flex;
+		align-items: center;
+		vertical-align: bottom;
+		margin: 0;
+
+		> .avatar {
+			display: inline-block;
+			height: var(--height);
+			aspect-ratio: 1;
+			vertical-align: bottom;
+			margin-right: 8px;
+		}
+
+		> .divider {
+			display: inline-block;
+			width: 0.5px;
+			height: 16px;
+			margin: 0 1em;
+			background: currentColor;
+			opacity: 0;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue
new file mode 100644
index 000000000..86d2812f5
--- /dev/null
+++ b/packages/client/src/ui/_common_/statusbars.vue
@@ -0,0 +1,75 @@
+<template>
+<div
+	class="dlrsnxqu" :class="{
+		verySmall: defaultStore.reactiveState.statusbarSize.value === 'verySmall',
+		small: defaultStore.reactiveState.statusbarSize.value === 'small',
+		medium: defaultStore.reactiveState.statusbarSize.value === 'medium',
+		large: defaultStore.reactiveState.statusbarSize.value === 'large'
+	}"
+>
+	<div v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" class="item" :class="{ black: x.black }">
+		<span class="name">{{ x.name }}</span>
+		<XRss v-if="x.type === 'rss'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url"/>
+		<XFederation v-else-if="x.type === 'federation'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
+		<XUserList v-else-if="x.type === 'userList'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :user-list-id="x.props.userListId"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue'));
+const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue'));
+const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue'));
+</script>
+
+<style lang="scss" scoped>
+.dlrsnxqu {
+	--height: 24px;
+	background: var(--panel);
+	font-size: 0.85em;
+
+	&.verySmall {
+		--height: 16px;
+		font-size: 0.75em;
+	}
+
+	&.small {
+		--height: 20px;
+		font-size: 0.8em;
+	}
+
+	&.large {
+		--height: 26px;
+		font-size: 0.875em;
+	}
+
+	> .item {
+		display: inline-flex;
+		vertical-align: bottom;
+		width: 100%;
+		line-height: var(--height);
+		height: var(--height);
+		overflow: clip;
+		contain: strict;
+
+		> .name {
+			padding: 0 6px;
+			font-weight: bold;
+			color: var(--accent);
+		}
+
+		> .body {
+			min-width: 0;
+			flex: 1;
+		}
+
+		&.black {
+			background: #000;
+			color: #fff;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index b3b9ddd55..111cf8022 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -5,26 +5,31 @@
 >
 	<XSidebar v-if="!isMobile"/>
 
-	<template v-for="ids in layout">
-		<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
-		<section
-			v-if="ids.length > 1"
-			class="folder column"
-			:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
-		>
-			<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
-		</section>
-		<DeckColumnCore
-			v-else
-			:ref="ids[0]"
-			:key="ids[0]"
-			class="column"
-			:column="columns.find(c => c.id === ids[0])"
-			:is-stacked="false"
-			:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
-			@parent-focus="moveFocus(ids[0], $event)"
-		/>
-	</template>
+	<div class="main">
+		<XStatusBars class="statusbars"/>
+		<div ref="columnsEl" class="columns">
+			<template v-for="ids in layout">
+				<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
+				<section
+					v-if="ids.length > 1"
+					class="folder column"
+					:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
+				>
+					<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
+				</section>
+				<DeckColumnCore
+					v-else
+					:ref="ids[0]"
+					:key="ids[0]"
+					class="column"
+					:column="columns.find(c => c.id === ids[0])"
+					:is-stacked="false"
+					:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
+					@parent-focus="moveFocus(ids[0], $event)"
+				/>
+			</template>
+		</div>
+	</div>
 
 	<div v-if="isMobile" class="buttons">
 		<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
@@ -51,7 +56,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, provide, ref, watch } from 'vue';
+import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue';
 import { v4 as uuid } from 'uuid';
 import XCommon from './_common_/common.vue';
 import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
@@ -64,6 +69,7 @@ import { menuDef } from '@/menu';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { mainRouter } from '@/router';
+const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 
 if (deckStore.state.navWindow) {
 	mainRouter.navHook = (path) => {
@@ -94,6 +100,8 @@ const menuIndicated = computed(() => {
 	return false;
 });
 
+let columnsEl = $ref<HTMLElement>();
+
 const addColumn = async (ev) => {
 	const columns = [
 		'main',
@@ -134,8 +142,10 @@ provide('shouldSpacerMin', true);
 document.documentElement.style.overflowY = 'hidden';
 document.documentElement.style.scrollBehavior = 'auto';
 window.addEventListener('wheel', (ev) => {
-	if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
-		document.documentElement.scrollLeft += ev.deltaY;
+	if (ev.target === columnsEl && ev.deltaX === 0) {
+		columnsEl.scrollLeft += ev.deltaY;
+	} else if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
+		columnsEl.scrollLeft += ev.deltaY;
 	}
 });
 loadDeck();
@@ -179,7 +189,6 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
 	height: calc(var(--vh, 1vh) * 100);
 	box-sizing: border-box;
 	flex: 1;
-	padding: var(--deckMargin);
 
 	&.center {
 		> .column:first-of-type {
@@ -195,16 +204,31 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
 		padding-bottom: 100px;
 	}
 
-	> .column {
-		flex-shrink: 0;
-		margin-right: var(--deckMargin);
+	> .main {
+		flex: 1;
+		min-width: 0;
+		display: flex;
+		flex-direction: column;
 
-		&.folder {
+		> .columns {
 			display: flex;
-			flex-direction: column;
+			flex: 1;
+			padding: var(--deckMargin);
+			overflow-x: auto;
+			overflow-y: clip;
 
-			> *:not(:last-child) {
-				margin-bottom: var(--deckMargin);
+			> .column {
+				flex-shrink: 0;
+				margin-right: var(--deckMargin);
+
+				&.folder {
+					display: flex;
+					flex-direction: column;
+
+					> *:not(:last-child) {
+						margin-bottom: var(--deckMargin);
+					}
+				}
 			}
 		}
 	}
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 3614f7de5..8c48510a4 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -2,14 +2,15 @@
 <div class="dkgtipfy" :class="{ wallpaper }">
 	<XSidebar v-if="!isMobile" class="sidebar"/>
 
-	<div class="contents" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
-		<main>
-			<div class="content">
+	<MkStickyContainer class="contents">
+		<template #header><XStatusBars :class="$style.statusbars"/></template>
+		<main style="min-width: 0;" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
+			<div :class="$style.content">
 				<RouterView/>
 			</div>
-			<div class="spacer"></div>
+			<div :class="$style.spacer"></div>
 		</main>
-	</div>
+	</MkStickyContainer>
 
 	<div v-if="isDesktop" ref="widgetsEl" class="widgets">
 		<XWidgets @mounted="attachSticky"/>
@@ -71,6 +72,7 @@ import { mainRouter } from '@/router';
 import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/sidebar.vue'));
+const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 
 const DESKTOP_THRESHOLD = 1100;
 const MOBILE_THRESHOLD = 500;
@@ -235,18 +237,6 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
 		width: 100%;
 		min-width: 0;
 		background: var(--bg);
-
-		> main {
-			min-width: 0;
-
-			> .spacer {
-				height: calc(env(safe-area-inset-bottom, 0px) + 96px);
-
-				@media (min-width: ($widgets-hide-threshold + 1px)) {
-					display: none;
-				}
-			}
-		}
 	}
 
 	> .widgets {
@@ -396,5 +386,20 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
 }
 </style>
 
-<style lang="scss">
+<style lang="scss" module>
+.statusbars {
+	position: sticky;
+	top: 0;
+	left: 0;
+}
+
+.spacer {
+	$widgets-hide-threshold: 1090px;
+
+	height: calc(env(safe-area-inset-bottom, 0px) + 96px);
+
+	@media (min-width: ($widgets-hide-threshold + 1px)) {
+		display: none;
+	}
+}
 </style>

From dddb39430272b417979c2979997f57fc88df82ff Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 14:43:28 +0900
Subject: [PATCH 063/100] chore(client): tweak style

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

diff --git a/packages/client/src/pages/explore.featured.vue b/packages/client/src/pages/explore.featured.vue
index ecb68928a..0f32804b7 100644
--- a/packages/client/src/pages/explore.featured.vue
+++ b/packages/client/src/pages/explore.featured.vue
@@ -1,6 +1,6 @@
 <template>
 <MkSpacer :content-max="800">
-	<MkTab v-model="tab">
+	<MkTab v-model="tab" style="margin-bottom: var(--margin);">
 		<option value="notes">{{ i18n.ts.notes }}</option>
 		<option value="polls">{{ i18n.ts.poll }}</option>
 	</MkTab>

From 136cb465153218f86b7bfabfaf77215556aabba7 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 14:44:18 +0900
Subject: [PATCH 064/100] =?UTF-8?q?fix(client):=20=E3=83=95=E3=82=A9?=
 =?UTF-8?q?=E3=83=AD=E3=83=AF=E3=83=BC=E4=B8=80=E8=A6=A7=E3=81=8C=E3=83=95?=
 =?UTF-8?q?=E3=82=A9=E3=83=AD=E3=83=BC=E3=81=AB=E3=81=AB=E3=82=83=E3=81=A3?=
 =?UTF-8?q?=E3=81=A6=E3=81=84=E3=82=8B=E3=82=93=E3=81=A0=E3=81=AB=E3=82=83?=
 =?UTF-8?q?=E3=81=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/packages/client/src/pages/user/followers.vue b/packages/client/src/pages/user/followers.vue
index c0c0e01d7..296a4b7b4 100644
--- a/packages/client/src/pages/user/followers.vue
+++ b/packages/client/src/pages/user/followers.vue
@@ -4,7 +4,7 @@
 	<MkSpacer :content-max="1000">
 		<transition name="fade" mode="out-in">
 			<div v-if="user">
-				<XFollowList :user="user" type="following"/>
+				<XFollowList :user="user" type="followers"/>
 			</div>
 			<MkError v-else-if="error" @retry="fetch()"/>
 			<MkLoading v-else/>

From 11e4df92accfadb6c033db87e977ca4f41b4440d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 14:45:20 +0900
Subject: [PATCH 065/100] 12.112.0-beta.14

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

diff --git a/package.json b/package.json
index a661bc648..0e6b5b7a8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.13",
+	"version": "12.112.0-beta.14",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 569cc28025932190c88d5f3d2cbaa253dbe41ce0 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 16:17:31 +0900
Subject: [PATCH 066/100] fix(client): style tweak for ios

---
 .../src/components/drive-file-thumbnail.vue   |  2 +-
 packages/client/src/components/form/range.vue |  2 +-
 packages/client/src/components/marquee.vue    |  2 +-
 .../client/src/components/note-preview.vue    |  2 +-
 .../client/src/components/note-simple.vue     |  2 +-
 packages/client/src/components/note.vue       |  2 +-
 packages/client/src/components/tag-cloud.vue  |  2 +-
 packages/client/src/components/toast.vue      |  2 +-
 packages/client/src/components/ui/button.vue  |  2 +-
 .../client/src/components/ui/container.vue    | 25 ++++++++++---------
 packages/client/src/components/ui/modal.vue   |  2 +-
 packages/client/src/pages/about-misskey.vue   |  2 +-
 packages/client/src/pages/about.vue           |  2 +-
 packages/client/src/pages/admin/overview.vue  |  4 +--
 .../client/src/pages/antenna-timeline.vue     |  2 +-
 .../client/src/pages/settings/profile.vue     |  2 +-
 packages/client/src/pages/settings/theme.vue  |  2 +-
 packages/client/src/pages/timeline.vue        |  2 +-
 .../client/src/pages/user-list-timeline.vue   |  2 +-
 .../client/src/pages/user/index.photos.vue    |  2 +-
 .../client/src/pages/welcome.entrance.a.vue   |  2 +-
 packages/client/src/style.scss                |  2 +-
 .../client/src/ui/_common_/statusbars.vue     |  2 +-
 packages/client/src/ui/classic.vue            |  2 +-
 24 files changed, 37 insertions(+), 36 deletions(-)

diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue
index b346585ce..16c77f726 100644
--- a/packages/client/src/components/drive-file-thumbnail.vue
+++ b/packages/client/src/components/drive-file-thumbnail.vue
@@ -59,7 +59,7 @@ const isThumbnailAvailable = computed(() => {
 	display: flex;
 	background: var(--panel);
 	border-radius: 8px;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 
 	> .icon-sub {
 		position: absolute;
diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index 387ad26f3..f87b92183 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -189,7 +189,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 				height: 3px;
 				background: rgba(0, 0, 0, 0.1);
 				border-radius: 999px;
-				overflow: clip;
+				overflow: hidden; overflow: clip;
 
 				> .highlight {
 					position: absolute;
diff --git a/packages/client/src/components/marquee.vue b/packages/client/src/components/marquee.vue
index 468503351..e7d6f20ed 100644
--- a/packages/client/src/components/marquee.vue
+++ b/packages/client/src/components/marquee.vue
@@ -76,7 +76,7 @@ export default {
 
 <style lang="scss" module>
 .wrap {
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 }
 .content {
 	display: inline-block;
diff --git a/packages/client/src/components/note-preview.vue b/packages/client/src/components/note-preview.vue
index a78b49965..be7214db1 100644
--- a/packages/client/src/components/note-preview.vue
+++ b/packages/client/src/components/note-preview.vue
@@ -27,7 +27,7 @@ const props = defineProps<{
 	display: flex;
 	margin: 0;
 	padding: 0;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 	font-size: 0.95em;
 
 	&.min-width_350px {
diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue
index b813b9a2b..93c34b6bf 100644
--- a/packages/client/src/components/note-simple.vue
+++ b/packages/client/src/components/note-simple.vue
@@ -36,7 +36,7 @@ const showContent = $ref(false);
 	display: flex;
 	margin: 0;
 	padding: 0;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 	font-size: 0.95em;
 
 	&.min-width_350px {
diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue
index c2c92f541..98c5c9a67 100644
--- a/packages/client/src/components/note.vue
+++ b/packages/client/src/components/note.vue
@@ -297,7 +297,7 @@ function readPromo() {
 	position: relative;
 	transition: box-shadow 0.1s ease;
 	font-size: 1.05em;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 	contain: content;
 
 	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
diff --git a/packages/client/src/components/tag-cloud.vue b/packages/client/src/components/tag-cloud.vue
index 13f528ecf..5ffa7321e 100644
--- a/packages/client/src/components/tag-cloud.vue
+++ b/packages/client/src/components/tag-cloud.vue
@@ -73,7 +73,7 @@ defineExpose({
 <style lang="scss" scoped>
 .meijqfqm {
 	position: relative;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 	display: grid;
 	place-items: center;
 
diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue
index c9fad64eb..e0230dccd 100644
--- a/packages/client/src/components/toast.vue
+++ b/packages/client/src/components/toast.vue
@@ -54,7 +54,7 @@ onMounted(() => {
 		width: min-content;
 		box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
 		border-radius: 8px;
-		overflow: clip;
+		overflow: hidden; overflow: clip;
 		text-align: center;
 		pointer-events: none;
 
diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue
index e6b20d988..d8052b511 100644
--- a/packages/client/src/components/ui/button.vue
+++ b/packages/client/src/components/ui/button.vue
@@ -148,7 +148,7 @@ export default defineComponent({
 	text-decoration: none;
 	background: var(--buttonBg);
 	border-radius: 5px;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 	box-sizing: border-box;
 	transition: background 0.1s ease;
 
diff --git a/packages/client/src/components/ui/container.vue b/packages/client/src/components/ui/container.vue
index 7c595d811..e14242827 100644
--- a/packages/client/src/components/ui/container.vue
+++ b/packages/client/src/components/ui/container.vue
@@ -10,7 +10,8 @@
 			</button>
 		</div>
 	</header>
-	<transition :name="$store.state.animation ? 'container-toggle' : ''"
+	<transition
+		:name="$store.state.animation ? 'container-toggle' : ''"
 		@enter="enter"
 		@after-enter="afterEnter"
 		@leave="leave"
@@ -34,37 +35,37 @@ export default defineComponent({
 		showHeader: {
 			type: Boolean,
 			required: false,
-			default: true
+			default: true,
 		},
 		thin: {
 			type: Boolean,
 			required: false,
-			default: false
+			default: false,
 		},
 		naked: {
 			type: Boolean,
 			required: false,
-			default: false
+			default: false,
 		},
 		foldable: {
 			type: Boolean,
 			required: false,
-			default: false
+			default: false,
 		},
 		expanded: {
 			type: Boolean,
 			required: false,
-			default: true
+			default: true,
 		},
 		scrollable: {
 			type: Boolean,
 			required: false,
-			default: false
+			default: false,
 		},
 		maxHeight: {
 			type: Number,
 			required: false,
-			default: null
+			default: null,
 		},
 	},
 	data() {
@@ -79,12 +80,12 @@ export default defineComponent({
 			const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0;
 			this.$el.style.minHeight = `${headerHeight}px`;
 			if (showBody) {
-				this.$el.style.flexBasis = `auto`;
+				this.$el.style.flexBasis = 'auto';
 			} else {
 				this.$el.style.flexBasis = `${headerHeight}px`;
 			}
 		}, {
-			immediate: true
+			immediate: true,
 		});
 
 		this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px');
@@ -124,7 +125,7 @@ export default defineComponent({
 		afterLeave(el) {
 			el.style.height = null;
 		},
-	}
+	},
 });
 </script>
 
@@ -142,7 +143,7 @@ export default defineComponent({
 
 .ukygtjoj {
 	position: relative;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 
 	&.naked {
 		background: transparent !important;
diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue
index d6a29ec4b..385f6cdb2 100644
--- a/packages/client/src/components/ui/modal.vue
+++ b/packages/client/src/components/ui/modal.vue
@@ -389,7 +389,7 @@ defineExpose({
 		left: 0;
 		width: 100%;
 		height: 100%;
-		overflow: clip;
+		overflow: hidden; overflow: clip;
 
 		> .content {
 			position: fixed;
diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue
index fd7b5f936..a80041b5c 100644
--- a/packages/client/src/pages/about-misskey.vue
+++ b/packages/client/src/pages/about-misskey.vue
@@ -1,7 +1,7 @@
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<div style="overflow: clip;">
+	<div style="overflow: hidden; overflow: clip;">
 		<MkSpacer :content-max="600" :margin-min="20">
 			<div class="_formRoot znqjceqz">
 				<div id="debug"></div>
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index e482c4ca6..6241bbbdd 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -138,7 +138,7 @@ definePageMetadata(computed(() => ({
 .fwhjspax {
 	text-align: center;
 	border-radius: 10px;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 	background-size: cover;
 	background-position: center center;
 
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index 316cf04f1..7e085106b 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -561,7 +561,7 @@ definePageMetadata({
 				> .body {
 					background: var(--panel);
 					border-radius: var(--radius);
-					overflow: clip;
+					overflow: hidden; overflow: clip;
 				}
 			}
 
@@ -620,7 +620,7 @@ definePageMetadata({
 				> .body {
 					background: var(--panel);
 					border-radius: var(--radius);
-					overflow: clip;
+					overflow: hidden; overflow: clip;
 				}
 			}
 		}
diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue
index 831ced4ea..f84300764 100644
--- a/packages/client/src/pages/antenna-timeline.vue
+++ b/packages/client/src/pages/antenna-timeline.vue
@@ -110,7 +110,7 @@ definePageMetadata(computed(() => antenna ? {
 	> .tl {
 		background: var(--bg);
 		border-radius: var(--radius);
-		overflow: clip;
+		overflow: hidden; overflow: clip;
 	}
 
 	&.min-width_800px {
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index 6b60148df..cbc732723 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -192,7 +192,7 @@ definePageMetadata({
 	background-size: cover;
 	background-position: center;
 	border-radius: 10px;
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 
 	> .avatar {
 		display: inline-block;
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index 0ed4abc37..1bdad3e75 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -175,7 +175,7 @@ definePageMetadata({
 		> .toggleWrapper {
 			display: inline-block;
 			text-align: left;
-			overflow: clip;
+			overflow: hidden; overflow: clip;
 			padding: 0 100px;
 
 			input {
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index 40eb85ff4..8554a9aeb 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -177,7 +177,7 @@ definePageMetadata(computed(() => ({
 	> .tl {
 		background: var(--bg);
 		border-radius: var(--radius);
-		overflow: clip;
+		overflow: hidden; overflow: clip;
 	}
 }
 </style>
diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue
index 593db1dea..3fca6f141 100644
--- a/packages/client/src/pages/user-list-timeline.vue
+++ b/packages/client/src/pages/user-list-timeline.vue
@@ -103,7 +103,7 @@ definePageMetadata(computed(() => list ? {
 	> .tl {
 		background: var(--bg);
 		border-radius: var(--radius);
-		overflow: clip;
+		overflow: hidden; overflow: clip;
 	}
 
 	&.min-width_800px {
diff --git a/packages/client/src/pages/user/index.photos.vue b/packages/client/src/pages/user/index.photos.vue
index 79dd1726e..cedb0e05f 100644
--- a/packages/client/src/pages/user/index.photos.vue
+++ b/packages/client/src/pages/user/index.photos.vue
@@ -90,7 +90,7 @@ export default defineComponent({
 		> .img {
 			height: 128px;
 			border-radius: 6px;
-			overflow: clip;
+			overflow: hidden; overflow: clip;
 		}
 	}
 
diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue
index 8ff7d9057..457e38cb2 100644
--- a/packages/client/src/pages/welcome.entrance.a.vue
+++ b/packages/client/src/pages/welcome.entrance.a.vue
@@ -275,7 +275,7 @@ function showMenu(ev) {
 			-webkit-backdrop-filter: var(--blur, blur(15px));
 			backdrop-filter: var(--blur, blur(15px));
 			border-radius: 999px;
-			overflow: clip;
+			overflow: hidden; overflow: clip;
 			width: 800px;
 			padding: 8px 0;
 
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index 94b5dd8cb..5bd88d5c4 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -260,7 +260,7 @@ hr {
 ._panel {
 	background: var(--panel);
 	border-radius: var(--radius);
-	overflow: clip;
+	overflow: hidden; overflow: clip;
 }
 
 ._block {
diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue
index 86d2812f5..11912e141 100644
--- a/packages/client/src/ui/_common_/statusbars.vue
+++ b/packages/client/src/ui/_common_/statusbars.vue
@@ -52,7 +52,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 		width: 100%;
 		line-height: var(--height);
 		height: var(--height);
-		overflow: clip;
+		overflow: hidden; overflow: clip;
 		contain: strict;
 
 		> .name {
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 412b6db34..310232aec 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -241,7 +241,7 @@ onMounted(() => {
 			border-left: solid 1px var(--divider);
 			border-right: solid 1px var(--divider);
 			border-radius: 0;
-			overflow: clip;
+			overflow: hidden; overflow: clip;
 			--margin: 12px;
 		}
 

From 20f9bb10db5d28d72d5da33edbc3ad80ee31e8c1 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 16:19:47 +0900
Subject: [PATCH 067/100] fix(client): fix wrong import

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

diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue
index f84300764..d62fc21b4 100644
--- a/packages/client/src/pages/antenna-timeline.vue
+++ b/packages/client/src/pages/antenna-timeline.vue
@@ -24,7 +24,7 @@ import { scroll } from '@/scripts/scroll';
 import * as os from '@/os';
 import { useRouter } from '@/router';
 import { definePageMetadata } from '@/scripts/page-metadata';
-import i18n from '@/components/global/i18n';
+import i18n from '@/i18n';
 
 const router = useRouter();
 

From 2ae11f265b6801f7c90d5561d0e9aa591fb54fbe Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 16:36:13 +0900
Subject: [PATCH 068/100] chore(client): tweak style

---
 packages/client/src/components/ui/menu.vue        | 4 ++--
 packages/client/src/ui/_common_/statusbar-rss.vue | 4 ++--
 packages/client/src/widgets/rss-marquee.vue       | 6 ++++--
 3 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index dad5dfa8b..cb4ec7c34 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -136,11 +136,11 @@ function focusDown() {
 	> .item {
 		display: block;
 		position: relative;
-		padding: 8px 18px;
+		padding: 6px 18px;
 		width: 100%;
 		box-sizing: border-box;
 		white-space: nowrap;
-		font-size: 0.9em;
+		font-size: 0.85em;
 		line-height: 20px;
 		text-align: left;
 		overflow: hidden;
diff --git a/packages/client/src/ui/_common_/statusbar-rss.vue b/packages/client/src/ui/_common_/statusbar-rss.vue
index ddfc6faaa..83e78a376 100644
--- a/packages/client/src/ui/_common_/statusbar-rss.vue
+++ b/packages/client/src/ui/_common_/statusbar-rss.vue
@@ -79,9 +79,9 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
 			display: inline-block;
 			width: 0.5px;
 			height: var(--height);
-			margin: 0 1em;
+			margin: 0 2em;
 			background: currentColor;
-			opacity: 0.7;
+			opacity: 0.3;
 		}
 	}
 }
diff --git a/packages/client/src/widgets/rss-marquee.vue b/packages/client/src/widgets/rss-marquee.vue
index c20954c1e..938113b53 100644
--- a/packages/client/src/widgets/rss-marquee.vue
+++ b/packages/client/src/widgets/rss-marquee.vue
@@ -118,10 +118,12 @@ defineExpose<WidgetComponentExpose>({
 
 .ekmkgxbk {
 	> .feed {
+		--height: 42px;
 		padding: 0;
 		font-size: 0.9em;
-		line-height: 42px;
-		height: 42px;
+		line-height: var(--height);
+		height: var(--height);
+		contain: strict;
 
 		::v-deep(.item) {
 			display: inline-flex;

From bd7bda0c0d06e665af4c3981aece3ca046c51ec2 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 16:36:23 +0900
Subject: [PATCH 069/100] fix(client): fix wrong import

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

diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue
index d62fc21b4..309f94f9f 100644
--- a/packages/client/src/pages/antenna-timeline.vue
+++ b/packages/client/src/pages/antenna-timeline.vue
@@ -24,7 +24,7 @@ import { scroll } from '@/scripts/scroll';
 import * as os from '@/os';
 import { useRouter } from '@/router';
 import { definePageMetadata } from '@/scripts/page-metadata';
-import i18n from '@/i18n';
+import { i18n } from '@/i18n';
 
 const router = useRouter();
 

From 36f2b2785272da8425d31296feeadbb591ec21cc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 16:46:00 +0900
Subject: [PATCH 070/100] chore(client): tweak style

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

diff --git a/packages/client/src/components/global/page-header.vue b/packages/client/src/components/global/page-header.vue
index 0ab15e0ca..e6917611f 100644
--- a/packages/client/src/components/global/page-header.vue
+++ b/packages/client/src/components/global/page-header.vue
@@ -172,7 +172,7 @@ onUnmounted(() => {
 
 <style lang="scss" scoped>
 .fdidabkb {
-	--height: 60px;
+	--height: 55px;
 	display: flex;
 	width: 100%;
 	-webkit-backdrop-filter: var(--blur, blur(15px));

From 222777c80317395e664bdb6d98534fcf31e1d103 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 16:50:51 +0900
Subject: [PATCH 071/100] chore(client): rename marquee -> ticker

---
 CHANGELOG.md                                                  | 2 +-
 locales/ja-JP.yml                                             | 2 +-
 packages/client/src/widgets/index.ts                          | 4 ++--
 .../client/src/widgets/{rss-marquee.vue => rss-ticker.vue}    | 4 ++--
 4 files changed, 6 insertions(+), 6 deletions(-)
 rename packages/client/src/widgets/{rss-marquee.vue => rss-ticker.vue} (97%)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f185f63f..da8145b10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,7 +22,7 @@ You should also include the user name that made the change.
 - Client: Improve control panel @syuilo
 - Client: Show warning in control panel when there is an unresolved abuse report @syuilo
 - Client: Add instance-cloud widget @syuilo
-- Client: Add rss-marquee widget @syuilo
+- Client: Add rss-ticker widget @syuilo
 - Client: Removing entries from a clip @futchitwo
 - Client: Poll highlights in explore page @syuilo
 - ユーザーにモデレーションメモを残せる機能 @syuilo
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 01d001688..bfbfff99a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1251,7 +1251,7 @@ _widgets:
   trends: "トレンド"
   clock: "時計"
   rss: "RSSリーダー"
-  rssMarquee: "RSSリーダー(マーキー)"
+  rssTicker: "RSSティッカー"
   activity: "アクティビティ"
   photos: "フォト"
   digitalClock: "デジタル時計"
diff --git a/packages/client/src/widgets/index.ts b/packages/client/src/widgets/index.ts
index ed65f5291..baf6acd23 100644
--- a/packages/client/src/widgets/index.ts
+++ b/packages/client/src/widgets/index.ts
@@ -6,7 +6,7 @@ export default function(app: App) {
 	app.component('MkwTimeline', defineAsyncComponent(() => import('./timeline.vue')));
 	app.component('MkwCalendar', defineAsyncComponent(() => import('./calendar.vue')));
 	app.component('MkwRss', defineAsyncComponent(() => import('./rss.vue')));
-	app.component('MkwRssMarquee', defineAsyncComponent(() => import('./rss-marquee.vue')));
+	app.component('MkwRssTicker', defineAsyncComponent(() => import('./rss-ticker.vue')));
 	app.component('MkwTrends', defineAsyncComponent(() => import('./trends.vue')));
 	app.component('MkwClock', defineAsyncComponent(() => import('./clock.vue')));
 	app.component('MkwActivity', defineAsyncComponent(() => import('./activity.vue')));
@@ -30,7 +30,7 @@ export const widgets = [
 	'timeline',
 	'calendar',
 	'rss',
-	'rssMarquee',
+	'rssTicker',
 	'trends',
 	'clock',
 	'activity',
diff --git a/packages/client/src/widgets/rss-marquee.vue b/packages/client/src/widgets/rss-ticker.vue
similarity index 97%
rename from packages/client/src/widgets/rss-marquee.vue
rename to packages/client/src/widgets/rss-ticker.vue
index 938113b53..06995bc86 100644
--- a/packages/client/src/widgets/rss-marquee.vue
+++ b/packages/client/src/widgets/rss-ticker.vue
@@ -1,5 +1,5 @@
 <template>
-<MkContainer :naked="widgetProps.transparent" :show-header="widgetProps.showHeader" class="mkw-rss-marquee">
+<MkContainer :naked="widgetProps.transparent" :show-header="widgetProps.showHeader" class="mkw-rss-ticker">
 	<template #header><i class="fas fa-rss-square"></i>RSS</template>
 	<template #func><button class="_button" @click="configure"><i class="fas fa-cog"></i></button></template>
 
@@ -27,7 +27,7 @@ import * as os from '@/os';
 import MkContainer from '@/components/ui/container.vue';
 import { useInterval } from '@/scripts/use-interval';
 
-const name = 'rssMarquee';
+const name = 'rssTicker';
 
 const widgetPropsDef = {
 	url: {

From 2e37f5bc6b4925acb71b80dc281171134896c725 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 17:04:44 +0900
Subject: [PATCH 072/100] 12.112.0-beta.15

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

diff --git a/package.json b/package.json
index 0e6b5b7a8..2066d8c6c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.14",
+	"version": "12.112.0-beta.15",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 1fe89d6aabf8afd3417e20e5df1460ca816476c6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 19:01:08 +0900
Subject: [PATCH 073/100] fix typo

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

diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts
index cbe47dc7c..e02c7b97e 100644
--- a/packages/backend/src/server/api/endpoints/federation/stats.ts
+++ b/packages/backend/src/server/api/endpoints/federation/stats.ts
@@ -54,7 +54,7 @@ export default define(meta, paramDef, async (ps) => {
 	]);
 
 	const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0);
-	const gotPubCount = topSubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
+	const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
 
 	return await awaitAll({
 		topSubInstances: Instances.packMany(topSubInstances),

From ff80f1b584def39bfb59e3564b562a0629458a71 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 19:11:10 +0900
Subject: [PATCH 074/100] fix(client): contextmenu of deck not working

---
 packages/client/src/ui/deck.vue | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 111cf8022..1cf1a1481 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -1,13 +1,12 @@
 <template>
 <div
 	class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
-	@contextmenu.self.prevent="onContextmenu"
 >
 	<XSidebar v-if="!isMobile"/>
 
 	<div class="main">
 		<XStatusBars class="statusbars"/>
-		<div ref="columnsEl" class="columns">
+		<div ref="columnsEl" class="columns" @contextmenu.self.prevent="onContextmenu">
 			<template v-for="ids in layout">
 				<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
 				<section

From be4266e611ed064622f67d231321d380c35aca4d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 20:30:58 +0900
Subject: [PATCH 075/100] enhance(client): refine deck

Fix #7720
---
 locales/ja-JP.yml                             |   2 -
 packages/client/src/pages/settings/deck.vue   |  14 --
 .../client/src/pages/settings/statusbars.vue  |   1 +
 packages/client/src/themes/_dark.json5        |   1 +
 packages/client/src/themes/_light.json5       |   1 +
 .../client/src/ui/_common_/statusbars.vue     |   8 +-
 packages/client/src/ui/deck.vue               | 120 +++++++++++-------
 packages/client/src/ui/deck/column.vue        |  73 +++++++----
 packages/client/src/ui/deck/deck-store.ts     |   8 --
 9 files changed, 132 insertions(+), 96 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index bfbfff99a..13b9fcfd4 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1721,8 +1721,6 @@ _notification:
 _deck:
   alwaysShowMainColumn: "常にメインカラムを表示"
   columnAlign: "カラムの寄せ"
-  columnMargin: "カラム間のマージン"
-  columnHeaderHeight: "カラムのヘッダー幅"
   addColumn: "カラムを追加"
   swapLeft: "左に移動"
   swapRight: "右に移動"
diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue
index b1cf8a8cc..96cb22bca 100644
--- a/packages/client/src/pages/settings/deck.vue
+++ b/packages/client/src/pages/settings/deck.vue
@@ -10,18 +10,6 @@
 		<option value="center">{{ i18n.ts.center }}</option>
 	</FormRadios>
 
-	<FormRadios v-model="columnHeaderHeight" class="_formBlock">
-		<template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template>
-		<option :value="42">{{ i18n.ts.narrow }}</option>
-		<option :value="45">{{ i18n.ts.medium }}</option>
-		<option :value="48">{{ i18n.ts.wide }}</option>
-	</FormRadios>
-
-	<FormInput v-model="columnMargin" type="number" class="_formBlock">
-		<template #label>{{ i18n.ts._deck.columnMargin }}</template>
-		<template #suffix>px</template>
-	</FormInput>
-
 	<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
 </div>
 </template>
@@ -41,8 +29,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
 const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
 const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
 const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
-const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
-const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
 const profile = computed(deckStore.makeGetterSetter('profile'));
 
 watch(navWindow, async () => {
diff --git a/packages/client/src/pages/settings/statusbars.vue b/packages/client/src/pages/settings/statusbars.vue
index dea5e0ffd..18de11975 100644
--- a/packages/client/src/pages/settings/statusbars.vue
+++ b/packages/client/src/pages/settings/statusbars.vue
@@ -12,6 +12,7 @@
 		<option value="small">{{ i18n.ts.small }}</option>
 		<option value="medium">{{ i18n.ts.medium }}</option>
 		<option value="large">{{ i18n.ts.large }}</option>
+		<option value="veryLarge">{{ i18n.ts.large }}+</option>
 	</FormRadios>
 </div>
 </template>
diff --git a/packages/client/src/themes/_dark.json5 b/packages/client/src/themes/_dark.json5
index 5c6e7755e..88ec8a545 100644
--- a/packages/client/src/themes/_dark.json5
+++ b/packages/client/src/themes/_dark.json5
@@ -77,6 +77,7 @@
 		codeString: '#ffb675',
 		codeNumber: '#cfff9e',
 		codeBoolean: '#c59eff',
+		deckDivider: '#000',
 		htmlThemeColor: '@bg',
 		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
diff --git a/packages/client/src/themes/_light.json5 b/packages/client/src/themes/_light.json5
index 66e70d5e1..bad1291c8 100644
--- a/packages/client/src/themes/_light.json5
+++ b/packages/client/src/themes/_light.json5
@@ -77,6 +77,7 @@
 		codeString: '#b98710',
 		codeNumber: '#0fbbbb',
 		codeBoolean: '#62b70c',
+		deckDivider: ':darken<3<@bg',
 		htmlThemeColor: '@bg',
 		X2: ':darken<2<@panel',
 		X3: 'rgba(0, 0, 0, 0.05)',
diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue
index 11912e141..7b3a68ec4 100644
--- a/packages/client/src/ui/_common_/statusbars.vue
+++ b/packages/client/src/ui/_common_/statusbars.vue
@@ -4,7 +4,8 @@
 		verySmall: defaultStore.reactiveState.statusbarSize.value === 'verySmall',
 		small: defaultStore.reactiveState.statusbarSize.value === 'small',
 		medium: defaultStore.reactiveState.statusbarSize.value === 'medium',
-		large: defaultStore.reactiveState.statusbarSize.value === 'large'
+		large: defaultStore.reactiveState.statusbarSize.value === 'large',
+		veryLarge: defaultStore.reactiveState.statusbarSize.value === 'veryLarge',
 	}"
 >
 	<div v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" class="item" :class="{ black: x.black }">
@@ -46,6 +47,11 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 		font-size: 0.875em;
 	}
 
+	&.veryLarge {
+		--height: 30px;
+		font-size: 0.9em;
+	}
+
 	> .item {
 		display: inline-flex;
 		vertical-align: bottom;
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 1cf1a1481..88fc39061 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -1,32 +1,37 @@
 <template>
 <div
-	class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
+	class="mk-deck" :class="[{ isMobile }]"
 >
 	<XSidebar v-if="!isMobile"/>
 
 	<div class="main">
 		<XStatusBars class="statusbars"/>
-		<div ref="columnsEl" class="columns" @contextmenu.self.prevent="onContextmenu">
-			<template v-for="ids in layout">
-				<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
-				<section
-					v-if="ids.length > 1"
-					class="folder column"
-					:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
-				>
-					<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
-				</section>
-				<DeckColumnCore
-					v-else
-					:ref="ids[0]"
-					:key="ids[0]"
-					class="column"
-					:column="columns.find(c => c.id === ids[0])"
-					:is-stacked="false"
-					:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
-					@parent-focus="moveFocus(ids[0], $event)"
-				/>
-			</template>
+		<div class="columnsWrapper">
+			<div ref="columnsEl" class="columns" :class="deckStore.reactiveState.columnAlign.value" @contextmenu.self.prevent="onContextmenu">
+				<template v-for="ids in layout">
+					<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
+					<section
+						v-if="ids.length > 1"
+						class="folder column"
+						:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
+					>
+						<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
+					</section>
+					<DeckColumnCore
+						v-else
+						:ref="ids[0]"
+						:key="ids[0]"
+						class="column"
+						:column="columns.find(c => c.id === ids[0])"
+						:is-stacked="false"
+						:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
+						@parent-focus="moveFocus(ids[0], $event)"
+					/>
+				</template>
+			</div>
+			<div class="sideMenu">
+				<button class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
+			</div>
 		</div>
 	</div>
 
@@ -183,22 +188,14 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
 	// TODO: ここではなくて、各カラムで自身の幅に応じて上書きするようにしたい
 	--margin: var(--marginHalf);
 
+	--deckDividerThickness: 5px;
+
 	display: flex;
 	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 	height: calc(var(--vh, 1vh) * 100);
 	box-sizing: border-box;
 	flex: 1;
 
-	&.center {
-		> .column:first-of-type {
-			margin-left: auto;
-		}
-
-		> .column:last-of-type {
-			margin-right: auto;
-		}
-	}
-
 	&.isMobile {
 		padding-bottom: 100px;
 	}
@@ -209,24 +206,55 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
 		display: flex;
 		flex-direction: column;
 
-		> .columns {
-			display: flex;
+		> .columnsWrapper {
 			flex: 1;
-			padding: var(--deckMargin);
-			overflow-x: auto;
-			overflow-y: clip;
+			display: flex;
+			flex-direction: row;
 
-			> .column {
-				flex-shrink: 0;
-				margin-right: var(--deckMargin);
+			> .columns {
+				flex: 1;
+				display: flex;
+				overflow-x: auto;
+				overflow-y: clip;
 
-				&.folder {
-					display: flex;
-					flex-direction: column;
-
-					> *:not(:last-child) {
-						margin-bottom: var(--deckMargin);
+				&.center {
+					> .column:first-of-type {
+						margin-left: auto;
 					}
+
+					> .column:last-of-type {
+						margin-right: auto;
+					}
+				}
+
+				> .column {
+					flex-shrink: 0;
+					border-right: solid var(--deckDividerThickness) var(--deckDivider);
+
+					&:first-child {
+						border-left: solid var(--deckDividerThickness) var(--deckDivider);
+					}
+
+					&.folder {
+						display: flex;
+						flex-direction: column;
+
+						> *:not(:last-child) {
+							border-bottom: solid var(--deckDividerThickness) var(--deckDivider);
+						}
+					}
+				}
+			}
+
+			> .sideMenu {
+				display: flex;
+				flex-direction: column;
+				justify-content: center;
+				width: 32px;
+
+				> .button {
+					width: 100%;
+					aspect-ratio: 1;
 				}
 			}
 		}
diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue
index 6db3549fb..e8e554d72 100644
--- a/packages/client/src/ui/deck/column.vue
+++ b/packages/client/src/ui/deck/column.vue
@@ -1,13 +1,14 @@
 <template>
 <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
-<section v-hotkey="keymap" class="dnpfarvg _panel _narrow_"
+<section
+	v-hotkey="keymap" class="dnpfarvg _narrow_"
 	:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
-	:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
 	@dragover.prevent.stop="onDragover"
 	@dragleave="onDragleave"
 	@drop.prevent.stop="onDrop"
 >
-	<header :class="{ indicated }"
+	<header
+		:class="{ indicated }"
 		draggable="true"
 		@click="goTop"
 		@dragstart="onDragstart"
@@ -22,7 +23,7 @@
 			<slot name="action"></slot>
 		</div>
 		<span class="header"><slot name="header"></slot></span>
-		<button v-if="func" v-tooltip="func.title" class="menu _button" @click.stop="func.handler"><i :class="func.icon || 'fas fa-cog'"></i></button>
+		<button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-cog"></i></button>
 	</header>
 	<div v-show="active" ref="body">
 		<slot></slot>
@@ -39,9 +40,8 @@ export type DeckFunc = {
 </script>
 <script lang="ts" setup>
 import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
+import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column , deckStore } from './deck-store';
 import * as os from '@/os';
-import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store';
-import { deckStore } from './deck-store';
 import { i18n } from '@/i18n';
 
 provide('shouldHeaderThin', true);
@@ -105,7 +105,7 @@ function onOtherDragEnd() {
 function toggleActive() {
 	if (!props.isStacked) return;
 	updateColumn(props.column.id, {
-		active: !props.column.active
+		active: !props.column.active,
 	});
 }
 
@@ -118,69 +118,83 @@ function getMenu() {
 				name: {
 					type: 'string',
 					label: i18n.ts.name,
-					default: props.column.name
+					default: props.column.name,
 				},
 				width: {
 					type: 'number',
 					label: i18n.ts.width,
-					default: props.column.width
+					default: props.column.width,
 				},
 				flexible: {
 					type: 'boolean',
 					label: i18n.ts.flexible,
-					default: props.column.flexible
-				}
+					default: props.column.flexible,
+				},
 			});
 			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, {
 		icon: 'fas fa-window-restore',
 		text: i18n.ts._deck.stackLeft,
 		action: () => {
 			stackLeftColumn(props.column.id);
-		}
+		},
 	}, props.isStacked ? {
 		icon: 'fas fa-window-maximize',
 		text: i18n.ts._deck.popRight,
 		action: () => {
 			popRightColumn(props.column.id);
-		}
+		},
 	} : undefined, null, {
 		icon: 'fas fa-trash-alt',
 		text: i18n.ts.remove,
 		danger: true,
 		action: () => {
 			removeColumn(props.column.id);
-		}
+		},
 	}];
+
+	if (props.func) {
+		items.unshift(null);
+		items.unshift({
+			icon: props.func.icon,
+			text: props.func.title,
+			action: props.func.handler,
+		});
+	}
+
 	return items;
 }
 
+function showSettingsMenu(ev: MouseEvent) {
+	os.popupMenu(getMenu(), ev.currentTarget ?? ev.target);
+}
+
 function onContextmenu(ev: MouseEvent) {
 	os.contextMenu(getMenu(), ev);
 }
@@ -188,7 +202,7 @@ function onContextmenu(ev: MouseEvent) {
 function goTop() {
 	body.scrollTo({
 		top: 0,
-		behavior: 'smooth'
+		behavior: 'smooth',
 	});
 }
 
@@ -239,15 +253,13 @@ function onDrop(ev) {
 <style lang="scss" scoped>
 .dnpfarvg {
 	--root-margin: 10px;
+	--deckColumnHeaderHeight: 42px;
 
 	height: 100%;
 	overflow: hidden;
-	contain: content;
-	box-shadow: 0 0 8px 0 var(--shadow);
+	contain: strict;
 
 	&.draghover {
-		box-shadow: 0 0 0 2px var(--focus);
-
 		&:after {
 			content: "";
 			display: block;
@@ -262,7 +274,18 @@ function onDrop(ev) {
 	}
 
 	&.dragging {
-		box-shadow: 0 0 0 2px var(--focus);
+		&:after {
+			content: "";
+			display: block;
+			position: absolute;
+			z-index: 1000;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			background: var(--focus);
+			opacity: 0.5;
+		}
 	}
 
 	&.dropready {
diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts
index 03d57c346..8d876a4cd 100644
--- a/packages/client/src/ui/deck/deck-store.ts
+++ b/packages/client/src/ui/deck/deck-store.ts
@@ -54,14 +54,6 @@ export const deckStore = markRaw(new Storage('deck', {
 		where: 'deviceAccount',
 		default: true,
 	},
-	columnMargin: {
-		where: 'deviceAccount',
-		default: 16,
-	},
-	columnHeaderHeight: {
-		where: 'deviceAccount',
-		default: 42,
-	},
 }));
 
 export const loadDeck = async () => {

From 8e1a8e659af9e7f921f5bda6110f6b6815c65aea Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 20:32:21 +0900
Subject: [PATCH 076/100] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index da8145b10..639ffd5d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ You should also include the user name that made the change.
 - Client: Add rss-ticker widget @syuilo
 - Client: Removing entries from a clip @futchitwo
 - Client: Poll highlights in explore page @syuilo
+- Client: Improve deck UI @syuilo
 - ユーザーにモデレーションメモを残せる機能 @syuilo
 - Make possible to delete an account by admin @syuilo
 - Improve player detection in URL preview @mei23

From ba9ded3c96fa0b7aaaea38dd8e42a77a52cc0934 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 20:32:34 +0900
Subject: [PATCH 077/100] 12.112.0-beta.16

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

diff --git a/package.json b/package.json
index 2066d8c6c..700cd07c3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.112.0-beta.15",
+	"version": "12.112.0-beta.16",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 1831a6a339c555037b1079dacca3f3df3d67e407 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sun, 3 Jul 2022 20:54:54 +0900
Subject: [PATCH 078/100] =?UTF-8?q?fix:=20streaming=E3=83=86=E3=82=B9?=
 =?UTF-8?q?=E3=83=88=E3=81=8A=E3=81=9D=E3=81=84=20(#8912)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/test/streaming.ts | 1295 ++++++++++------------------
 packages/backend/test/utils.ts     |    6 +-
 2 files changed, 471 insertions(+), 830 deletions(-)

diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts
index f080b71dd..621d07f9c 100644
--- a/packages/backend/test/streaming.ts
+++ b/packages/backend/test/streaming.ts
@@ -3,22 +3,12 @@ process.env.NODE_ENV = 'test';
 import * as assert from 'assert';
 import * as childProcess from 'child_process';
 import { Following } from '../src/models/entities/following.js';
-import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js';
+import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from './utils.js';
 
 describe('Streaming', () => {
 	let p: childProcess.ChildProcess;
 	let Followings: any;
 
-	beforeEach(async () => {
-		p = await startServer();
-		const connection = await initTestDb(true);
-		Followings = connection.getRepository(Following);
-	});
-
-	afterEach(async () => {
-		await shutdownServer(p);
-	});
-
 	const follow = async (follower: any, followee: any) => {
 		await Followings.save({
 			id: 'a',
@@ -34,871 +24,522 @@ describe('Streaming', () => {
 		});
 	};
 
-	it('mention event', () => new Promise(async done => {
-		const alice = await signup({ username: 'alice' });
-		const bob = await signup({ username: 'bob' });
+	describe('Streaming', () => {
+		// Local users
+		let ayano: any;
+		let kyoko: any;
+		let chitose: any;
 
-		const ws = await connectStream(bob, 'main', ({ type, body }) => {
-			if (type == 'mention') {
-				assert.deepStrictEqual(body.userId, alice.id);
-				ws.close();
-				done();
-			}
-		});
+		// Remote users
+		let akari: any;
+		let chinatsu: any;
 
-		post(alice, {
-			text: 'foo @bob bar',
-		});
-	}));
+		let kyokoNote: any;
+		let list: any;
 
-	it('renote event', () => new Promise(async done => {
-		const alice = await signup({ username: 'alice' });
-		const bob = await signup({ username: 'bob' });
-		const bobNote = await post(bob, {
-			text: 'foo',
-		});
+		before(async () => {
+			p = await startServer();
+			const connection = await initTestDb(true);
+			Followings = connection.getRepository(Following);
 
-		const ws = await connectStream(bob, 'main', ({ type, body }) => {
-			if (type == 'renote') {
-				assert.deepStrictEqual(body.renoteId, bobNote.id);
-				ws.close();
-				done();
-			}
-		});
+			ayano = await signup({ username: 'ayano' });
+			kyoko = await signup({ username: 'kyoko' });
+			chitose = await signup({ username: 'chitose' });
 
-		post(alice, {
-			renoteId: bobNote.id,
-		});
-	}));
+			akari = await signup({ username: 'akari', host: 'example.com' });
+			chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
 
-	describe('Home Timeline', () => {
-		it('自分の投稿が流れる', () => new Promise(async done => {
-			const post = {
-				text: 'foo',
-			};
+			kyokoNote = await post(kyoko, { text: 'foo' });
 
-			const me = await signup();
+			// Follow: ayano => kyoko
+			await api('following/create', { userId: kyoko.id }, ayano);
 
-			const ws = await connectStream(me, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.text, post.text);
-					ws.close();
-					done();
-				}
-			});
+			// Follow: ayano => akari
+			await follow(ayano, akari);
 
-			request('/notes/create', post, me);
-		}));
-
-		it('フォローしているユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					assert.deepStrictEqual(body.text, 'foo');
-					ws.close();
-					done();
-				}
-			});
-
-			// Bob が Alice 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [alice.id],
-			});
-		}));
-
-		it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-			const carol = await signup({ username: 'carol' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// Bob が Carol 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [carol.id],
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
-
-	describe('Local Timeline', () => {
-		it('自分の投稿が流れる', () => new Promise(async done => {
-			const me = await signup();
-
-			const ws = await connectStream(me, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, me.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(me, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('リモートユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしてたとしてもリモートユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('ホーム指定の投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// ホーム指定
-			post(bob, {
-				text: 'foo',
-				visibility: 'home',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしているローカルユーザーのダイレクト投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// Bob が Alice 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [alice.id],
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// フォロワー宛て投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'followers',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
-
-	describe('Hybrid Timeline', () => {
-		it('自分の投稿が流れる', () => new Promise(async done => {
-			const me = await signup();
-
-			const ws = await connectStream(me, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, me.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(me, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしているリモートユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			// Alice が Bob をフォロー
-			await follow(alice, bob);
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないリモートユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					assert.deepStrictEqual(body.text, 'foo');
-					ws.close();
-					done();
-				}
-			});
-
-			// Bob が Alice 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [alice.id],
-			});
-		}));
-
-		it('フォローしているユーザーのホーム投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					assert.deepStrictEqual(body.text, 'foo');
-					ws.close();
-					done();
-				}
-			});
-
-			// ホーム投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'home',
-			});
-		}));
-
-		it('フォローしていないローカルユーザーのホーム投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// ホーム投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'home',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// フォロワー宛て投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'followers',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
-
-	describe('Global Timeline', () => {
-		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないリモートユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('ホーム投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// ホーム投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'home',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
-
-	describe('UserList Timeline', () => {
-		it('リストに入れているユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// リスト作成
-			const list = await request('/users/lists/create', {
+			// List: chitose => ayano, kyoko
+			list = await api('users/lists/create', {
 				name: 'my list',
-			}, alice).then(x => x.body);
+			}, chitose).then(x => x.body);
 
-			// Alice が Bob をリスイン
-			await request('/users/lists/push', {
+			await api('users/lists/push', {
 				listId: list.id,
-				userId: bob.id,
-			}, alice);
+				userId: ayano.id,
+			}, chitose);
 
-			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			}, {
+			await api('users/lists/push', {
 				listId: list.id,
+				userId: kyoko.id,
+			}, chitose);
+		});
+
+		after(async () => {
+			await shutdownServer(p);
+		});
+
+		describe('Events', () => {
+			it('mention event', async () => {
+				const fired = await waitFire(
+					kyoko, 'main',	// kyoko:main
+					() => post(ayano, { text: 'foo @kyoko bar' }),	// ayano mention => kyoko
+					msg => msg.type === 'mention' && msg.body.userId === ayano.id	// wait ayano
+				);
+
+				assert.strictEqual(fired, true);
 			});
 
-			post(bob, {
-				text: 'foo',
+			it('renote event', async () => {
+				const fired = await waitFire(
+					kyoko, 'main',	// kyoko:main
+					() => post(ayano, { renoteId: kyokoNote.id }),	// ayano renote
+					msg => msg.type === 'renote' && msg.body.renoteId === kyokoNote.id	// wait renote
+				);
+
+				assert.strictEqual(fired, true);
 			});
-		}));
+		});
 
-		it('リストに入れていないユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
+		describe('Home Timeline', () => {
+			it('自分の投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'homeTimeline',	// ayano:Home
+					() => api('notes/create', { text: 'foo' }, ayano),	// ayano posts
+					msg => msg.type === 'note' && msg.body.text === 'foo'
+				);
 
-			// リスト作成
-			const list = await request('/users/lists/create', {
-				name: 'my list',
-			}, alice).then(x => x.body);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			}, {
-				listId: list.id,
+				assert.strictEqual(fired, true);
 			});
 
-			post(bob, {
-				text: 'foo',
+			it('フォローしているユーザーの投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'homeTimeline',		// ayano:home
+					() => api('notes/create', { text: 'foo' }, kyoko),	// kyoko posts
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, true);
 			});
 
-			setTimeout(() => {
+			it('フォローしていないユーザーの投稿は流れない', async () => {
+				const fired = await waitFire(
+					kyoko, 'homeTimeline',	// kyoko:home
+					() => api('notes/create', { text: 'foo' }, ayano),	// ayano posts
+					msg => msg.type === 'note' && msg.body.userId === ayano.id	// wait ayano
+				);
+
 				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		// #4471
-		it('リストに入れているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// リスト作成
-			const list = await request('/users/lists/create', {
-				name: 'my list',
-			}, alice).then(x => x.body);
-
-			// Alice が Bob をリスイン
-			await request('/users/lists/push', {
-				listId: list.id,
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					assert.deepStrictEqual(body.text, 'foo');
-					ws.close();
-					done();
-				}
-			}, {
-				listId: list.id,
 			});
 
-			// Bob が Alice 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [alice.id],
-			});
-		}));
+			it('フォローしているユーザーのダイレクト投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'homeTimeline',	// ayano:home
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id], }, kyoko),	// kyoko dm => ayano
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
 
-		// #4335
-		it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// リスト作成
-			const list = await request('/users/lists/create', {
-				name: 'my list',
-			}, alice).then(x => x.body);
-
-			// Alice が Bob をリスイン
-			await request('/users/lists/push', {
-				listId: list.id,
-				userId: bob.id,
-			}, alice);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			}, {
-				listId: list.id,
+				assert.strictEqual(fired, true);
 			});
 
-			// フォロワー宛て投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'followers',
-			});
+			it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'homeTimeline',	// ayano:home
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id], }, kyoko),	// kyoko dm => chitose
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
 
-			setTimeout(() => {
 				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
+			});
+		});	// Home
 
-	describe('Hashtag Timeline', () => {
-		it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => {
-			const me = await signup();
+		describe('Local Timeline', () => {
+			it('自分の投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo' }, ayano),	// ayano posts
+					msg => msg.type === 'note' && msg.body.text === 'foo'
+				);
 
-			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.text, '#foo');
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないローカルユーザーの投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo' }, chitose),	// chitose posts
+					msg => msg.type === 'note' && msg.body.userId === chitose.id	// wait chitose
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('リモートユーザーの投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo' }, chinatsu),	// chinatsu posts
+					msg => msg.type === 'note' && msg.body.userId === chinatsu.id	// wait chinatsu
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo' }, akari),	// akari posts
+					msg => msg.type === 'note' && msg.body.userId === akari.id	// wait akari
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('ホーム指定の投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),	// kyoko home posts
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko),	// kyoko DM => ayano
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
+					msg => msg.type === 'note' && msg.body.userId === chitose.id	// wait chitose
+				);
+
+				assert.strictEqual(fired, false);
+			});
+		});
+
+		describe('Hybrid Timeline', () => {
+			it('自分の投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo' }, ayano),	// ayano posts
+					msg => msg.type === 'note' && msg.body.text === 'foo'
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないローカルユーザーの投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo' }, chitose),	// chitose posts
+					msg => msg.type === 'note' && msg.body.userId === chitose.id	// wait chitose
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしているリモートユーザーの投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo' }, akari),	// akari posts
+					msg => msg.type === 'note' && msg.body.userId === akari.id	// wait akari
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないリモートユーザーの投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo' }, chinatsu),	// chinatsu posts
+					msg => msg.type === 'note' && msg.body.userId === chinatsu.id	// wait chinatsu
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしているユーザーのダイレクト投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko),
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしているユーザーのホーム投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo', visibility: 'home' }, chitose),
+					msg => msg.type === 'note' && msg.body.userId === chitose.id
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
+					msg => msg.type === 'note' && msg.body.userId === chitose.id
+				);
+
+				assert.strictEqual(fired, false);
+			});
+		});
+
+		describe('Global Timeline', () => {
+			it('フォローしていないローカルユーザーの投稿が流れる', () => async () => {
+				const fired = await waitFire(
+					ayano, 'globalTimeline',	// ayano:Global
+					() => api('notes/create', { text: 'foo' }, chitose),	// chitose posts
+					msg => msg.type === 'note' && msg.body.userId === chitose.id	// wait chitose
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないリモートユーザーの投稿が流れる', () => async () => {
+				const fired = await waitFire(
+					ayano, 'globalTimeline',	// ayano:Global
+					() => api('notes/create', { text: 'foo' }, chinatsu),	// chinatsu posts
+					msg => msg.type === 'note' && msg.body.userId === chinatsu.id	// wait chinatsu
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('ホーム投稿は流れない', () => async () => {
+				const fired = await waitFire(
+					ayano, 'globalTimeline',	// ayano:Global
+					() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),	// kyoko posts
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, false);
+			});
+		});
+
+		describe('UserList Timeline', () => {
+			it('リストに入れているユーザーの投稿が流れる', () => async () => {
+				const fired = await waitFire(
+					chitose, 'userList',
+					() => api('notes/create', { text: 'foo' }, ayano),
+					msg => msg.type === 'note' && msg.body.userId === ayano.id,
+					{ listId: list.id, }
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('リストに入れていないユーザーの投稿は流れない', () => async () => {
+				const fired = await waitFire(
+					chitose, 'userList',
+					() => api('notes/create', { text: 'foo' }, chinatsu),
+					msg => msg.type === 'note' && msg.body.userId === chinatsu.id,
+					{ listId: list.id, }
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			// #4471
+			it('リストに入れているユーザーのダイレクト投稿が流れる', () => async () => {
+				const fired = await waitFire(
+					chitose, 'userList',
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano),
+					msg => msg.type === 'note' && msg.body.userId === ayano.id,
+					{ listId: list.id, }
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			// #4335
+			it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => async () => {
+				const fired = await waitFire(
+					chitose, 'userList',
+					() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko),
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id,
+					{ listId: list.id, }
+				);
+
+				assert.strictEqual(fired, false);
+			});
+		});
+
+		describe('Hashtag Timeline', () => {
+			it('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => {
+				const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+					if (type == 'note') {
+						assert.deepStrictEqual(body.text, '#foo');
+						ws.close();
+						done();
+					}
+				}, {
+					q: [
+						['foo'],
+					],
+				});
+
+				post(chitose, {
+					text: '#foo',
+				});
+			}));
+
+			it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
+				let fooCount = 0;
+				let barCount = 0;
+				let fooBarCount = 0;
+	
+				const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+					if (type == 'note') {
+						if (body.text === '#foo') fooCount++;
+						if (body.text === '#bar') barCount++;
+						if (body.text === '#foo #bar') fooBarCount++;
+					}
+				}, {
+					q: [
+						['foo', 'bar'],
+					],
+				});
+	
+				post(chitose, {
+					text: '#foo',
+				});
+	
+				post(chitose, {
+					text: '#bar',
+				});
+	
+				post(chitose, {
+					text: '#foo #bar',
+				});
+	
+				setTimeout(() => {
+					assert.strictEqual(fooCount, 0);
+					assert.strictEqual(barCount, 0);
+					assert.strictEqual(fooBarCount, 1);
 					ws.close();
 					done();
-				}
-			}, {
-				q: [
-					['foo'],
-				],
-			});
+				}, 3000);
+			}));
 
-			post(me, {
-				text: '#foo',
-			});
-		}));
+			it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => {
+				let fooCount = 0;
+				let barCount = 0;
+				let fooBarCount = 0;
+				let piyoCount = 0;
 
-		it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => {
-			const me = await signup();
+				const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+					if (type == 'note') {
+						if (body.text === '#foo') fooCount++;
+						if (body.text === '#bar') barCount++;
+						if (body.text === '#foo #bar') fooBarCount++;
+						if (body.text === '#piyo') piyoCount++;
+					}
+				}, {
+					q: [
+						['foo'],
+						['bar'],
+					],
+				});
 
-			let fooCount = 0;
-			let barCount = 0;
-			let fooBarCount = 0;
+				post(chitose, {
+					text: '#foo',
+				});
 
-			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
-				if (type == 'note') {
-					if (body.text === '#foo') fooCount++;
-					if (body.text === '#bar') barCount++;
-					if (body.text === '#foo #bar') fooBarCount++;
-				}
-			}, {
-				q: [
-					['foo', 'bar'],
-				],
-			});
+				post(chitose, {
+					text: '#bar',
+				});
 
-			post(me, {
-				text: '#foo',
-			});
+				post(chitose, {
+					text: '#foo #bar',
+				});
 
-			post(me, {
-				text: '#bar',
-			});
+				post(chitose, {
+					text: '#piyo',
+				});
 
-			post(me, {
-				text: '#foo #bar',
-			});
+				setTimeout(() => {
+					assert.strictEqual(fooCount, 1);
+					assert.strictEqual(barCount, 1);
+					assert.strictEqual(fooBarCount, 1);
+					assert.strictEqual(piyoCount, 0);
+					ws.close();
+					done();
+				}, 3000);
+			}));
 
-			setTimeout(() => {
-				assert.strictEqual(fooCount, 0);
-				assert.strictEqual(barCount, 0);
-				assert.strictEqual(fooBarCount, 1);
-				ws.close();
-				done();
-			}, 3000);
-		}));
+			it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => {
+				let fooCount = 0;
+				let barCount = 0;
+				let fooBarCount = 0;
+				let piyoCount = 0;
+				let waaaCount = 0;
 
-		it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => {
-			const me = await signup();
+				const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+					if (type == 'note') {
+						if (body.text === '#foo') fooCount++;
+						if (body.text === '#bar') barCount++;
+						if (body.text === '#foo #bar') fooBarCount++;
+						if (body.text === '#piyo') piyoCount++;
+						if (body.text === '#waaa') waaaCount++;
+					}
+				}, {
+					q: [
+						['foo', 'bar'],
+						['piyo'],
+					],
+				});
 
-			let fooCount = 0;
-			let barCount = 0;
-			let fooBarCount = 0;
-			let piyoCount = 0;
+				post(chitose, {
+					text: '#foo',
+				});
 
-			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
-				if (type == 'note') {
-					if (body.text === '#foo') fooCount++;
-					if (body.text === '#bar') barCount++;
-					if (body.text === '#foo #bar') fooBarCount++;
-					if (body.text === '#piyo') piyoCount++;
-				}
-			}, {
-				q: [
-					['foo'],
-					['bar'],
-				],
-			});
+				post(chitose, {
+					text: '#bar',
+				});
 
-			post(me, {
-				text: '#foo',
-			});
+				post(chitose, {
+					text: '#foo #bar',
+				});
 
-			post(me, {
-				text: '#bar',
-			});
+				post(chitose, {
+					text: '#piyo',
+				});
 
-			post(me, {
-				text: '#foo #bar',
-			});
+				post(chitose, {
+					text: '#waaa',
+				});
 
-			post(me, {
-				text: '#piyo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fooCount, 1);
-				assert.strictEqual(barCount, 1);
-				assert.strictEqual(fooBarCount, 1);
-				assert.strictEqual(piyoCount, 0);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => {
-			const me = await signup();
-
-			let fooCount = 0;
-			let barCount = 0;
-			let fooBarCount = 0;
-			let piyoCount = 0;
-			let waaaCount = 0;
-
-			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
-				if (type == 'note') {
-					if (body.text === '#foo') fooCount++;
-					if (body.text === '#bar') barCount++;
-					if (body.text === '#foo #bar') fooBarCount++;
-					if (body.text === '#piyo') piyoCount++;
-					if (body.text === '#waaa') waaaCount++;
-				}
-			}, {
-				q: [
-					['foo', 'bar'],
-					['piyo'],
-				],
-			});
-
-			post(me, {
-				text: '#foo',
-			});
-
-			post(me, {
-				text: '#bar',
-			});
-
-			post(me, {
-				text: '#foo #bar',
-			});
-
-			post(me, {
-				text: '#piyo',
-			});
-
-			post(me, {
-				text: '#waaa',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fooCount, 0);
-				assert.strictEqual(barCount, 0);
-				assert.strictEqual(fooBarCount, 1);
-				assert.strictEqual(piyoCount, 1);
-				assert.strictEqual(waaaCount, 0);
-				ws.close();
-				done();
-			}, 3000);
-		}));
+				setTimeout(() => {
+					assert.strictEqual(fooCount, 0);
+					assert.strictEqual(barCount, 0);
+					assert.strictEqual(fooBarCount, 1);
+					assert.strictEqual(piyoCount, 1);
+					assert.strictEqual(waaaCount, 0);
+					ws.close();
+					done();
+				}, 3000);
+			}));
+		});
 	});
 });
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 0ee15067d..245cf858d 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -186,7 +186,7 @@ export function connectStream(user: any, channel: string, listener: (message: Re
 	});
 }
 
-export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean) => {
+export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => {
 	return new Promise<boolean>(async (res, rej) => {
 		let timer: NodeJS.Timeout;
 
@@ -198,7 +198,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond
 					if (timer) clearTimeout(timer);
 					res(true);
 				}
-			});
+			}, params);
 		} catch (e) {
 			rej(e);
 		}
@@ -208,7 +208,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond
 		timer = setTimeout(() => {
 			ws.close();
 			res(false);
-		}, 5000);
+		}, 3000);
 
 		try {
 			await trgr();

From ad4d2cec914bcdb640d945be20c2ace24390bf10 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 3 Jul 2022 23:13:41 +0900
Subject: [PATCH 079/100] enhance(client): tweak deck

---
 locales/ja-JP.yml               |   2 +
 packages/client/src/ui/deck.vue | 114 ++++++++++++++++++--------------
 2 files changed, 65 insertions(+), 51 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 13b9fcfd4..496613400 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1729,6 +1729,8 @@ _deck:
   stackLeft: "左に重ねる"
   popRight: "右に出す"
   profile: "プロファイル"
+  introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
+  introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
 
   _columns:
     main: "メイン"
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 88fc39061..53e76ea14 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -6,31 +6,34 @@
 
 	<div class="main">
 		<XStatusBars class="statusbars"/>
-		<div class="columnsWrapper">
-			<div ref="columnsEl" class="columns" :class="deckStore.reactiveState.columnAlign.value" @contextmenu.self.prevent="onContextmenu">
-				<template v-for="ids in layout">
-					<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
-					<section
-						v-if="ids.length > 1"
-						class="folder column"
-						:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
-					>
-						<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
-					</section>
-					<DeckColumnCore
-						v-else
-						:ref="ids[0]"
-						:key="ids[0]"
-						class="column"
-						:column="columns.find(c => c.id === ids[0])"
-						:is-stacked="false"
-						:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
-						@parent-focus="moveFocus(ids[0], $event)"
-					/>
-				</template>
+		<div ref="columnsEl" class="columns" :class="deckStore.reactiveState.columnAlign.value" @contextmenu.self.prevent="onContextmenu">
+			<template v-for="ids in layout">
+				<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
+				<section
+					v-if="ids.length > 1"
+					class="folder column"
+					:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
+				>
+					<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
+				</section>
+				<DeckColumnCore
+					v-else
+					:ref="ids[0]"
+					:key="ids[0]"
+					class="column"
+					:column="columns.find(c => c.id === ids[0])"
+					:is-stacked="false"
+					:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
+					@parent-focus="moveFocus(ids[0], $event)"
+				/>
+			</template>
+			<div v-if="layout.length === 0" class="intro _panel">
+				<div>{{ i18n.ts._deck.introduction }}</div>
+				<MkButton primary class="add" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton>
+				<div>{{ i18n.ts._deck.introduction2 }}</div>
 			</div>
 			<div class="sideMenu">
-				<button class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
+				<button v-tooltip="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
 			</div>
 		</div>
 	</div>
@@ -67,6 +70,7 @@ import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-
 import DeckColumnCore from '@/ui/deck/column-core.vue';
 import XSidebar from '@/ui/_common_/sidebar.vue';
 import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
+import MkButton from '@/components/ui/button.vue';
 import { getScrollContainer } from '@/scripts/scroll';
 import * as os from '@/os';
 import { menuDef } from '@/menu';
@@ -206,47 +210,55 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
 		display: flex;
 		flex-direction: column;
 
-		> .columnsWrapper {
+		> .columns {
 			flex: 1;
 			display: flex;
-			flex-direction: row;
+			overflow-x: auto;
+			overflow-y: clip;
 
-			> .columns {
-				flex: 1;
-				display: flex;
-				overflow-x: auto;
-				overflow-y: clip;
-
-				&.center {
-					> .column:first-of-type {
-						margin-left: auto;
-					}
-
-					> .column:last-of-type {
-						margin-right: auto;
-					}
+			&.center {
+				> .column:first-of-type {
+					margin-left: auto;
 				}
 
-				> .column {
-					flex-shrink: 0;
-					border-right: solid var(--deckDividerThickness) var(--deckDivider);
+				> .column:last-of-type {
+					margin-right: auto;
+				}
+			}
 
-					&:first-child {
-						border-left: solid var(--deckDividerThickness) var(--deckDivider);
-					}
+			> .column {
+				flex-shrink: 0;
+				border-right: solid var(--deckDividerThickness) var(--deckDivider);
 
-					&.folder {
-						display: flex;
-						flex-direction: column;
+				&:first-of-type {
+					border-left: solid var(--deckDividerThickness) var(--deckDivider);
+				}
 
-						> *:not(:last-child) {
-							border-bottom: solid var(--deckDividerThickness) var(--deckDivider);
-						}
+				&.folder {
+					display: flex;
+					flex-direction: column;
+
+					> *:not(:last-of-type) {
+						border-bottom: solid var(--deckDividerThickness) var(--deckDivider);
 					}
 				}
 			}
 
+			> .intro {
+				padding: 32px;
+				height: min-content;
+				text-align: center;
+				margin: auto;
+
+				> .add {
+					margin: 1em auto;
+				}
+			}
+
 			> .sideMenu {
+				flex-shrink: 0;
+				margin-right: 0;
+				margin-left: auto;
 				display: flex;
 				flex-direction: column;
 				justify-content: center;

From 284aa8b8ca167841763e19d8f123514269acde24 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 4 Jul 2022 01:12:36 +0900
Subject: [PATCH 080/100] chore(client): tweak style

---
 .../client/src/ui/_common_/statusbar-user-list.vue    | 11 +++++++++--
 packages/client/src/ui/_common_/statusbars.vue        |  7 ++++++-
 2 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/ui/_common_/statusbar-user-list.vue b/packages/client/src/ui/_common_/statusbar-user-list.vue
index 01240dc6b..2757e9118 100644
--- a/packages/client/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/client/src/ui/_common_/statusbar-user-list.vue
@@ -6,7 +6,7 @@
 				<span v-for="note in notes" :key="note.id" class="item">
 					<img class="avatar" :src="note.user.avatarUrl" decoding="async"/>
 					<MkA class="text" :to="notePage(note)">
-						<Mfm :text="getNoteSummary(note)" :plain="true" :nowrap="true" :custom-emojis="note.emojis"/>
+						<Mfm class="text" :text="getNoteSummary(note)" :plain="true" :nowrap="true" :custom-emojis="note.emojis"/>
 					</MkA>
 					<span class="divider"></span>
 				</span>
@@ -91,11 +91,18 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
 			margin-right: 8px;
 		}
 
+		> .text {
+			> .text {
+				display: inline-block;
+				vertical-align: bottom;
+			}
+		}
+
 		> .divider {
 			display: inline-block;
 			width: 0.5px;
 			height: 16px;
-			margin: 0 1em;
+			margin: 0 2em;
 			background: currentColor;
 			opacity: 0;
 		}
diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue
index 7b3a68ec4..c18771c54 100644
--- a/packages/client/src/ui/_common_/statusbars.vue
+++ b/packages/client/src/ui/_common_/statusbars.vue
@@ -29,25 +29,30 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 <style lang="scss" scoped>
 .dlrsnxqu {
 	--height: 24px;
+	--nameMargin: 10px;
 	background: var(--panel);
 	font-size: 0.85em;
 
 	&.verySmall {
+		--nameMargin: 7px;
 		--height: 16px;
 		font-size: 0.75em;
 	}
 
 	&.small {
+		--nameMargin: 8px;
 		--height: 20px;
 		font-size: 0.8em;
 	}
 
 	&.large {
+		--nameMargin: 12px;
 		--height: 26px;
 		font-size: 0.875em;
 	}
 
 	&.veryLarge {
+		--nameMargin: 14px;
 		--height: 30px;
 		font-size: 0.9em;
 	}
@@ -62,7 +67,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 		contain: strict;
 
 		> .name {
-			padding: 0 6px;
+			padding: 0 var(--nameMargin);
 			font-weight: bold;
 			color: var(--accent);
 		}

From 2a24fc18ca07ba3cd2dd194d14afbbc1ac16e4b6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 4 Jul 2022 01:37:47 +0900
Subject: [PATCH 081/100] enhance(client): tweak statusbar

---
 .../pages/settings/statusbars.statusbar.vue   | 28 ++++---
 .../client/src/pages/settings/statusbars.vue  | 10 +--
 packages/client/src/store.ts                  |  6 +-
 .../src/ui/_common_/statusbar-user-list.vue   |  2 +
 .../client/src/ui/_common_/statusbars.vue     | 77 ++++++++++---------
 5 files changed, 63 insertions(+), 60 deletions(-)

diff --git a/packages/client/src/pages/settings/statusbars.statusbar.vue b/packages/client/src/pages/settings/statusbars.statusbar.vue
index ad2fa557a..6b03ad46e 100644
--- a/packages/client/src/pages/settings/statusbars.statusbar.vue
+++ b/packages/client/src/pages/settings/statusbars.statusbar.vue
@@ -7,7 +7,7 @@
 		<option value="userList">User list timeline</option>
 	</FormSelect>
 
-	<MkInput v-model="statusbar.name" class="_formBlock">
+	<MkInput v-model="statusbar.name" manual-save class="_formBlock">
 		<template #label>Name</template>
 	</MkInput>
 
@@ -15,14 +15,23 @@
 		<template #label>Black</template>
 	</MkSwitch>
 
+	<FormRadios v-model="statusbar.size" class="_formBlock">
+		<template #label>Size</template>
+		<option value="verySmall">{{ i18n.ts.small }}+</option>
+		<option value="small">{{ i18n.ts.small }}</option>
+		<option value="medium">{{ i18n.ts.medium }}</option>
+		<option value="large">{{ i18n.ts.large }}</option>
+		<option value="veryLarge">{{ i18n.ts.large }}+</option>
+	</FormRadios>
+
 	<template v-if="statusbar.type === 'rss'">
-		<MkInput v-model="statusbar.props.url" class="_formBlock" type="url">
+		<MkInput v-model="statusbar.props.url" manual-save class="_formBlock" type="url">
 			<template #label>URL</template>
 		</MkInput>
-		<MkInput v-model="statusbar.props.refreshIntervalSec" class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
 			<template #label>Refresh interval</template>
 		</MkInput>
-		<MkInput v-model="statusbar.props.marqueeDuration" class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.marqueeDuration" manual-save class="_formBlock" type="number">
 			<template #label>Duration</template>
 		</MkInput>
 		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
@@ -30,10 +39,10 @@
 		</MkSwitch>
 	</template>
 	<template v-else-if="statusbar.type === 'federation'">
-		<MkInput v-model="statusbar.props.refreshIntervalSec" class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
 			<template #label>Refresh interval</template>
 		</MkInput>
-		<MkInput v-model="statusbar.props.marqueeDuration" class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.marqueeDuration" manual-save class="_formBlock" type="number">
 			<template #label>Duration</template>
 		</MkInput>
 		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
@@ -48,10 +57,10 @@
 			<template #label>{{ i18n.ts.userList }}</template>
 			<option v-for="list in userLists" :value="list.id">{{ list.name }}</option>
 		</FormSelect>
-		<MkInput v-model="statusbar.props.refreshIntervalSec" class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
 			<template #label>Refresh interval</template>
 		</MkInput>
-		<MkInput v-model="statusbar.props.marqueeDuration" class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.marqueeDuration" manual-save class="_formBlock" type="number">
 			<template #label>Duration</template>
 		</MkInput>
 		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
@@ -60,7 +69,6 @@
 	</template>
 
 	<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-		<FormButton @click="save">save</FormButton>
 		<FormButton danger @click="del">Delete</FormButton>
 	</div>
 </div>
@@ -109,6 +117,8 @@ watch(() => statusbar.type, () => {
 	}
 });
 
+watch(statusbar, save);
+
 async function save() {
 	const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
 	const statusbars = JSON.parse(JSON.stringify(defaultStore.state.statusbars));
diff --git a/packages/client/src/pages/settings/statusbars.vue b/packages/client/src/pages/settings/statusbars.vue
index 18de11975..bcfff1652 100644
--- a/packages/client/src/pages/settings/statusbars.vue
+++ b/packages/client/src/pages/settings/statusbars.vue
@@ -6,14 +6,6 @@
 		<XStatusbar :_id="x.id" :user-lists="userLists"/>
 	</FormFolder>
 	<FormButton @click="add">add</FormButton>
-	<FormRadios v-model="statusbarSize" class="_formBlock">
-		<template #label>Size</template>
-		<option value="verySmall">{{ i18n.ts.small }}+</option>
-		<option value="small">{{ i18n.ts.small }}</option>
-		<option value="medium">{{ i18n.ts.medium }}</option>
-		<option value="large">{{ i18n.ts.large }}</option>
-		<option value="veryLarge">{{ i18n.ts.large }}+</option>
-	</FormRadios>
 </div>
 </template>
 
@@ -30,7 +22,6 @@ import { unisonReload } from '@/scripts/unison-reload';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 
-const statusbarSize = computed(defaultStore.makeGetterSetter('statusbarSize'));
 const statusbars = defaultStore.reactiveState.statusbars;
 
 let userLists = $ref();
@@ -46,6 +37,7 @@ async function add() {
 		id: uuid(),
 		type: null,
 		black: false,
+		size: 'medium',
 		props: {},
 	});
 }
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index cde907017..7ddab18f0 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -94,13 +94,11 @@ export const defaultStore = markRaw(new Storage('base', {
 			name: string;
 			id: string;
 			type: string;
+			size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
+			black: boolean;
 			props: Record<string, any>;
 		}[],
 	},
-	statusbarSize: {
-		where: 'deviceAccount',
-		default: 'medium',
-	},
 	widgets: {
 		where: 'deviceAccount',
 		default: [] as {
diff --git a/packages/client/src/ui/_common_/statusbar-user-list.vue b/packages/client/src/ui/_common_/statusbar-user-list.vue
index 2757e9118..12533d05f 100644
--- a/packages/client/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/client/src/ui/_common_/statusbar-user-list.vue
@@ -52,6 +52,8 @@ const tick = () => {
 	});
 };
 
+watch(() => props.userListId, tick);
+
 useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
 	immediate: true,
 	afterMounted: true,
diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue
index c18771c54..c448b0956 100644
--- a/packages/client/src/ui/_common_/statusbars.vue
+++ b/packages/client/src/ui/_common_/statusbars.vue
@@ -1,14 +1,14 @@
 <template>
-<div
-	class="dlrsnxqu" :class="{
-		verySmall: defaultStore.reactiveState.statusbarSize.value === 'verySmall',
-		small: defaultStore.reactiveState.statusbarSize.value === 'small',
-		medium: defaultStore.reactiveState.statusbarSize.value === 'medium',
-		large: defaultStore.reactiveState.statusbarSize.value === 'large',
-		veryLarge: defaultStore.reactiveState.statusbarSize.value === 'veryLarge',
-	}"
->
-	<div v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" class="item" :class="{ black: x.black }">
+<div class="dlrsnxqu">
+	<div
+		v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" class="item" :class="[{ black: x.black }, {
+			verySmall: x.size === 'verySmall',
+			small: x.size === 'small',
+			medium: x.size === 'medium',
+			large: x.size === 'large',
+			veryLarge: x.size === 'veryLarge',
+		}]"
+	>
 		<span class="name">{{ x.name }}</span>
 		<XRss v-if="x.type === 'rss'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url"/>
 		<XFederation v-else-if="x.type === 'federation'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
@@ -28,37 +28,38 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 
 <style lang="scss" scoped>
 .dlrsnxqu {
-	--height: 24px;
-	--nameMargin: 10px;
 	background: var(--panel);
-	font-size: 0.85em;
-
-	&.verySmall {
-		--nameMargin: 7px;
-		--height: 16px;
-		font-size: 0.75em;
-	}
-
-	&.small {
-		--nameMargin: 8px;
-		--height: 20px;
-		font-size: 0.8em;
-	}
-
-	&.large {
-		--nameMargin: 12px;
-		--height: 26px;
-		font-size: 0.875em;
-	}
-
-	&.veryLarge {
-		--nameMargin: 14px;
-		--height: 30px;
-		font-size: 0.9em;
-	}
 
 	> .item {
-		display: inline-flex;
+		--height: 24px;
+		--nameMargin: 10px;
+		font-size: 0.85em;
+
+		&.verySmall {
+			--nameMargin: 7px;
+			--height: 16px;
+			font-size: 0.75em;
+		}
+
+		&.small {
+			--nameMargin: 8px;
+			--height: 20px;
+			font-size: 0.8em;
+		}
+
+		&.large {
+			--nameMargin: 12px;
+			--height: 26px;
+			font-size: 0.875em;
+		}
+
+		&.veryLarge {
+			--nameMargin: 14px;
+			--height: 30px;
+			font-size: 0.9em;
+		}
+
+		display: flex;
 		vertical-align: bottom;
 		width: 100%;
 		line-height: var(--height);

From f0e08288e91c631ba71e5ac26ac8e985781d7d82 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Sun, 3 Jul 2022 21:41:10 +0200
Subject: [PATCH 082/100] update changelog

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 639ffd5d7..b693d984b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ You should also include the user name that made the change.
 - Client: Removing entries from a clip @futchitwo
 - Client: Poll highlights in explore page @syuilo
 - Client: Improve deck UI @syuilo
+- Client: Word mute also checks content warnings @Johann150
 - ユーザーにモデレーションメモを残せる機能 @syuilo
 - Make possible to delete an account by admin @syuilo
 - Improve player detection in URL preview @mei23
@@ -33,12 +34,17 @@ You should also include the user name that made the change.
 - Server: Improve performance
 - Server: Supports IPv6 on Redis transport. @mei23  
   IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`.
+- Server: Add possibility to log IP addresses of users @syuilo
 
 ### Bugfixes
 - Server: Fix GenerateVideoThumbnail failed @mei23
 - Server: Ensure temp directory cleanup @Johann150
 - favicons of federated instances not showing @syuilo
 - Admin: The checkbox for blocking an instance works again @Johann150
+- Client: Prevent access to user pages when not logged in @pixeldesu @Johann150
+- Client: Disable some hotkeys (e.g. for creating a post) for not logged in users @pixeldesu
+- Client: Ask users that are not logged in to log in when trying to vote in a poll @Johann150
+- Instance mutes also apply in antennas etc. @Johann150
 
 ## 12.111.1 (2022/06/13)
 

From 3439b4abf71f74f23f1450b26f6e3988527eb08f Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 10:35:27 +0200
Subject: [PATCH 083/100] fix: spellcheck is boolean not string

---
 packages/client/src/components/forgot-password.vue | 4 ++--
 packages/client/src/components/signin.vue          | 4 ++--
 packages/client/src/components/signup.vue          | 6 +++---
 packages/client/src/pages/admin/abuses.vue         | 4 ++--
 packages/client/src/pages/admin/users.vue          | 4 ++--
 packages/client/src/pages/settings/2fa.vue         | 2 +-
 packages/client/src/pages/welcome.setup.vue        | 2 +-
 7 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue
index 19c1f23c8..6ed89d45d 100644
--- a/packages/client/src/components/forgot-password.vue
+++ b/packages/client/src/components/forgot-password.vue
@@ -9,12 +9,12 @@
 
 	<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
 		<div class="main _formRoot">
-			<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
+			<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
 				<template #label>{{ i18n.ts.username }}</template>
 				<template #prefix>@</template>
 			</MkInput>
 
-			<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
+			<MkInput v-model="email" class="_formBlock" type="email" :spellcheck="false" required>
 				<template #label>{{ i18n.ts.emailAddress }}</template>
 				<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
 			</MkInput>
diff --git a/packages/client/src/components/signin.vue b/packages/client/src/components/signin.vue
index b772d1479..dacc61016 100644
--- a/packages/client/src/components/signin.vue
+++ b/packages/client/src/components/signin.vue
@@ -6,7 +6,7 @@
 			{{ message }}
 		</MkInfo>
 		<div v-if="!totpLogin" class="normal-signin">
-			<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
+			<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
 				<template #prefix>@</template>
 				<template #suffix>@{{ host }}</template>
 			</MkInput>
@@ -32,7 +32,7 @@
 					<template #label>{{ i18n.ts.password }}</template>
 					<template #prefix><i class="fas fa-lock"></i></template>
 				</MkInput>
-				<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
+				<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false" required>
 					<template #label>{{ i18n.ts.token }}</template>
 					<template #prefix><i class="fas fa-gavel"></i></template>
 				</MkInput>
diff --git a/packages/client/src/components/signup.vue b/packages/client/src/components/signup.vue
index a1327e1a1..dd4a2b18b 100644
--- a/packages/client/src/components/signup.vue
+++ b/packages/client/src/components/signup.vue
@@ -1,11 +1,11 @@
 <template>
 <form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
 	<template v-if="meta">
-		<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" spellcheck="false" required>
+		<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
 			<template #label>{{ $ts.invitationCode }}</template>
 			<template #prefix><i class="fas fa-key"></i></template>
 		</MkInput>
-		<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
+		<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
 			<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
 			<template #prefix>@</template>
 			<template #suffix>@{{ host }}</template>
@@ -19,7 +19,7 @@
 				<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
 			</template>
 		</MkInput>
-		<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
+		<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
 			<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
 			<template #prefix><i class="fas fa-envelope"></i></template>
 			<template #caption>
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index 59d457dde..11cf284b2 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -27,10 +27,10 @@
 					</div>
 					<!-- TODO
 			<div class="inputs" style="display: flex; padding-top: 1.2em;">
-				<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false">
+				<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false">
 					<span>{{ $ts.username }}</span>
 				</MkInput>
-				<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'">
+				<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params().origin === 'local'">
 					<span>{{ $ts.host }}</span>
 				</MkInput>
 			</div>
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
index fc2ab3ea0..c6755672f 100644
--- a/packages/client/src/pages/admin/users.vue
+++ b/packages/client/src/pages/admin/users.vue
@@ -30,11 +30,11 @@
 						</MkSelect>
 					</div>
 					<div class="inputs">
-						<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
+						<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:modelValue="$refs.users.reload()">
 							<template #prefix>@</template>
 							<template #label>{{ $ts.username }}</template>
 						</MkInput>
-						<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
+						<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
 							<template #prefix>@</template>
 							<template #label>{{ $ts.host }}</template>
 						</MkInput>
diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue
index fb3a7a17f..d72d3e206 100644
--- a/packages/client/src/pages/settings/2fa.vue
+++ b/packages/client/src/pages/settings/2fa.vue
@@ -55,7 +55,7 @@
 			<li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"><p>{{ $ts._2fa.step2Url }}<br>{{ twoFactorData.url }}</p></li>
 			<li>
 				{{ i18n.ts._2fa.step3 }}<br>
-				<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
+				<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
 				<MkButton primary @click="submit">{{ i18n.ts.done }}</MkButton>
 			</li>
 		</ol>
diff --git a/packages/client/src/pages/welcome.setup.vue b/packages/client/src/pages/welcome.setup.vue
index 1a2f46028..4892ab6ea 100644
--- a/packages/client/src/pages/welcome.setup.vue
+++ b/packages/client/src/pages/welcome.setup.vue
@@ -3,7 +3,7 @@
 	<h1>Welcome to Misskey!</h1>
 	<div class="_formRoot">
 		<p>{{ $ts.intro }}</p>
-		<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-admin-username class="_formBlock">
+		<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username class="_formBlock">
 			<template #label>{{ $ts.username }}</template>
 			<template #prefix>@</template>
 			<template #suffix>@{{ host }}</template>

From 694d2c94a134102e33de8f0d456dd7737b1e6177 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 4 Jul 2022 21:28:59 +0900
Subject: [PATCH 084/100] =?UTF-8?q?enhance(client):=20deck=E3=81=AE?=
 =?UTF-8?q?=E3=82=A6=E3=82=A4=E3=82=B8=E3=82=A7=E3=83=83=E3=83=88=E3=82=AB?=
 =?UTF-8?q?=E3=83=A9=E3=83=A0=E3=81=8C=E6=9C=AA=E8=A8=AD=E5=AE=9A=E3=81=AE?=
 =?UTF-8?q?=E6=99=82=E3=81=AB=E8=AA=AC=E6=98=8E=E3=82=92=E8=A1=A8=E7=A4=BA?=
 =?UTF-8?q?=E3=81=99=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

---
 locales/ja-JP.yml                              | 1 +
 packages/client/src/ui/deck/widgets-column.vue | 7 +++++++
 2 files changed, 8 insertions(+)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 496613400..968448992 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1731,6 +1731,7 @@ _deck:
   profile: "プロファイル"
   introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
   introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
+  widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"
 
   _columns:
     main: "メイン"
diff --git a/packages/client/src/ui/deck/widgets-column.vue b/packages/client/src/ui/deck/widgets-column.vue
index 9b10f602f..f181fc328 100644
--- a/packages/client/src/ui/deck/widgets-column.vue
+++ b/packages/client/src/ui/deck/widgets-column.vue
@@ -3,6 +3,7 @@
 	<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<div class="wtdtxvec">
+		<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" class="intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
 		<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
 	</div>
 </XColumn>
@@ -13,6 +14,7 @@ import { } from 'vue';
 import XColumn from './column.vue';
 import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
 import XWidgets from '@/components/widgets.vue';
+import { i18n } from '@/i18n';
 
 const props = defineProps<{
 	column: Column;
@@ -52,5 +54,10 @@ function func() {
 	--panelBorder: none;
 
 	padding: 0 var(--margin);
+
+	> .intro {
+		padding: 16px;
+		text-align: center;
+	}
 }
 </style>

From aeb17130214c05b0bc3425fefb09f382fecc3b52 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 4 Jul 2022 21:29:07 +0900
Subject: [PATCH 085/100] 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 b810abd08..a84e9b9b7 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -74,7 +74,7 @@
 		"uuid": "8.3.2",
 		"v-debounce": "0.1.2",
 		"vanilla-tilt": "1.7.2",
-		"vite": "3.0.0-beta.5",
+		"vite": "3.0.0-beta.6",
 		"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 a7b46b70d..3c77a94a2 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -4215,10 +4215,10 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
-vite@3.0.0-beta.5:
-  version "3.0.0-beta.5"
-  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.0-beta.5.tgz#708d5b732dee98d77877cb094b567f5596508b5b"
-  integrity sha512-SfesZuCME4fEmLy4hgsJAg55HRiTgDhH3oPM44XePrdKP5FqYvDkzpSWl6ldDOJYTskKWafGyyuYfXoxodv40Q==
+vite@3.0.0-beta.6:
+  version "3.0.0-beta.6"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.0-beta.6.tgz#dd54c304ce7ceca243be8a114f28c431bbc447a1"
+  integrity sha512-jAxxCGXs6oIO3dFh7gwDEP9RqFzYY+ULDWawS1dd3HfM4FCr8rkOnLljDoBBIDdTNM8M7pDzdoYSmpPEOJqyZQ==
   dependencies:
     esbuild "^0.14.47"
     postcss "^8.4.14"

From 454c90e28b3e8faad87cca8dac3e92453065f0a3 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 15:27:21 +0200
Subject: [PATCH 086/100] fix: replace use of window

---
 packages/client/src/components/abuse-report-window.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue
index 511434962..6b8e36c4d 100644
--- a/packages/client/src/components/abuse-report-window.vue
+++ b/packages/client/src/components/abuse-report-window.vue
@@ -1,5 +1,5 @@
 <template>
-<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
+<XWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
 	<template #header>
 		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
 		<I18n :src="i18n.ts.reportAbuseOf" tag="span">
@@ -40,7 +40,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const window = ref<InstanceType<typeof XWindow>>();
+const uiWindow = ref<InstanceType<typeof XWindow>>();
 const comment = ref(props.initialComment || '');
 
 function send() {
@@ -52,7 +52,7 @@ function send() {
 			type: 'success',
 			text: i18n.ts.abuseReported
 		});
-		window.value?.close();
+		uiWindow.value?.close();
 		emit('closed');
 	});
 }

From c80e7f9a89132ae8ad7a7b572759bb777ee00682 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 15:56:16 +0200
Subject: [PATCH 087/100] fix(lint): semicolong spacing

---
 packages/client/src/components/formula.vue | 3 ++-
 packages/client/src/components/ui/hr.vue   | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/formula.vue b/packages/client/src/components/formula.vue
index fbb40bace..431b4e6c3 100644
--- a/packages/client/src/components/formula.vue
+++ b/packages/client/src/components/formula.vue
@@ -3,7 +3,8 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';import * as os from '@/os';
+import { defineComponent, defineAsyncComponent } from 'vue';
+import * as os from '@/os';
 
 export default defineComponent({
 	components: {
diff --git a/packages/client/src/components/ui/hr.vue b/packages/client/src/components/ui/hr.vue
index 6b075cb44..0cb5b4887 100644
--- a/packages/client/src/components/ui/hr.vue
+++ b/packages/client/src/components/ui/hr.vue
@@ -3,7 +3,8 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';import * as os from '@/os';
+import { defineComponent } from 'vue';
+import * as os from '@/os';
 
 export default defineComponent({});
 </script>

From 2a8dbb43c035af97d3c2c27cdec85e462366af83 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 15:59:24 +0200
Subject: [PATCH 088/100] fix lint padded-blocks

---
 packages/client/src/components/page/page.vue    | 1 -
 packages/client/src/directives/get-size.ts      | 1 -
 packages/client/src/scripts/gen-search-query.ts | 1 -
 packages/client/src/scripts/hpml/evaluator.ts   | 1 -
 packages/client/src/scripts/hpml/lib.ts         | 1 -
 5 files changed, 5 deletions(-)

diff --git a/packages/client/src/components/page/page.vue b/packages/client/src/components/page/page.vue
index a06776237..58c43b22b 100644
--- a/packages/client/src/components/page/page.vue
+++ b/packages/client/src/components/page/page.vue
@@ -24,7 +24,6 @@ export default defineComponent({
 		},
 	},
 	setup(props, ctx) {
-
 		const hpml = new Hpml(props.page, {
 			randomSeed: Math.random(),
 			visitor: $i,
diff --git a/packages/client/src/directives/get-size.ts b/packages/client/src/directives/get-size.ts
index 2c4e9c188..76b54ea4b 100644
--- a/packages/client/src/directives/get-size.ts
+++ b/packages/client/src/directives/get-size.ts
@@ -34,7 +34,6 @@ function calc(src: Element) {
 
 export default {
 	mounted(src, binding, vn) {
-
 		const resize = new ResizeObserver((entries, observer) => {
 			calc(src);
 		});
diff --git a/packages/client/src/scripts/gen-search-query.ts b/packages/client/src/scripts/gen-search-query.ts
index 57a06c280..b413cbbab 100644
--- a/packages/client/src/scripts/gen-search-query.ts
+++ b/packages/client/src/scripts/gen-search-query.ts
@@ -21,7 +21,6 @@ export async function genSearchQuery(v: any, q: string) {
 				}
 			}
 		}
-
 	}
 	return {
 		query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
diff --git a/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts
index 8106687b6..10023edff 100644
--- a/packages/client/src/scripts/hpml/evaluator.ts
+++ b/packages/client/src/scripts/hpml/evaluator.ts
@@ -159,7 +159,6 @@ export class Hpml {
 
 	@autobind
 	private evaluate(expr: Expr, scope: HpmlScope): any {
-
 		if (isLiteralValue(expr)) {
 			if (expr.type === null) {
 				return null;
diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts
index 01a44ffcd..558c780f4 100644
--- a/packages/client/src/scripts/hpml/lib.ts
+++ b/packages/client/src/scripts/hpml/lib.ts
@@ -170,7 +170,6 @@ export const funcDefs: Record<string, { in: any[]; out: any; category: string; i
 };
 
 export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) {
-
 	const date = new Date();
 	const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
 

From 3514e0afdb3ed2911376fb5c523a514f059ee538 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 16:05:41 +0200
Subject: [PATCH 089/100] fix lint no-fallthrough

---
 packages/client/src/components/poll-editor.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue
index d2b88f6bf..a068aca79 100644
--- a/packages/client/src/components/poll-editor.vue
+++ b/packages/client/src/components/poll-editor.vue
@@ -116,8 +116,11 @@ function get() {
 		let base = parseInt(after.value);
 		switch (unit.value) {
 			case 'day': base *= 24;
+				// fallthrough
 			case 'hour': base *= 60;
+				// fallthrough
 			case 'minute': base *= 60;
+				// fallthrough
 			case 'second': return base *= 1000;
 			default: return null;
 		}

From 99cc363327de3f1317a7dae9716d67061a1a17a5 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 16:06:46 +0200
Subject: [PATCH 090/100] fix lint vue/require-valid-default-prop

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

diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index 547543770..6892b1924 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -99,12 +99,12 @@ export default defineComponent({
 		buttonsLeft: {
 			type: Array,
 			required: false,
-			default: [],
+			default: () => [],
 		},
 		buttonsRight: {
 			type: Array,
 			required: false,
-			default: [],
+			default: () => [],
 		},
 	},
 

From d23b262519aecdf2a477419ad6a4ff09f0161324 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 16:17:07 +0200
Subject: [PATCH 091/100] refactor: remove unnecessary computed

Fixes lint no-const-assign.
---
 packages/client/src/pages/admin/bot-protection.vue | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue
index d2e7919b4..d316f973b 100644
--- a/packages/client/src/pages/admin/bot-protection.vue
+++ b/packages/client/src/pages/admin/bot-protection.vue
@@ -61,27 +61,22 @@ let hcaptchaSecretKey: string | null = $ref(null);
 let recaptchaSiteKey: string | null = $ref(null);
 let recaptchaSecretKey: string | null = $ref(null);
 
-const enableHcaptcha = $computed(() => provider === 'hcaptcha');
-const enableRecaptcha = $computed(() => provider === 'recaptcha');
-
 async function init() {
 	const meta = await os.api('admin/meta');
-	enableHcaptcha = meta.enableHcaptcha;
 	hcaptchaSiteKey = meta.hcaptchaSiteKey;
 	hcaptchaSecretKey = meta.hcaptchaSecretKey;
-	enableRecaptcha = meta.enableRecaptcha;
 	recaptchaSiteKey = meta.recaptchaSiteKey;
 	recaptchaSecretKey = meta.recaptchaSecretKey;
 
-	provider = enableHcaptcha ? 'hcaptcha' : enableRecaptcha ? 'recaptcha' : null;
+	provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : null;
 }
 
 function save() {
 	os.apiWithDialog('admin/update-meta', {
-		enableHcaptcha,
+		enableHcaptcha: provider === 'hcaptcha',
 		hcaptchaSiteKey,
 		hcaptchaSecretKey,
-		enableRecaptcha,
+		enableRecaptcha: provider === 'recaptcha',
 		recaptchaSiteKey,
 		recaptchaSecretKey,
 	}).then(() => {

From 0dc3a8b2e84c6c4eac65d879e2817d9fc97fbfdf Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 16:22:11 +0200
Subject: [PATCH 092/100] fix lint: use let instead of const for $ref

Fixes lint no-const-assign.
---
 packages/client/src/pages/admin/overview.user.vue | 2 +-
 packages/client/src/pages/gallery/post.vue        | 4 ++--
 packages/client/src/pages/theme-editor.vue        | 2 +-
 packages/client/src/ui/classic.vue                | 4 ++--
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/client/src/pages/admin/overview.user.vue b/packages/client/src/pages/admin/overview.user.vue
index 40592b280..d70336f3c 100644
--- a/packages/client/src/pages/admin/overview.user.vue
+++ b/packages/client/src/pages/admin/overview.user.vue
@@ -19,7 +19,7 @@ const props = defineProps<{
 	user: misskey.entities.User;
 }>();
 
-const chart = $ref(null);
+let chart = $ref(null);
 
 os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => {
 	chart = res;
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index e16ccc315..e87a541e9 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -74,8 +74,8 @@ const props = defineProps<{
 	postId: string;
 }>();
 
-const post = $ref(null);
-const error = $ref(null);
+let post = $ref(null);
+let error = $ref(null);
 const otherPostsPagination = {
 	endpoint: 'users/gallery/posts' as const,
 	limit: 6,
diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue
index cec383359..d0a26c9cf 100644
--- a/packages/client/src/pages/theme-editor.vue
+++ b/packages/client/src/pages/theme-editor.vue
@@ -118,7 +118,7 @@ const fgColors = [
 	{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
 ];
 
-const theme = $ref<Partial<Theme>>({
+let theme = $ref<Partial<Theme>>({
 	base: 'light',
 	props: lightTheme.props,
 });
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 310232aec..a2c26f536 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -60,8 +60,8 @@ const DESKTOP_THRESHOLD = 1100;
 let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
 
 let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
-const widgetsShowing = $ref(false);
-const fullView = $ref(false);
+let widgetsShowing = $ref(false);
+let fullView = $ref(false);
 let globalHeaderHeight = $ref(0);
 const wallpaper = localStorage.getItem('wallpaper') != null;
 const showMenuOnTop = $computed(() => defaultStore.state.menuDisplay === 'top');

From ae3a51a673cc669f123e4663c7aa245ebc4684d6 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 16:33:55 +0200
Subject: [PATCH 093/100] fix lint no-undef

---
 packages/client/src/components/code-core.vue     | 2 +-
 packages/client/src/pages/admin/_header_.vue     | 1 -
 packages/client/src/pages/my-antennas/editor.vue | 1 +
 packages/client/src/pages/my-lists/list.vue      | 1 +
 packages/client/src/pages/theme-editor.vue       | 1 +
 packages/client/src/ui/deck/main-column.vue      | 2 +-
 6 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/code-core.vue b/packages/client/src/components/code-core.vue
index 45a38afe0..65dee5cda 100644
--- a/packages/client/src/components/code-core.vue
+++ b/packages/client/src/components/code-core.vue
@@ -5,7 +5,7 @@
 
 <script lang="ts" setup>
 import { computed } from 'vue';
-import 'prismjs';
+import { Prism } from 'prismjs';
 import 'prismjs/themes/prism-okaidia.css';
 
 const props = defineProps<{
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index 73747e116..aea2663c3 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -75,7 +75,6 @@ const hasTabs = computed(() => {
 
 const showTabsPopup = (ev: MouseEvent) => {
 	if (!hasTabs.value) return;
-	if (!narrow.value) return;
 	ev.preventDefault();
 	ev.stopPropagation();
 	const menu = props.tabs.map(tab => ({
diff --git a/packages/client/src/pages/my-antennas/editor.vue b/packages/client/src/pages/my-antennas/editor.vue
index 6f3c4afbf..9470257c6 100644
--- a/packages/client/src/pages/my-antennas/editor.vue
+++ b/packages/client/src/pages/my-antennas/editor.vue
@@ -46,6 +46,7 @@
 
 <script lang="ts" setup>
 import { watch } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
 import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import MkTextarea from '@/components/form/textarea.vue';
diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue
index 5bc0bf41d..892878ae8 100644
--- a/packages/client/src/pages/my-lists/list.vue
+++ b/packages/client/src/pages/my-lists/list.vue
@@ -41,6 +41,7 @@ import MkButton from '@/components/ui/button.vue';
 import * as os from '@/os';
 import { mainRouter } from '@/router';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { i18n } from '@/i18n';
 
 const props = defineProps<{
 	listId: string;
diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue
index d0a26c9cf..44b5a05f2 100644
--- a/packages/client/src/pages/theme-editor.vue
+++ b/packages/client/src/pages/theme-editor.vue
@@ -78,6 +78,7 @@ import FormButton from '@/components/ui/button.vue';
 import FormTextarea from '@/components/form/textarea.vue';
 import FormFolder from '@/components/form/folder.vue';
 
+import { $i } from '@/account';
 import { Theme, applyTheme } from '@/scripts/theme';
 import lightTheme from '@/themes/_light.json5';
 import darkTheme from '@/themes/_dark.json5';
diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue
index 670b4a212..9a5fd43af 100644
--- a/packages/client/src/ui/deck/main-column.vue
+++ b/packages/client/src/ui/deck/main-column.vue
@@ -53,7 +53,7 @@ function onContextmenu(ev: MouseEvent) {
 	if (isLink(ev.target as HTMLElement)) return;
 	if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return;
 	if (window.getSelection()?.toString() !== '') return;
-	const path = router.currentRoute.value.path;
+	const path = mainRouter.currentRoute.value.path;
 	os.contextMenu([{
 		type: 'label',
 		text: path,

From 7cbd70bd36814391d69cf30ad09510fefced7483 Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 16:39:04 +0200
Subject: [PATCH 094/100] fix lint no-prototype-builtins

---
 packages/client/src/components/form-dialog.vue | 2 +-
 packages/client/src/plugin.ts                  | 2 +-
 packages/client/src/scripts/array.ts           | 2 +-
 packages/client/src/widgets/widget.ts          | 5 +++--
 4 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue
index 5fd9ec460..f05dde16f 100644
--- a/packages/client/src/components/form-dialog.vue
+++ b/packages/client/src/components/form-dialog.vue
@@ -98,7 +98,7 @@ export default defineComponent({
 
 	created() {
 		for (const item in this.form) {
-			this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null;
+			this.values[item] = this.form[item].default ?? null;
 		}
 	},
 
diff --git a/packages/client/src/plugin.ts b/packages/client/src/plugin.ts
index ca7b4b73d..de1c95567 100644
--- a/packages/client/src/plugin.ts
+++ b/packages/client/src/plugin.ts
@@ -38,7 +38,7 @@ export function install(plugin) {
 function createPluginEnv(opts) {
 	const config = new Map();
 	for (const [k, v] of Object.entries(opts.plugin.config || {})) {
-		config.set(k, jsToVal(opts.plugin.configData.hasOwnProperty(k) ? opts.plugin.configData[k] : v.default));
+		config.set(k, jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default));
 	}
 
 	return {
diff --git a/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts
index 29d027de1..26c6195d6 100644
--- a/packages/client/src/scripts/array.ts
+++ b/packages/client/src/scripts/array.ts
@@ -98,7 +98,7 @@ export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
 export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
 	return collections.reduce((obj: Record<string, T[]>, item: T) => {
 		const key = keySelector(item);
-		if (!obj.hasOwnProperty(key)) {
+		if (typeof obj[key] === 'undefined') {
 			obj[key] = [];
 		}
 
diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts
index 9626d0161..9fdfe7f3e 100644
--- a/packages/client/src/widgets/widget.ts
+++ b/packages/client/src/widgets/widget.ts
@@ -36,8 +36,9 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
 
 	const mergeProps = () => {
 		for (const prop of Object.keys(propsDef)) {
-			if (widgetProps.hasOwnProperty(prop)) continue;
-			widgetProps[prop] = propsDef[prop].default;
+			if (typeof widgetProps[prop] === 'undefined') {
+				widgetProps[prop] = propsDef[prop].default;
+			}
 		}
 	};
 	watch(widgetProps, () => {

From 95388159805f1dc17431a705c5a712b483a42dcd Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Mon, 4 Jul 2022 16:46:48 +0200
Subject: [PATCH 095/100] fix lint @typescript-eslint/ban-types

---
 packages/client/src/components/global/router-view.vue | 3 ---
 packages/client/src/components/tag-cloud.vue          | 2 --
 packages/client/src/scripts/autocomplete.ts           | 2 +-
 packages/client/src/scripts/hotkey.ts                 | 8 +++++---
 packages/client/src/scripts/url.ts                    | 2 +-
 5 files changed, 7 insertions(+), 10 deletions(-)

diff --git a/packages/client/src/components/global/router-view.vue b/packages/client/src/components/global/router-view.vue
index 7138faaa9..fca2371f0 100644
--- a/packages/client/src/components/global/router-view.vue
+++ b/packages/client/src/components/global/router-view.vue
@@ -13,9 +13,6 @@ const props = defineProps<{
 	router?: Router;
 }>();
 
-const emit = defineEmits<{
-}>();
-
 const router = props.router ?? inject('router');
 
 if (router == null) {
diff --git a/packages/client/src/components/tag-cloud.vue b/packages/client/src/components/tag-cloud.vue
index 5ffa7321e..9f3bc1c60 100644
--- a/packages/client/src/components/tag-cloud.vue
+++ b/packages/client/src/components/tag-cloud.vue
@@ -13,8 +13,6 @@
 import { onMounted, ref, watch, PropType, onBeforeUnmount } from 'vue';
 import tinycolor from 'tinycolor2';
 
-const props = defineProps<{}>();
-
 const loaded = !!window.TagCanvas;
 const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
 const computedStyle = getComputedStyle(document.documentElement);
diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts
index 8d9bdee8f..3ef622417 100644
--- a/packages/client/src/scripts/autocomplete.ts
+++ b/packages/client/src/scripts/autocomplete.ts
@@ -8,7 +8,7 @@ export class Autocomplete {
 		x: Ref<number>;
 		y: Ref<number>;
 		q: Ref<string | null>;
-		close: Function;
+		close: () => void;
 	} | null;
 	private textarea: HTMLInputElement | HTMLTextAreaElement;
 	private currentType: string;
diff --git a/packages/client/src/scripts/hotkey.ts b/packages/client/src/scripts/hotkey.ts
index fd9c74f6c..bd8c3b6ca 100644
--- a/packages/client/src/scripts/hotkey.ts
+++ b/packages/client/src/scripts/hotkey.ts
@@ -1,6 +1,8 @@
 import keyCode from './keycode';
 
-type Keymap = Record<string, Function>;
+type Callback = (ev: KeyboardEvent) => void;
+
+type Keymap = Record<string, Callback>;
 
 type Pattern = {
 	which: string[];
@@ -11,14 +13,14 @@ type Pattern = {
 
 type Action = {
 	patterns: Pattern[];
-	callback: Function;
+	callback: Callback;
 	allowRepeat: boolean;
 };
 
 const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => {
 	const result = {
 		patterns: [],
-		callback: callback,
+		callback,
 		allowRepeat: true
 	} as Action;
 
diff --git a/packages/client/src/scripts/url.ts b/packages/client/src/scripts/url.ts
index 542b00e0f..86735de9f 100644
--- a/packages/client/src/scripts/url.ts
+++ b/packages/client/src/scripts/url.ts
@@ -1,4 +1,4 @@
-export function query(obj: {}): string {
+export function query(obj: Record<string, any>): string {
 	const params = Object.entries(obj)
 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
 		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);

From bc49a0e9be665741e828971708833dbee78f51fc Mon Sep 17 00:00:00 2001
From: CyberRex <hspwinx86@gmail.com>
Date: Tue, 5 Jul 2022 00:21:01 +0900
Subject: [PATCH 096/100] Add additional drive capacity change support (#8867)

* Add additional drive capacity change support

* Update packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts

Co-authored-by: Johann150 <johann@qwertqwefsday.eu>

* :art:

* show instance default capacity in placeholder

* fix

* update api/drive

* fix

* remove :

* fix lint

Co-authored-by: Johann150 <johann@qwertqwefsday.eu>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
---
 locales/ja-JP.yml                             |  3 ++
 .../1655813815729-driveCapacityOverrideMb.js  | 13 +++++
 packages/backend/src/models/entities/user.ts  |  6 +++
 .../backend/src/models/repositories/user.ts   |  1 +
 packages/backend/src/server/api/endpoints.ts  |  2 +
 .../admin/drive-capacity-override.ts          | 47 +++++++++++++++++++
 .../backend/src/server/api/endpoints/drive.ts |  2 +-
 .../backend/src/services/drive/add-file.ts    | 11 ++++-
 packages/client/src/pages/user-info.vue       | 38 +++++++++++++--
 9 files changed, 117 insertions(+), 6 deletions(-)
 create mode 100644 packages/backend/migration/1655813815729-driveCapacityOverrideMb.js
 create mode 100644 packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 968448992..ce25095d5 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -203,6 +203,7 @@ done: "完了"
 processing: "処理中"
 preview: "プレビュー"
 default: "デフォルト"
+defaultValueIs: "デフォルト: {value}"
 noCustomEmojis: "絵文字はありません"
 noJobs: "ジョブはありません"
 federating: "連合中"
@@ -855,6 +856,8 @@ noEmailServerWarning: "メールサーバーの設定がされていません。
 thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。"
 recommended: "推奨"
 check: "チェック"
+driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更"
+driveCapOverrideCaption: "0以下を指定すると解除されます。"
 requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。"
 isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
 typeToConfirm: "この操作を行うには {x} と入力してください"
diff --git a/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js
new file mode 100644
index 000000000..f257cd112
--- /dev/null
+++ b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js
@@ -0,0 +1,13 @@
+export class driveCapacityOverrideMb1655813815729 {
+    name = 'driveCapacityOverrideMb1655813815729'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`);
+    }
+}
diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts
index df92fb825..bc9446be4 100644
--- a/packages/backend/src/models/entities/user.ts
+++ b/packages/backend/src/models/entities/user.ts
@@ -218,6 +218,12 @@ export class User {
 	})
 	public token: string | null;
 
+	@Column('integer', {
+		nullable: true,
+		comment: 'Overrides user drive capacity limit',
+	})
+	public driveCapacityOverrideMb: number | null;
+
 	constructor(data: Partial<User>) {
 		if (data == null) return;
 
diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts
index 8a4e48efd..645091395 100644
--- a/packages/backend/src/models/repositories/user.ts
+++ b/packages/backend/src/models/repositories/user.ts
@@ -315,6 +315,7 @@ export const UserRepository = db.getRepository(User).extend({
 			} : undefined) : undefined,
 			emojis: populateEmojis(user.emojis, user.host),
 			onlineStatus: this.getOnlineStatus(user),
+			driveCapacityOverrideMb: user.driveCapacityOverrideMb,
 
 			...(opts.detail ? {
 				url: profile!.url,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 4a2ecebd8..4644f34d9 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -314,6 +314,7 @@ import * as ep___users_search from './endpoints/users/search.js';
 import * as ep___users_show from './endpoints/users/show.js';
 import * as ep___users_stats from './endpoints/users/stats.js';
 import * as ep___fetchRss from './endpoints/fetch-rss.js';
+import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js';
 
 const eps = [
 	['admin/meta', ep___admin_meta],
@@ -629,6 +630,7 @@ const eps = [
 	['users/search', ep___users_search],
 	['users/show', ep___users_show],
 	['users/stats', ep___users_stats],
+	['admin/drive-capacity-override', ep___admin_driveCapOverride],
 	['fetch-rss', ep___fetchRss],
 ];
 
diff --git a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts
new file mode 100644
index 000000000..a4b29770e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts
@@ -0,0 +1,47 @@
+import define from '../../define.js';
+import { Users } from '@/models/index.js';
+import { User } from '@/models/entities/user.js';
+import { insertModerationLog } from '@/services/insert-moderation-log.js';
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		userId: { type: 'string', format: 'misskey:id' },
+		overrideMb: { type: 'number', nullable: true },
+	},
+	required: ['userId', 'overrideMb'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, paramDef, async (ps, me) => {
+	const user = await Users.findOneBy({ id: ps.userId });
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	if (!Users.isLocalUser(user)) {
+		throw new Error('user is not local user');
+	} 
+
+	/*if (user.isAdmin) {
+		throw new Error('cannot suspend admin');
+	}
+	if (user.isModerator) {
+		throw new Error('cannot suspend moderator');
+	}*/
+
+	await Users.update(user.id, {
+		driveCapacityOverrideMb: ps.overrideMb,
+	});
+
+	insertModerationLog(me, 'change-drive-capacity-override', {
+		targetId: user.id,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 47e940cdd..82497adef 100644
--- a/packages/backend/src/server/api/endpoints/drive.ts
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -39,7 +39,7 @@ export default define(meta, paramDef, async (ps, user) => {
 	const usage = await DriveFiles.calcDriveUsageOf(user.id);
 
 	return {
-		capacity: 1024 * 1024 * instance.localDriveCapacityMb,
+		capacity: 1024 * 1024 * (user.driveCapacityOverrideMb || instance.localDriveCapacityMb),
 		usage: usage,
 	};
 });
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index a25413187..0dfad11cf 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -307,7 +307,7 @@ async function deleteOldFile(user: IRemoteUser) {
 
 type AddFileArgs = {
 	/** User who wish to add file */
-	user: { id: User['id']; host: User['host'] } | null;
+	user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null;
 	/** File path */
 	path: string;
 	/** Name */
@@ -371,9 +371,16 @@ export async function addFile({
 	//#region Check drive usage
 	if (user && !isLink) {
 		const usage = await DriveFiles.calcDriveUsageOf(user);
+		const u = await Users.findOneBy({ id: user.id });
 
 		const instance = await fetchMeta();
-		const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
+		let driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
+
+		if (Users.isLocalUser(user) && u?.driveCapacityOverrideMb != null) {
+			driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb;
+			logger.debug('drive capacity override applied');
+			logger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
+		}
 
 		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
 
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index 204ece7eb..51d224dfd 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -85,6 +85,17 @@
 				</FormSection>
 			</div>
 			<div v-else-if="tab === 'moderation'" class="_formRoot">
+				<FormSection>
+					<template #label>Drive Capacity Override</template>
+
+					<FormInput v-if="user.host == null" v-model="driveCapacityOverrideMb" inline :manual-save="true" type="number" :placeholder="i18n.t('defaultValueIs', { value: instance.driveCapacityPerLocalUserMb })" @update:model-value="applyDriveCapacityOverride">
+						<template #label>{{ i18n.ts.driveCapOverrideLabel }}</template>
+						<template #suffix>MB</template>
+						<template #caption>
+							{{ i18n.ts.driveCapOverrideCaption }}
+						</template>
+					</FormInput>
+				</FormSection>
 				<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch>
 				<FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
 				<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch>
@@ -141,7 +152,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, defineComponent, watch } from 'vue';
+import { computed, watch } from 'vue';
 import * as misskey from 'misskey-js';
 import MkChart from '@/components/chart.vue';
 import MkObjectView from '@/components/object-view.vue';
@@ -150,6 +161,8 @@ import FormSwitch from '@/components/form/switch.vue';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import FormButton from '@/components/ui/button.vue';
+import FormInput from '@/components/form/input.vue';
+import FormSplit from '@/components/form/split.vue';
 import FormFolder from '@/components/form/folder.vue';
 import MkKeyValue from '@/components/key-value.vue';
 import MkSelect from '@/components/form/select.vue';
@@ -164,6 +177,7 @@ import { userPage, acct } from '@/filters/user';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { i18n } from '@/i18n';
 import { iAmAdmin, iAmModerator } from '@/account';
+import { instance } from '@/instance';
 
 const props = defineProps<{
 	userId: string;
@@ -172,13 +186,14 @@ const props = defineProps<{
 let tab = $ref('overview');
 let chartSrc = $ref('per-user-notes');
 let user = $ref<null | misskey.entities.UserDetailed>();
-let init = $ref();
+let init = $ref<ReturnType<typeof createFetcher>>();
 let info = $ref();
 let ips = $ref(null);
 let ap = $ref(null);
 let moderator = $ref(false);
 let silenced = $ref(false);
 let suspended = $ref(false);
+let driveCapacityOverrideMb: number | null = $ref(0);
 let moderationNote = $ref('');
 const filesPagination = {
 	endpoint: 'admin/drive/files' as const,
@@ -203,6 +218,7 @@ function createFetcher() {
 			moderator = info.isModerator;
 			silenced = info.isSilenced;
 			suspended = info.isSuspended;
+			driveCapacityOverrideMb = user.driveCapacityOverrideMb;
 			moderationNote = info.moderationNote;
 
 			watch($$(moderationNote), async () => {
@@ -289,6 +305,22 @@ async function deleteAllFiles() {
 	await refreshUser();
 }
 
+async function applyDriveCapacityOverride() {
+	let driveCapOrMb = driveCapacityOverrideMb;
+	if (driveCapacityOverrideMb && driveCapacityOverrideMb < 0) {
+		driveCapOrMb = null;
+	}
+	try {
+		await os.apiWithDialog('admin/drive-capacity-override', { userId: user.id, overrideMb: driveCapOrMb });
+		await refreshUser();
+	} catch (e) {
+		os.alert({
+			type: 'error',
+			text: e.toString(),
+		});
+	}
+}
+
 async function deleteAccount() {
 	const confirm = await os.confirm({
 		type: 'warning',
@@ -319,7 +351,7 @@ watch(() => props.userId, () => {
 	immediate: true,
 });
 
-watch(() => user, () => {
+watch($$(user), () => {
 	os.api('ap/get', {
 		uri: user.uri ?? `${url}/users/${user.id}`,
 	}).then(res => {

From 0356651441adf4496186bb72aec6df835be67944 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 4 Jul 2022 15:26:18 +0000
Subject: [PATCH 097/100] update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b693d984b..a3444a761 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,7 @@ You should also include the user name that made the change.
 - Server: Supports IPv6 on Redis transport. @mei23  
   IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`.
 - Server: Add possibility to log IP addresses of users @syuilo
+- Add additional drive capacity change support @CyberRex0
 
 ### Bugfixes
 - Server: Fix GenerateVideoThumbnail failed @mei23

From 1d3dd83c31313a47b9e94d538e5c367543fab432 Mon Sep 17 00:00:00 2001
From: Usuyuki <63891531+Usuyuki@users.noreply.github.com>
Date: Tue, 5 Jul 2022 11:17:42 +0900
Subject: [PATCH 098/100] =?UTF-8?q?fix:typo=20=E3=80=8C=E6=9C=89=E5=8A=B9?=
 =?UTF-8?q?=E3=81=99=E3=82=8B=E5=BF=85=E8=A6=81=E2=80=A6=E3=80=8D=E2=86=92?=
 =?UTF-8?q?=E3=80=8C=E6=9C=89=E5=8A=B9=E3=81=AB=E3=81=99=E3=82=8B=E5=BF=85?=
 =?UTF-8?q?=E8=A6=81=E2=80=A6=E3=80=8D=20(#8936)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ce25095d5..de5826b21 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -543,7 +543,7 @@ relays: "リレー"
 addRelay: "リレーの追加"
 inboxUrl: "inboxのURL"
 addedRelays: "追加済みのリレー"
-serviceworkerInfo: "プッシュ通知を行うには有効する必要があります。"
+serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。"
 deletedNote: "削除された投稿"
 invisibleNote: "非公開の投稿"
 enableInfiniteScroll: "自動でもっと見る"

From 698409a1b1718a30291121d8fc271d506c4a3d1d Mon Sep 17 00:00:00 2001
From: Kainoa Kanter <44733677+ThatOneCalculator@users.noreply.github.com>
Date: Mon, 4 Jul 2022 19:21:59 -0700
Subject: [PATCH 099/100] chore: fix client lint errors (#8934)

* Fix client lint

* Hide no-v-html

* Ignore banned type

* Update page-editor.vue
---
 .../client/src/components/autocomplete.vue    |  1 +
 packages/client/src/components/code-core.vue  |  1 +
 .../client/src/components/formula-core.vue    |  2 +-
 packages/client/src/pages/admin/emojis.vue    |  2 +-
 .../els/page-editor.el.radio-button.vue       |  2 +-
 .../src/pages/page-editor/page-editor.vue     | 17 ++--
 packages/client/src/pages/user-info.vue       |  4 +-
 .../client/src/pages/welcome.entrance.a.vue   |  1 +
 .../client/src/pages/welcome.entrance.b.vue   |  1 +
 .../client/src/pages/welcome.entrance.c.vue   |  1 +
 packages/client/src/scripts/hpml/index.ts     | 14 ++--
 packages/client/src/scripts/hpml/lib.ts       | 80 ++++++++++---------
 packages/client/src/ui/visitor/a.vue          | 12 ++-
 packages/client/src/ui/visitor/kanban.vue     |  5 +-
 14 files changed, 78 insertions(+), 65 deletions(-)

diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue
index ae708026e..144281e3c 100644
--- a/packages/client/src/components/autocomplete.vue
+++ b/packages/client/src/components/autocomplete.vue
@@ -20,6 +20,7 @@
 			<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
 			<span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
 			<span v-else class="emoji">{{ emoji.emoji }}</span>
+			<!-- eslint-disable-next-line vue/no-v-html -->
 			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
 			<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
 		</li>
diff --git a/packages/client/src/components/code-core.vue b/packages/client/src/components/code-core.vue
index 65dee5cda..a816f3480 100644
--- a/packages/client/src/components/code-core.vue
+++ b/packages/client/src/components/code-core.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/no-v-html -->
 <template>
 <code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code>
 <pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
diff --git a/packages/client/src/components/formula-core.vue b/packages/client/src/components/formula-core.vue
index 49a61ab80..8db8932fc 100644
--- a/packages/client/src/components/formula-core.vue
+++ b/packages/client/src/components/formula-core.vue
@@ -1,4 +1,4 @@
-
+<!-- eslint-disable vue/no-v-html -->
 <template>
 <div v-if="block" v-html="compiledFormula"></div>
 <span v-else v-html="compiledFormula"></span>
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index 4d847daa5..5ed2b1478 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -1,7 +1,7 @@
 <template>
 <div>
 	<MkStickyContainer>
-		<template #header><XHeader :actions="headerActions" :tabs="headerTabs" v-model:tab="tab"/></template>
+		<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 		<MkSpacer :content-max="900">
 			<div class="ogwlenmc">
 				<div v-if="tab === 'local'" class="local">
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue b/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue
index 183e60a69..4b28f120a 100644
--- a/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue
@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<{
 let values: string = $ref(props.value.values.join('\n'));
 
 watch(values, () => {
-	props.value.values = values.split('\n')
+	props.value.values = values.split('\n');
 }, {
 	deep: true
 });
diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue
index 3ce48e89f..aaa61e6e3 100644
--- a/packages/client/src/pages/page-editor/page-editor.vue
+++ b/packages/client/src/pages/page-editor/page-editor.vue
@@ -1,6 +1,6 @@
 <template>
 <MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs" v-model:tab="tab"/></template>
+	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700">
 		<div class="jqqmcavi">
 			<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton>
@@ -82,7 +82,7 @@
 </template>
 
 <script lang="ts" setup>
-import { defineComponent, defineAsyncComponent, computed, provide, watch } from 'vue';
+import { defineAsyncComponent, computed, provide, watch } from 'vue';
 import 'prismjs';
 import { highlight, languages } from 'prismjs/components/prism-core';
 import 'prismjs/components/prism-clike';
@@ -93,7 +93,6 @@ import { v4 as uuid } from 'uuid';
 import XVariable from './page-editor.script-block.vue';
 import XBlocks from './page-editor.blocks.vue';
 import MkTextarea from '@/components/form/textarea.vue';
-import MkContainer from '@/components/ui/container.vue';
 import MkButton from '@/components/ui/button.vue';
 import MkSelect from '@/components/form/select.vue';
 import MkSwitch from '@/components/form/switch.vue';
@@ -168,15 +167,15 @@ function save() {
 	const options = getSaveOptions();
 
 	const onError = err => {
-		if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') {
-			if (err.info.param == 'name') {
+		if (err.id === '3d81ceae-475f-4600-b2a8-2bc116157532') {
+			if (err.info.param === 'name') {
 				os.alert({
 					type: 'error',
 					title: i18n.ts._pages.invalidNameTitle,
 					text: i18n.ts._pages.invalidNameText,
 				});
 			}
-		} else if (err.code == 'NAME_ALREADY_EXISTS') {
+		} else if (err.code === 'NAME_ALREADY_EXISTS') {
 			os.alert({
 				type: 'error',
 				text: i18n.ts._pages.nameAlreadyExists,
@@ -310,7 +309,7 @@ function getPageBlockList() {
 function getScriptBlockList(type: string = null) {
 	const list = [];
 
-	const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number');
+	const blocks = blockDefs.filter(block => type == null || block.out == null || block.out === type || typeof block.out === 'number');
 
 	for (const block of blocks) {
 		const category = list.find(x => x.category === block.category);
@@ -345,8 +344,8 @@ function getScriptBlockList(type: string = null) {
 	return list;
 }
 
-function setEyeCatchingImage(e) {
-	selectFile(e.currentTarget ?? e.target, null).then(file => {
+function setEyeCatchingImage(img) {
+	selectFile(img.currentTarget ?? img.target, null).then(file => {
 		eyeCatchingImageId = file.id;
 	});
 }
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index 51d224dfd..df5d80002 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -313,10 +313,10 @@ async function applyDriveCapacityOverride() {
 	try {
 		await os.apiWithDialog('admin/drive-capacity-override', { userId: user.id, overrideMb: driveCapOrMb });
 		await refreshUser();
-	} catch (e) {
+	} catch (err) {
 		os.alert({
 			type: 'error',
-			text: e.toString(),
+			text: err.toString(),
 		});
 	}
 }
diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue
index 457e38cb2..f9d585221 100644
--- a/packages/client/src/pages/welcome.entrance.a.vue
+++ b/packages/client/src/pages/welcome.entrance.a.vue
@@ -23,6 +23,7 @@
 					<span class="text">{{ instanceName }}</span>
 				</h1>
 				<div class="about">
+					<!-- eslint-disable-next-line vue/no-v-html -->
 					<div class="desc" v-html="meta.description || i18n.ts.headlineMisskey"></div>
 				</div>
 				<div class="action">
diff --git a/packages/client/src/pages/welcome.entrance.b.vue b/packages/client/src/pages/welcome.entrance.b.vue
index 053087fda..344dc9aed 100644
--- a/packages/client/src/pages/welcome.entrance.b.vue
+++ b/packages/client/src/pages/welcome.entrance.b.vue
@@ -9,6 +9,7 @@
 				<img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span>
 			</h1>
 			<div class="about">
+				<!-- eslint-disable-next-line vue/no-v-html -->
 				<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
 			</div>
 			<div class="action">
diff --git a/packages/client/src/pages/welcome.entrance.c.vue b/packages/client/src/pages/welcome.entrance.c.vue
index 6bf487e16..d583c5df3 100644
--- a/packages/client/src/pages/welcome.entrance.c.vue
+++ b/packages/client/src/pages/welcome.entrance.c.vue
@@ -21,6 +21,7 @@
 						<img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span>
 					</h1>
 					<div class="about">
+						<!-- eslint-disable-next-line vue/no-v-html -->
 						<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
 					</div>
 					<div class="action">
diff --git a/packages/client/src/scripts/hpml/index.ts b/packages/client/src/scripts/hpml/index.ts
index ac81eac2d..7cf88d596 100644
--- a/packages/client/src/scripts/hpml/index.ts
+++ b/packages/client/src/scripts/hpml/index.ts
@@ -14,13 +14,13 @@ export type Fn = {
 export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
 
 export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = {
-	text:          { out: 'string',      category: 'value', icon: 'fas fa-quote-right', },
-	multiLineText: { out: 'string',      category: 'value', icon: 'fas fa-align-left', },
-	textList:      { out: 'stringArray', category: 'value', icon: 'fas fa-list', },
-	number:        { out: 'number',      category: 'value', icon: 'fas fa-sort-numeric-up', },
-	ref:           { out: null,          category: 'value', icon: 'fas fa-magic', },
-	aiScriptVar:   { out: null,          category: 'value', icon: 'fas fa-magic', },
-	fn:            { out: 'function',    category: 'value', icon: 'fas fa-square-root-alt', },
+	text: { out: 'string', category: 'value', icon: 'fas fa-quote-right', },
+	multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left', },
+	textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list', },
+	number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up', },
+	ref: { out: null, category: 'value', icon: 'fas fa-magic', },
+	aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic', },
+	fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt', },
 };
 
 export const blockDefs = [
diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts
index 558c780f4..cab467a92 100644
--- a/packages/client/src/scripts/hpml/lib.ts
+++ b/packages/client/src/scripts/hpml/lib.ts
@@ -125,54 +125,56 @@ export function initAiLib(hpml: Hpml) {
 				}
 			});
 			*/
-		})
+		}),
 	};
 }
 
 export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
-	if:              { in: ['boolean', 0, 0],              out: 0,             category: 'flow',       icon: 'fas fa-share-alt', },
-	for:             { in: ['number', 'function'],         out: null,          category: 'flow',       icon: 'fas fa-recycle', },
-	not:             { in: ['boolean'],                    out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
-	or:              { in: ['boolean', 'boolean'],         out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
-	and:             { in: ['boolean', 'boolean'],         out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
-	add:             { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-plus', },
-	subtract:        { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-minus', },
-	multiply:        { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-times', },
-	divide:          { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-divide', },
-	mod:             { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-divide', },
-	round:           { in: ['number'],                     out: 'number',      category: 'operation',  icon: 'fas fa-calculator', },
-	eq:              { in: [0, 0],                         out: 'boolean',     category: 'comparison', icon: 'fas fa-equals', },
-	notEq:           { in: [0, 0],                         out: 'boolean',     category: 'comparison', icon: 'fas fa-not-equal', },
-	gt:              { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-greater-than', },
-	lt:              { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-less-than', },
-	gtEq:            { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-greater-than-equal', },
-	ltEq:            { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-less-than-equal', },
-	strLen:          { in: ['string'],                     out: 'number',      category: 'text',       icon: 'fas fa-quote-right', },
-	strPick:         { in: ['string', 'number'],           out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
-	strReplace:      { in: ['string', 'string', 'string'], out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
-	strReverse:      { in: ['string'],                     out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
-	join:            { in: ['stringArray', 'string'],      out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
-	stringToNumber:  { in: ['string'],                     out: 'number',      category: 'convert',    icon: 'fas fa-exchange-alt', },
-	numberToString:  { in: ['number'],                     out: 'string',      category: 'convert',    icon: 'fas fa-exchange-alt', },
-	splitStrByLine:  { in: ['string'],                     out: 'stringArray', category: 'convert',    icon: 'fas fa-exchange-alt', },
-	pick:            { in: [null, 'number'],               out: null,          category: 'list',       icon: 'fas fa-indent', },
-	listLen:         { in: [null],                         out: 'number',      category: 'list',       icon: 'fas fa-indent', },
-	rannum:          { in: ['number', 'number'],           out: 'number',      category: 'random',     icon: 'fas fa-dice', },
-	dailyRannum:     { in: ['number', 'number'],           out: 'number',      category: 'random',     icon: 'fas fa-dice', },
-	seedRannum:      { in: [null, 'number', 'number'],     out: 'number',      category: 'random',     icon: 'fas fa-dice', },
-	random:          { in: ['number'],                     out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
-	dailyRandom:     { in: ['number'],                     out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
-	seedRandom:      { in: [null, 'number'],               out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
-	randomPick:      { in: [0],                            out: 0,             category: 'random',     icon: 'fas fa-dice', },
-	dailyRandomPick: { in: [0],                            out: 0,             category: 'random',     icon: 'fas fa-dice', },
-	seedRandomPick:  { in: [null, 0],                      out: 0,             category: 'random',     icon: 'fas fa-dice', },
-	DRPWPM:      { in: ['stringArray'],                out: 'string',      category: 'random',     icon: 'fas fa-dice', }, // dailyRandomPickWithProbabilityMapping
+	if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'fas fa-share-alt' },
+	for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle' },
+	not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' },
+	or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' },
+	and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' },
+	add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-plus' },
+	subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-minus' },
+	multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-times' },
+	divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' },
+	mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' },
+	round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator' },
+	eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals' },
+	notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal' },
+	gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than' },
+	lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than' },
+	gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal' },
+	ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal' },
+	strLen: { in: ['string'], out: 'number', category: 'text', icon: 'fas fa-quote-right' },
+	strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'fas fa-quote-right' },
+	strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' },
+	strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' },
+	join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' },
+	stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt' },
+	numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt' },
+	splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt' },
+	pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent' },
+	listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent' },
+	rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' },
+	dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' },
+	seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' },
+	random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' },
+	dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' },
+	seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' },
+	randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' },
+	dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' },
+	seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice' },
+	DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice' }, // dailyRandomPickWithProbabilityMapping
 };
 
 export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) {
 	const date = new Date();
 	const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
 
+	// SHOULD be fine to ignore since it's intended + function shape isn't defined
+	// eslint-disable-next-line @typescript-eslint/ban-types
 	const funcs: Record<string, Function> = {
 		not: (a: boolean) => !a,
 		or: (a: boolean, b: boolean) => a || b,
@@ -188,7 +190,7 @@ export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, vi
 			const result: any[] = [];
 			for (let i = 0; i < times; i++) {
 				result.push(fn.exec({
-					[fn.slots[0]]: i + 1
+					[fn.slots[0]]: i + 1,
 				}));
 			}
 			return result;
diff --git a/packages/client/src/ui/visitor/a.vue b/packages/client/src/ui/visitor/a.vue
index e98247cbb..2473af549 100644
--- a/packages/client/src/ui/visitor/a.vue
+++ b/packages/client/src/ui/visitor/a.vue
@@ -4,6 +4,7 @@
 		<div>
 			<h1 v-if="meta"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
 			<div v-if="meta" class="about">
+				<!-- eslint-disable-next-line vue/no-v-html -->
 				<div class="desc" v-html="meta.description || $ts.introMisskey"></div>
 			</div>
 			<div class="action">
@@ -101,13 +102,18 @@ export default defineComponent({
 	},
 
 	methods: {
-		setParallax(el) {
-			//new simpleParallax(el);
-		},
+		// @ThatOneCalculator: Are these methods even used?
+		// I can't find references to them anywhere else in the code...
+
+		// setParallax(el) {
+		// 	new simpleParallax(el);
+		// },
 
 		changePage(page) {
 			if (page == null) return;
+			// eslint-disable-next-line no-undef
 			if (page[symbols.PAGE_INFO]) {
+				// eslint-disable-next-line no-undef
 				this.pageInfo = page[symbols.PAGE_INFO];
 			}
 		},
diff --git a/packages/client/src/ui/visitor/kanban.vue b/packages/client/src/ui/visitor/kanban.vue
index ee0f11b83..44b555725 100644
--- a/packages/client/src/ui/visitor/kanban.vue
+++ b/packages/client/src/ui/visitor/kanban.vue
@@ -1,10 +1,11 @@
+<!-- eslint-disable vue/no-v-html -->
 <template>
 <div class="rwqkcmrc" :style="{ backgroundImage: transparent ? 'none' : `url(${ $instance.backgroundImageUrl })` }">
 	<div class="back" :class="{ transparent }"></div>
 	<div class="contents">
 		<div class="wrapper">
 			<h1 v-if="meta" :class="{ full }">
-				<MkA to="/" class="link"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></MkA>
+				<MkA to="/" class="link"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl" alt="logo"><span v-else class="text">{{ instanceName }}</span></MkA>
 			</h1>
 			<template v-if="full">
 				<div v-if="meta" class="about">
@@ -21,7 +22,7 @@
 							<div class="title">{{ announcement.title }}</div>
 							<div class="content">
 								<Mfm :text="announcement.text"/>
-								<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
+								<img v-if="announcement.imageUrl" :src="announcement.imageUrl" alt="announcement image"/>
 							</div>
 						</section>
 					</MkPagination>

From 0e4389aa5a7fd74c8435cd56a7f688a85bb29040 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 5 Jul 2022 12:09:49 +0900
Subject: [PATCH 100/100] chore(client): tweak ui

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

diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index df5d80002..fd24ec284 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -85,17 +85,6 @@
 				</FormSection>
 			</div>
 			<div v-else-if="tab === 'moderation'" class="_formRoot">
-				<FormSection>
-					<template #label>Drive Capacity Override</template>
-
-					<FormInput v-if="user.host == null" v-model="driveCapacityOverrideMb" inline :manual-save="true" type="number" :placeholder="i18n.t('defaultValueIs', { value: instance.driveCapacityPerLocalUserMb })" @update:model-value="applyDriveCapacityOverride">
-						<template #label>{{ i18n.ts.driveCapOverrideLabel }}</template>
-						<template #suffix>MB</template>
-						<template #caption>
-							{{ i18n.ts.driveCapOverrideCaption }}
-						</template>
-					</FormInput>
-				</FormSection>
 				<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch>
 				<FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
 				<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch>
@@ -123,6 +112,17 @@
 
 					<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
 				</FormFolder>
+				<FormSection>
+					<template #label>Drive Capacity Override</template>
+
+					<FormInput v-if="user.host == null" v-model="driveCapacityOverrideMb" inline :manual-save="true" type="number" :placeholder="i18n.t('defaultValueIs', { value: instance.driveCapacityPerLocalUserMb })" @update:model-value="applyDriveCapacityOverride">
+						<template #label>{{ i18n.ts.driveCapOverrideLabel }}</template>
+						<template #suffix>MB</template>
+						<template #caption>
+							{{ i18n.ts.driveCapOverrideCaption }}
+						</template>
+					</FormInput>
+				</FormSection>
 			</div>
 			<div v-else-if="tab === 'chart'" class="_formRoot">
 				<div class="cmhjzshm">