From f3e29c4f6a51ced2babab00821669c2778067bd5 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Tue, 30 Nov 2021 23:03:03 +0900
Subject: [PATCH 01/29] =?UTF-8?q?fix:=20LTL=E3=82=84GTL=E3=81=8C=E7=84=A1?=
 =?UTF-8?q?=E5=8A=B9=E3=81=AB=E3=81=AA=E3=81=A3=E3=81=A6=E3=81=84=E3=82=8B?=
 =?UTF-8?q?=E5=A0=B4=E5=90=88=E3=81=A7=E3=82=82UI=E4=B8=8A=E3=81=AB?=
 =?UTF-8?q?=E3=82=BF=E3=83=96=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C?=
 =?UTF-8?q?=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#8026?=
 =?UTF-8?q?)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* add changelog

* 変換ミス修正
---
 CHANGELOG.md                           | 1 +
 packages/client/src/pages/timeline.vue | 6 +++---
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c38d6027f..208c560f8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@
 - クライアント: Renoteなノート詳細ページから元のノートページに遷移できるように
 
 ### Bugfixes
+- クライアント: LTLやGTLが無効になっている場合でもUI上にタブが表示される問題を修正
 - クライアント: ログインにおいてパスワードが誤っている際のエラーメッセージが正しく表示されない問題を修正
 - クライアント: リアクションツールチップ、Renoteツールチップのユーザーの並び順を修正
 - クライアント: サウンドのマスターボリュームが正しく保存されない問題を修正
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index 81de0277f5..b0a02d17a1 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -66,7 +66,7 @@ export default defineComponent({
 					icon: 'fas fa-home',
 					iconOnly: true,
 					onClick: () => { this.src = 'home'; this.saveSrc(); },
-				}, {
+				}, ...(this.isLocalTimelineAvailable ? [{
 					active: this.src === 'local',
 					title: this.$ts._timelines.local,
 					icon: 'fas fa-comments',
@@ -78,13 +78,13 @@ export default defineComponent({
 					icon: 'fas fa-share-alt',
 					iconOnly: true,
 					onClick: () => { this.src = 'social'; this.saveSrc(); },
-				}, {
+				}] : []), ...(this.isGlobalTimelineAvailable ? [{
 					active: this.src === 'global',
 					title: this.$ts._timelines.global,
 					icon: 'fas fa-globe',
 					iconOnly: true,
 					onClick: () => { this.src = 'global'; this.saveSrc(); },
-				}],
+				}] : [])],
 			})),
 		};
 	},

From 4cc2a561d5cc34b5d8eab2d2f429ef0db9c4a93a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 30 Nov 2021 23:08:34 +0900
Subject: [PATCH 02/29] :art:

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

diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 55afc5217f..011370f7f1 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -248,6 +248,7 @@ export default defineComponent({
 	}
 
 	> .sidebar {
+		border-right: solid 0.5px var(--divider);
 	}
 
 	> .contents {

From f38b6a1806cce7760dafb8d3635ec9654f123df3 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 2 Dec 2021 20:09:12 +0900
Subject: [PATCH 03/29] client: tweak ui

---
 .../client/src/components/form-dialog.vue     | 91 ++++++++++---------
 .../src/components/reaction-tooltip.vue       |  1 +
 .../components/reactions-viewer.details.vue   |  1 +
 .../client/src/components/ui/super-menu.vue   |  2 +-
 packages/client/src/pages/admin/abuses.vue    |  2 +-
 packages/client/src/pages/admin/emojis.vue    |  4 +-
 packages/client/src/pages/admin/files.vue     |  2 +-
 packages/client/src/pages/admin/users.vue     |  2 +-
 packages/client/src/pages/announcements.vue   |  2 +-
 packages/client/src/pages/channels.vue        |  6 +-
 packages/client/src/pages/federation.vue      |  2 +-
 packages/client/src/pages/follow-requests.vue |  2 +-
 packages/client/src/pages/gallery/index.vue   |  8 +-
 packages/client/src/pages/gallery/post.vue    |  2 +-
 .../client/src/pages/my-antennas/index.vue    | 22 +++--
 packages/client/src/pages/my-clips/index.vue  | 29 +++---
 packages/client/src/pages/my-groups/index.vue |  6 +-
 packages/client/src/pages/my-lists/index.vue  | 22 ++---
 packages/client/src/pages/my-lists/list.vue   | 53 ++++++-----
 packages/client/src/pages/page.vue            |  2 +-
 packages/client/src/pages/pages.vue           | 66 ++++++++------
 packages/client/src/pages/settings/apps.vue   |  2 +-
 packages/client/src/pages/settings/index.vue  |  2 +-
 .../client/src/pages/settings/mute-block.vue  |  4 +-
 .../client/src/pages/settings/security.vue    |  2 +-
 .../src/pages/settings/theme.manage.vue       | 32 +++----
 packages/client/src/pages/user/clips.vue      |  2 +-
 .../client/src/pages/user/follow-list.vue     |  2 +-
 packages/client/src/pages/user/gallery.vue    |  2 +-
 packages/client/src/pages/user/pages.vue      |  2 +-
 packages/client/src/pages/user/reactions.vue  |  2 +-
 packages/client/src/scripts/use-tooltip.ts    |  3 +-
 packages/client/src/ui/visitor/kanban.vue     |  2 +-
 33 files changed, 199 insertions(+), 185 deletions(-)

diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue
index 27810d315a..fbf49af5d2 100644
--- a/packages/client/src/components/form-dialog.vue
+++ b/packages/client/src/components/form-dialog.vue
@@ -12,66 +12,67 @@
 	<template #header>
 		{{ title }}
 	</template>
-	<FormBase class="xkpnjxcv">
-		<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
-			<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
-				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormInput>
-			<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
-				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormInput>
-			<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
-				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormTextarea>
-			<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
-				<span v-text="form[item].label || item"></span>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormSwitch>
-			<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
-				<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].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
-			</FormSelect>
-			<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
-				<template #desc><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">
-				<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" #desc>{{ form[item].description }}</template>
-			</FormRange>
-			<FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
-				<span v-text="form[item].content || item"></span>
-			</FormButton>
-		</template>
-	</FormBase>
+
+	<MkSpacer :margin-min="20" :margin-max="32">
+		<div class="xkpnjxcv _formRoot">
+			<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
+				<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1" 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>
+				</FormInput>
+				<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" 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>
+				</FormInput>
+				<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" 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>
+				</FormTextarea>
+				<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]" class="_formBlock">
+					<span v-text="form[item].label || item"></span>
+					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
+				</FormSwitch>
+				<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]" class="_formBlock">
+					<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].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>
+					<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" 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>
+				<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock">
+					<span v-text="form[item].content || item"></span>
+				</MkButton>
+			</template>
+		</div>
+	</MkSpacer>
 </XModalWindow>
 </template>
 
 <script lang="ts">
 import { defineComponent } from 'vue';
 import XModalWindow from '@/components/ui/modal-window.vue';
-import FormBase from './debobigego/base.vue';
-import FormInput from './debobigego/input.vue';
-import FormTextarea from './debobigego/textarea.vue';
-import FormSwitch from './debobigego/switch.vue';
-import FormSelect from './debobigego/select.vue';
-import FormRange from './debobigego/range.vue';
-import FormButton from './debobigego/button.vue';
-import FormRadios from './debobigego/radios.vue';
+import FormInput from './form/input.vue';
+import FormTextarea from './form/textarea.vue';
+import FormSwitch from './form/switch.vue';
+import FormSelect from './form/select.vue';
+import FormRange from './form/range.vue';
+import MkButton from './ui/button.vue';
+import FormRadios from './form/radios.vue';
 
 export default defineComponent({
 	components: {
 		XModalWindow,
-		FormBase,
 		FormInput,
 		FormTextarea,
 		FormSwitch,
 		FormSelect,
 		FormRange,
-		FormButton,
+		MkButton,
 		FormRadios,
 	},
 
diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue
index a52c295277..dda8e7c6d7 100644
--- a/packages/client/src/components/reaction-tooltip.vue
+++ b/packages/client/src/components/reaction-tooltip.vue
@@ -41,6 +41,7 @@ export default defineComponent({
 	> .icon {
 		display: block;
 		width: 60px;
+		font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
 		margin: 0 auto;
 	}
 
diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue
index 63c22b98c4..d6374517a2 100644
--- a/packages/client/src/components/reactions-viewer.details.vue
+++ b/packages/client/src/components/reactions-viewer.details.vue
@@ -62,6 +62,7 @@ export default defineComponent({
 		> .icon {
 			display: block;
 			width: 60px;
+			font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
 			margin: 0 auto;
 		}
 
diff --git a/packages/client/src/components/ui/super-menu.vue b/packages/client/src/components/ui/super-menu.vue
index cb2154c48d..63a1d7063d 100644
--- a/packages/client/src/components/ui/super-menu.vue
+++ b/packages/client/src/components/ui/super-menu.vue
@@ -52,7 +52,7 @@ export default defineComponent({
 
 		> .title {
 			opacity: 0.7;
-			margin: 0 0 8px 12px;
+			margin: 0 0 8px 0;
 		}
 	
 		> .items {
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index ff1c4c57fc..8df20097b3 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -33,7 +33,7 @@
 			</div>
 			-->
 
-			<MkPagination #default="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
+			<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
 				<div v-for="report in items" :key="report.id" class="bcekxzvu _card _gap">
 					<div class="_content target">
 						<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index 6f9a955da2..9c9b3b2d4f 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -7,7 +7,7 @@
 		</MkInput>
 		<MkPagination ref="emojis" :pagination="pagination">
 			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
-			<template #default="{items}">
+			<template v-slot="{items}">
 				<div class="ldhfsamy">
 					<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
 						<img :src="emoji.url" class="img" :alt="emoji.name"/>
@@ -31,7 +31,7 @@
 		</MkInput>
 		<MkPagination ref="remoteEmojis" :pagination="remotePagination">
 			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
-			<template #default="{items}">
+			<template v-slot="{items}">
 				<div class="ldhfsamy">
 					<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
 						<img :src="emoji.url" class="img" :alt="emoji.name"/>
diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue
index a6b0f8e59e..032e394a66 100644
--- a/packages/client/src/pages/admin/files.vue
+++ b/packages/client/src/pages/admin/files.vue
@@ -28,7 +28,7 @@
 					<template #label>MIME type</template>
 				</MkInput>
 			</div>
-			<MkPagination #default="{items}" ref="files" :pagination="pagination" class="urempief">
+			<MkPagination v-slot="{items}" ref="files" :pagination="pagination" class="urempief">
 				<button v-for="file in items" :key="file.id" class="file _panel _button _gap" @click="show(file, $event)">
 					<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
 					<div class="body">
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
index 016a013e51..e7a3437167 100644
--- a/packages/client/src/pages/admin/users.vue
+++ b/packages/client/src/pages/admin/users.vue
@@ -36,7 +36,7 @@
 			</MkInput>
 		</div>
 
-		<MkPagination #default="{items}" ref="users" :pagination="pagination" class="users">
+		<MkPagination v-slot="{items}" ref="users" :pagination="pagination" class="users">
 			<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
 				<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
 				<div class="body">
diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue
index 34879a18bd..ca94640dda 100644
--- a/packages/client/src/pages/announcements.vue
+++ b/packages/client/src/pages/announcements.vue
@@ -1,6 +1,6 @@
 <template>
 <MkSpacer :content-max="800">
-	<MkPagination #default="{items}" :pagination="pagination" class="ruryvtyk _content">
+	<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
 		<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
 			<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
 			<div class="_content">
diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue
index a7bd8a018c..a08c273279 100644
--- a/packages/client/src/pages/channels.vue
+++ b/packages/client/src/pages/channels.vue
@@ -10,20 +10,20 @@
 
 	<div class="_section">
 		<div v-if="tab === 'featured'" class="_content grwlizim featured">
-			<MkPagination #default="{items}" :pagination="featuredPagination">
+			<MkPagination v-slot="{items}" :pagination="featuredPagination">
 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
 			</MkPagination>
 		</div>
 
 		<div v-if="tab === 'following'" class="_content grwlizim following">
-			<MkPagination #default="{items}" :pagination="followingPagination">
+			<MkPagination v-slot="{items}" :pagination="followingPagination">
 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
 			</MkPagination>
 		</div>
 
 		<div v-if="tab === 'owned'" class="_content grwlizim owned">
 			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
-			<MkPagination #default="{items}" :pagination="ownedPagination">
+			<MkPagination v-slot="{items}" :pagination="ownedPagination">
 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
 			</MkPagination>
 		</div>
diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue
index a868c34478..4e5f428ff9 100644
--- a/packages/client/src/pages/federation.vue
+++ b/packages/client/src/pages/federation.vue
@@ -41,7 +41,7 @@
 			</div>
 		</div>
 
-		<MkPagination #default="{items}" ref="instances" :key="host + state" :pagination="pagination">
+		<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
 			<div class="dqokceoi">
 				<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`">
 					<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index a4de393995..54d695091d 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -7,7 +7,7 @@
 				<div>{{ $ts.noFollowRequests }}</div>
 			</div>
 		</template>
-		<template #default="{items}">
+		<template v-slot="{items}">
 			<div v-for="req in items" :key="req.id" class="user _panel">
 				<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
 				<div class="body">
diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue
index a036f4286b..cd0d2a40e4 100644
--- a/packages/client/src/pages/gallery/index.vue
+++ b/packages/client/src/pages/gallery/index.vue
@@ -9,7 +9,7 @@
 	<div v-if="tab === 'explore'">
 		<MkFolder class="_gap">
 			<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
-			<MkPagination #default="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
+			<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
 				<div class="vfpdbgtk">
 					<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
 				</div>
@@ -17,7 +17,7 @@
 		</MkFolder>
 		<MkFolder class="_gap">
 			<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
-			<MkPagination #default="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
+			<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
 				<div class="vfpdbgtk">
 					<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
 				</div>
@@ -25,7 +25,7 @@
 		</MkFolder>
 	</div>
 	<div v-else-if="tab === 'liked'">
-		<MkPagination #default="{items}" :pagination="likedPostsPagination">
+		<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
 			<div class="vfpdbgtk">
 				<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
 			</div>
@@ -33,7 +33,7 @@
 	</div>
 	<div v-else-if="tab === 'my'">
 		<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
-		<MkPagination #default="{items}" :pagination="myPostsPagination">
+		<MkPagination v-slot="{items}" :pagination="myPostsPagination">
 			<div class="vfpdbgtk">
 				<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
 			</div>
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index f145caf28e..096947e6f8 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -36,7 +36,7 @@
 			<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 #default="{items}" :pagination="otherPostsPagination">
+				<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
 					<div class="sdrarzaf">
 						<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
 					</div>
diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue
index 8fc17c3606..d185e796c3 100644
--- a/packages/client/src/pages/my-antennas/index.vue
+++ b/packages/client/src/pages/my-antennas/index.vue
@@ -1,15 +1,17 @@
 <template>
-<div class="ieepwinx _section">
-	<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
+<MkSpacer :content-max="700">
+	<div class="ieepwinx">
+		<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
 
-	<div class="_content">
-		<MkPagination #default="{items}" ref="list" :pagination="pagination">
-			<MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`">
-				<div class="name">{{ antenna.name }}</div>
-			</MkA>
-		</MkPagination>
+		<div class="">
+			<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
+				<MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`">
+					<div class="name">{{ antenna.name }}</div>
+				</MkA>
+			</MkPagination>
+		</div>
 	</div>
-</div>
+</MkSpacer>
 </template>
 
 <script lang="ts">
@@ -29,6 +31,7 @@ export default defineComponent({
 			[symbols.PAGE_INFO]: {
 				title: this.$ts.manageAntennas,
 				icon: 'fas fa-satellite',
+				bg: 'var(--bg)',
 				action: {
 					icon: 'fas fa-plus',
 					handler: this.create
@@ -45,7 +48,6 @@ export default defineComponent({
 
 <style lang="scss" scoped>
 .ieepwinx {
-	padding: 16px;
 
 	> .add {
 		margin: 0 auto 16px auto;
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
index fc2f6d7380..a5bbc3fd2d 100644
--- a/packages/client/src/pages/my-clips/index.vue
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -1,16 +1,16 @@
 <template>
-<div class="_section qtcaoidl">
-	<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
+<MkSpacer :content-max="700">
+	<div class="qtcaoidl">
+		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
 
-	<div class="_content">
-		<MkPagination #default="{items}" ref="list" :pagination="pagination" class="list">
+		<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="list">
 			<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
 				<b>{{ item.name }}</b>
 				<div v-if="item.description" class="description">{{ item.description }}</div>
 			</MkA>
 		</MkPagination>
 	</div>
-</div>
+</MkSpacer>
 </template>
 
 <script lang="ts">
@@ -31,6 +31,7 @@ export default defineComponent({
 			[symbols.PAGE_INFO]: {
 				title: this.$ts.clip,
 				icon: 'fas fa-paperclip',
+				bg: 'var(--bg)',
 				action: {
 					icon: 'fas fa-plus',
 					handler: this.create
@@ -86,17 +87,15 @@ export default defineComponent({
 		margin: 0 auto 16px auto;
 	}
 
-	> ._content {
-		> .list {
-			> .item {
-				display: block;
-				padding: 16px;
+	> .list {
+		> .item {
+			display: block;
+			padding: 16px;
 
-				> .description {
-					margin-top: 8px;
-					padding-top: 8px;
-					border-top: solid 0.5px var(--divider);
-				}
+			> .description {
+				margin-top: 8px;
+				padding-top: 8px;
+				border-top: solid 0.5px var(--divider);
 			}
 		}
 	}
diff --git a/packages/client/src/pages/my-groups/index.vue b/packages/client/src/pages/my-groups/index.vue
index e203b497df..c5019a5e5b 100644
--- a/packages/client/src/pages/my-groups/index.vue
+++ b/packages/client/src/pages/my-groups/index.vue
@@ -12,7 +12,7 @@
 		<div v-if="tab === 'owned'" class="_content">
 			<MkButton primary style="margin: 0 auto var(--margin) auto;" @click="create"><i class="fas fa-plus"></i> {{ $ts.createGroup }}</MkButton>
 
-			<MkPagination #default="{items}" ref="owned" :pagination="ownedPagination">
+			<MkPagination v-slot="{items}" ref="owned" :pagination="ownedPagination">
 				<div v-for="group in items" :key="group.id" class="_card">
 					<div class="_title"><MkA :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</MkA></div>
 					<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
@@ -21,7 +21,7 @@
 		</div>
 
 		<div v-else-if="tab === 'joined'" class="_content">
-			<MkPagination #default="{items}" ref="joined" :pagination="joinedPagination">
+			<MkPagination v-slot="{items}" ref="joined" :pagination="joinedPagination">
 				<div v-for="group in items" :key="group.id" class="_card">
 					<div class="_title">{{ group.name }}</div>
 					<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
@@ -30,7 +30,7 @@
 		</div>
 	
 		<div v-else-if="tab === 'invites'" class="_content">
-			<MkPagination #default="{items}" ref="invitations" :pagination="invitationPagination">
+			<MkPagination v-slot="{items}" ref="invitations" :pagination="invitationPagination">
 				<div v-for="invitation in items" :key="invitation.id" class="_card">
 					<div class="_title">{{ invitation.group.name }}</div>
 					<div class="_content"><MkAvatars :user-ids="invitation.group.userIds"/></div>
diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue
index 645035d4ed..94a869b9ff 100644
--- a/packages/client/src/pages/my-lists/index.vue
+++ b/packages/client/src/pages/my-lists/index.vue
@@ -1,14 +1,16 @@
 <template>
-<div class="qkcjvfiv">
-	<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
+<MkSpacer :content-max="700">
+	<div class="qkcjvfiv">
+		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
 
-	<MkPagination #default="{items}" ref="list" :pagination="pagination" class="lists _content">
-		<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
-			<div class="name">{{ list.name }}</div>
-			<MkAvatars :user-ids="list.userIds"/>
-		</MkA>
-	</MkPagination>
-</div>
+		<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="lists _content">
+			<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
+				<div class="name">{{ list.name }}</div>
+				<MkAvatars :user-ids="list.userIds"/>
+			</MkA>
+		</MkPagination>
+	</div>
+</MkSpacer>
 </template>
 
 <script lang="ts">
@@ -60,8 +62,6 @@ export default defineComponent({
 
 <style lang="scss" scoped>
 .qkcjvfiv {
-	padding: 16px;
-
 	> .add {
 		margin: 0 auto var(--margin) auto;
 	}
diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue
index bf73cdafde..0bfa20514b 100644
--- a/packages/client/src/pages/my-lists/list.vue
+++ b/packages/client/src/pages/my-lists/list.vue
@@ -1,35 +1,37 @@
 <template>
-<div class="mk-list-page">
-	<transition name="zoom" mode="out-in">
-		<div v-if="list" class="_section">
-			<div class="_content">
-				<MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
-				<MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton>
-				<MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton>
+<MkSpacer :content-max="700">
+	<div class="mk-list-page">
+		<transition name="zoom" mode="out-in">
+			<div v-if="list" class="_section">
+				<div class="_content">
+					<MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
+					<MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton>
+					<MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton>
+				</div>
 			</div>
-		</div>
-	</transition>
+		</transition>
 
-	<transition name="zoom" mode="out-in">
-		<div v-if="list" class="_section members _gap">
-			<div class="_title">{{ $ts.members }}</div>
-			<div class="_content">
-				<div class="users">
-					<div v-for="user in users" :key="user.id" class="user _panel">
-						<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
-						<div class="body">
-							<MkUserName :user="user" class="name"/>
-							<MkAcct :user="user" class="acct"/>
-						</div>
-						<div class="action">
-							<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
+		<transition name="zoom" mode="out-in">
+			<div v-if="list" class="_section members _gap">
+				<div class="_title">{{ $ts.members }}</div>
+				<div class="_content">
+					<div class="users">
+						<div v-for="user in users" :key="user.id" class="user _panel">
+							<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
+							<div class="body">
+								<MkUserName :user="user" class="name"/>
+								<MkAcct :user="user" class="acct"/>
+							</div>
+							<div class="action">
+								<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
+							</div>
 						</div>
 					</div>
 				</div>
 			</div>
-		</div>
-	</transition>
-</div>
+		</transition>
+	</div>
+</MkSpacer>
 </template>
 
 <script lang="ts">
@@ -49,6 +51,7 @@ export default defineComponent({
 			[symbols.PAGE_INFO]: computed(() => this.list ? {
 				title: this.list.name,
 				icon: 'fas fa-list-ul',
+				bg: 'var(--bg)',
 			} : null),
 			list: null,
 			users: [],
diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue
index efbdc175d8..bcc09b0235 100644
--- a/packages/client/src/pages/page.vue
+++ b/packages/client/src/pages/page.vue
@@ -48,7 +48,7 @@
 			<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 #default="{items}" :pagination="otherPostsPagination">
+				<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
 					<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_gap"/>
 				</MkPagination>
 			</MkContainer>
diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue
index a8ded5cda9..f1dd64f119 100644
--- a/packages/client/src/pages/pages.vue
+++ b/packages/client/src/pages/pages.vue
@@ -1,50 +1,40 @@
 <template>
-<MkSpacer>
-	<!-- TODO: MkHeaderに統合 -->
-	<MkTab v-if="$i" v-model="tab">
-		<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._pages.featured }}</option>
-		<option value="my"><i class="fas fa-edit"></i> {{ $ts._pages.my }}</option>
-		<option value="liked"><i class="fas fa-heart"></i> {{ $ts._pages.liked }}</option>
-	</MkTab>
+<MkSpacer :content-max="700">
+	<div v-if="tab === 'featured'" class="rknalgpo">
+		<MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
+			<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
+		</MkPagination>
+	</div>
 
-	<div class="_section">
-		<div v-if="tab === 'featured'" class="rknalgpo _content">
-			<MkPagination #default="{items}" :pagination="featuredPagesPagination">
-				<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
-			</MkPagination>
-		</div>
+	<div v-else-if="tab === 'my'" class="rknalgpo my">
+		<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
+		<MkPagination v-slot="{items}" :pagination="myPagesPagination">
+			<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
+		</MkPagination>
+	</div>
 
-		<div v-if="tab === 'my'" class="rknalgpo _content my">
-			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
-			<MkPagination #default="{items}" :pagination="myPagesPagination">
-				<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
-			</MkPagination>
-		</div>
-
-		<div v-if="tab === 'liked'" class="rknalgpo _content">
-			<MkPagination #default="{items}" :pagination="likedPagesPagination">
-				<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/>
-			</MkPagination>
-		</div>
+	<div v-else-if="tab === 'liked'" class="rknalgpo">
+		<MkPagination v-slot="{items}" :pagination="likedPagesPagination">
+			<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/>
+		</MkPagination>
 	</div>
 </MkSpacer>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
 import MkPagePreview from '@/components/page-preview.vue';
 import MkPagination from '@/components/ui/pagination.vue';
 import MkButton from '@/components/ui/button.vue';
-import MkTab from '@/components/tab.vue';
 import * as symbols from '@/symbols';
 
 export default defineComponent({
 	components: {
-		MkPagePreview, MkPagination, MkButton, MkTab
+		MkPagePreview, MkPagination, MkButton
 	},
 	data() {
 		return {
-			[symbols.PAGE_INFO]: {
+			[symbols.PAGE_INFO]: computed(() => ({
 				title: this.$ts.pages,
 				icon: 'fas fa-sticky-note',
 				bg: 'var(--bg)',
@@ -53,7 +43,23 @@ export default defineComponent({
 					text: this.$ts.create,
 					handler: this.create,
 				}],
-			},
+				tabs: [{
+					active: this.tab === 'featured',
+					title: this.$ts._pages.featured,
+					icon: 'fas fa-fire-alt',
+					onClick: () => { this.tab = 'featured'; },
+				}, {
+					active: this.tab === 'my',
+					title: this.$ts._pages.my,
+					icon: 'fas fa-edit',
+					onClick: () => { this.tab = 'my'; },
+				}, {
+					active: this.tab === 'liked',
+					title: this.$ts._pages.liked,
+					icon: 'fas fa-heart',
+					onClick: () => { this.tab = 'liked'; },
+				},]
+			})),
 			tab: 'featured',
 			featuredPagesPagination: {
 				endpoint: 'pages/featured',
diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue
index 10b5fc603e..b5fe4e0aed 100644
--- a/packages/client/src/pages/settings/apps.vue
+++ b/packages/client/src/pages/settings/apps.vue
@@ -7,7 +7,7 @@
 				<div>{{ $ts.nothing }}</div>
 			</div>
 		</template>
-		<template #default="{items}">
+		<template v-slot="{items}">
 			<div v-for="token in items" :key="token.id" class="_debobigegoPanel bfomjevm">
 				<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
 				<div class="body">
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 7a22a8dcd9..bfac1be77d 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -1,7 +1,7 @@
 <template>
 <div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
 	<div v-if="!narrow || page == null" class="nav">
-		<MkSpacer :content-max="700">
+		<MkSpacer :content-max="700" :margin-min="20">
 			<div class="baaadecd">
 				<div class="title">{{ $ts.settings }}</div>
 				<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue
index 4a9633a20d..4f42d5e429 100644
--- a/packages/client/src/pages/settings/mute-block.vue
+++ b/packages/client/src/pages/settings/mute-block.vue
@@ -7,7 +7,7 @@
 	<div v-if="tab === 'mute'">
 		<MkPagination :pagination="mutingPagination" class="muting">
 			<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
-			<template #default="{items}">
+			<template v-slot="{items}">
 				<FormGroup>
 					<FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
 						<MkAcct :user="mute.mutee"/>
@@ -19,7 +19,7 @@
 	<div v-if="tab === 'block'">
 		<MkPagination :pagination="blockingPagination" class="blocking">
 			<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
-			<template #default="{items}">
+			<template v-slot="{items}">
 				<FormGroup>
 					<FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
 						<MkAcct :user="block.blockee"/>
diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue
index 33dc299f5d..069f9d964d 100644
--- a/packages/client/src/pages/settings/security.vue
+++ b/packages/client/src/pages/settings/security.vue
@@ -13,7 +13,7 @@
 	<FormSection>
 		<template #label>{{ $ts.signinHistory }}</template>
 		<FormPagination :pagination="pagination">
-			<template #default="{items}">
+			<template v-slot="{items}">
 				<div>
 					<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
 						<header>
diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue
index ac4edbe66e..c605b1eb64 100644
--- a/packages/client/src/pages/settings/theme.manage.vue
+++ b/packages/client/src/pages/settings/theme.manage.vue
@@ -1,6 +1,6 @@
 <template>
-<FormBase>
-	<FormSelect v-model="selectedThemeId">
+<div class="_formRoot">
+	<FormSelect v-model="selectedThemeId" class="_formBlock">
 		<template #label>{{ $ts.theme }}</template>
 		<optgroup :label="$ts._theme.installedThemes">
 			<option v-for="x in installedThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
@@ -10,31 +10,31 @@
 		</optgroup>
 	</FormSelect>
 	<template v-if="selectedTheme">
-		<FormInput readonly :modelValue="selectedTheme.author">
-			<span>{{ $ts.author }}</span>
+		<FormInput readonly :modelValue="selectedTheme.author" class="_formBlock">
+			<template #label>{{ $ts.author }}</template>
 		</FormInput>
-		<FormTextarea v-if="selectedTheme.desc" readonly :modelValue="selectedTheme.desc">
-			<span>{{ $ts._theme.description }}</span>
+		<FormTextarea v-if="selectedTheme.desc" readonly :modelValue="selectedTheme.desc" class="_formBlock">
+			<template #label>{{ $ts._theme.description }}</template>
 		</FormTextarea>
-		<FormTextarea readonly tall :modelValue="selectedThemeCode">
-			<span>{{ $ts._theme.code }}</span>
-			<template #desc><button class="_textButton" @click="copyThemeCode()">{{ $ts.copy }}</button></template>
+		<FormTextarea readonly tall :modelValue="selectedThemeCode" class="_formBlock">
+			<template #label>{{ $ts._theme.code }}</template>
+			<template #caption><button class="_textButton" @click="copyThemeCode()">{{ $ts.copy }}</button></template>
 		</FormTextarea>
-		<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
+		<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
 	</template>
-</FormBase>
+</div>
 </template>
 
 <script lang="ts">
 import { defineComponent } from 'vue';
 import * as JSON5 from 'json5';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormSelect from '@/components/debobigego/select.vue';
-import FormRadios from '@/components/debobigego/radios.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormRadios from '@/components/form/radios.vue';
 import FormBase from '@/components/debobigego/base.vue';
 import FormGroup from '@/components/debobigego/group.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
+import FormInput from '@/components/form/input.vue';
+import FormButton from '@/components/ui/button.vue';
 import { Theme, builtinThemes } from '@/scripts/theme';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import * as os from '@/os';
diff --git a/packages/client/src/pages/user/clips.vue b/packages/client/src/pages/user/clips.vue
index 9e16bfc45b..aad5317ce0 100644
--- a/packages/client/src/pages/user/clips.vue
+++ b/packages/client/src/pages/user/clips.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<MkPagination #default="{items}" ref="list" :pagination="pagination">
+	<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
 		<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
 			<b>{{ item.name }}</b>
 			<div v-if="item.description" class="description">{{ item.description }}</div>
diff --git a/packages/client/src/pages/user/follow-list.vue b/packages/client/src/pages/user/follow-list.vue
index 2cfb8ee1ad..9fb8943fb8 100644
--- a/packages/client/src/pages/user/follow-list.vue
+++ b/packages/client/src/pages/user/follow-list.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<MkPagination #default="{items}" ref="list" :pagination="pagination" class="mk-following-or-followers">
+	<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="mk-following-or-followers">
 		<div class="users _isolated">
 			<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/>
 		</div>
diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue
index 9def25c2ae..860aa9f44f 100644
--- a/packages/client/src/pages/user/gallery.vue
+++ b/packages/client/src/pages/user/gallery.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<MkPagination #default="{items}" :pagination="pagination">
+	<MkPagination v-slot="{items}" :pagination="pagination">
 		<div class="jrnovfpt">
 			<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
 		</div>
diff --git a/packages/client/src/pages/user/pages.vue b/packages/client/src/pages/user/pages.vue
index eb8f10d8aa..40d1fe3842 100644
--- a/packages/client/src/pages/user/pages.vue
+++ b/packages/client/src/pages/user/pages.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<MkPagination #default="{items}" ref="list" :pagination="pagination">
+	<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
 		<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_gap"/>
 	</MkPagination>
 </div>
diff --git a/packages/client/src/pages/user/reactions.vue b/packages/client/src/pages/user/reactions.vue
index eff456372c..69c27de55b 100644
--- a/packages/client/src/pages/user/reactions.vue
+++ b/packages/client/src/pages/user/reactions.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<MkPagination #default="{items}" ref="list" :pagination="pagination">
+	<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
 		<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb">
 			<div class="header">
 				<MkAvatar class="avatar" :user="user"/>
diff --git a/packages/client/src/scripts/use-tooltip.ts b/packages/client/src/scripts/use-tooltip.ts
index 89e6b1be9d..a9bf6d93db 100644
--- a/packages/client/src/scripts/use-tooltip.ts
+++ b/packages/client/src/scripts/use-tooltip.ts
@@ -1,5 +1,6 @@
 import { isScreenTouching } from '@/os';
 import { Ref, ref } from 'vue';
+import { isDeviceTouch } from './is-device-touch';
 
 export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
 	let isHovering = false;
@@ -13,7 +14,7 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
 
 		// iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、その対策
 		// これが無いと、画面に触れてないのにツールチップが出たりしてしまう
-		if (!isScreenTouching) return;
+		if (isDeviceTouch && !isScreenTouching) return;
 
 		const showing = ref(true);
 		onShow(showing);
diff --git a/packages/client/src/ui/visitor/kanban.vue b/packages/client/src/ui/visitor/kanban.vue
index 2da4dd40b6..ee0f11b838 100644
--- a/packages/client/src/ui/visitor/kanban.vue
+++ b/packages/client/src/ui/visitor/kanban.vue
@@ -16,7 +16,7 @@
 				</div>
 				<div class="announcements panel">
 					<header>{{ $ts.announcements }}</header>
-					<MkPagination #default="{items}" :pagination="announcements" class="list">
+					<MkPagination v-slot="{items}" :pagination="announcements" class="list">
 						<section v-for="announcement in items" :key="announcement.id" class="item">
 							<div class="title">{{ announcement.title }}</div>
 							<div class="content">

From bcd188a0e0dab73a88baf97863a342f772c69007 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 2 Dec 2021 20:20:40 +0900
Subject: [PATCH 04/29] enhance(client): make possible to close image dialog
 with click

Related #8023
---
 CHANGELOG.md                                  |  1 +
 packages/client/src/components/media-list.vue | 11 ++++++++++-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 208c560f8a..37c73c0d64 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
 - API: /antennas/notes API で日付による絞り込みができるように
 - クライアント: アンケートに投票する際に確認ダイアログを出すように
 - クライアント: Renoteなノート詳細ページから元のノートページに遷移できるように
+- クライアント: 画像ポップアップでクリックで閉じられるように
 
 ### Bugfixes
 - クライアント: LTLやGTLが無効になっている場合でもUI上にタブが表示される問題を修正
diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue
index 4eef95af54..38f0f70662 100644
--- a/packages/client/src/components/media-list.vue
+++ b/packages/client/src/components/media-list.vue
@@ -53,7 +53,16 @@ export default defineComponent({
 				gallery: gallery.value,
 				children: '.image',
 				thumbSelector: '.image',
-				pswpModule: PhotoSwipe
+				loop: false,
+				padding: {
+					top: 32,
+					bottom: 32,
+					left: 32,
+					right: 32,
+				},
+				imageClickAction: 'close',
+				tapAction: 'toggle-controls',
+				pswpModule: PhotoSwipe,
 			});
 
 			lightbox.on('itemData', (e) => {

From 3eef0a65c2a47a696e2a4f35ebd2fde876c71ef5 Mon Sep 17 00:00:00 2001
From: nullobsi <me@nullob.si>
Date: Thu, 2 Dec 2021 03:27:42 -0800
Subject: [PATCH 05/29] fix mentions in replies (#8030)

---
 packages/client/src/components/post-form.vue | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index 6f75e12a77..9bad9a84f8 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -289,9 +289,14 @@ export default defineComponent({
 
 		if (this.reply && this.reply.text != null) {
 			const ast = mfm.parse(this.reply.text);
+			const otherHost = this.reply.user.host;
 
 			for (const x of extractMentions(ast)) {
-				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
+				const mention = x.host ?
+													`@${x.username}@${toASCII(x.host)}` :
+													(otherHost == null || otherHost == host) ?
+														`@${x.username}` :
+														`@${x.username}@${toASCII(otherHost)}`;
 
 				// 自分は除外
 				if (this.$i.username == x.username && x.host == null) continue;

From e46bb2f948760e70a0943158510575e8c33aee3e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 2 Dec 2021 20:29:35 +0900
Subject: [PATCH 06/29] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37c73c0d64..d500a3e634 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@
 - クライアント: サウンドのマスターボリュームが正しく保存されない問題を修正
 - クライアント: 一部環境において通知が表示されると操作不能になる問題を修正
 - クライアント: モバイルでタップしたときにツールチップが表示される問題を修正
+- クライアント: リモートインスタンスのノートに返信するとき、対象のノートにそのリモートインスタンス内のユーザーへのメンションが含まれていると、返信テキスト内にローカルユーザーへのメンションとして引き継がれてしまう場合がある問題を修正
 
 ### Changes
 - クライアント: ノートにモデレーターバッジを表示するのを廃止

From 14fa8b177d4520695a54f6ab52e498f9b9370a5d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 2 Dec 2021 20:46:16 +0900
Subject: [PATCH 07/29] client: tweak ui

---
 packages/client/src/components/global/spacer.vue | 2 +-
 packages/client/src/components/note-detailed.vue | 4 ++--
 packages/client/src/components/note.vue          | 3 ++-
 packages/client/src/ui/chat/note.vue             | 3 ++-
 4 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/components/global/spacer.vue b/packages/client/src/components/global/spacer.vue
index 34297a3c8b..417282ad12 100644
--- a/packages/client/src/components/global/spacer.vue
+++ b/packages/client/src/components/global/spacer.vue
@@ -34,7 +34,7 @@ export default defineComponent({
 		const content = ref<HTMLElement>();
 		const margin = ref(0);
 		const adjust = (rect: { width: number; height: number; }) => {
-			if (rect.width > (props.contentMax || 500)) {
+			if (rect.width > props.contentMax || rect.width > 500) {
 				margin.value = props.marginMax;
 			} else {
 				margin.value = props.marginMin;
diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue
index 03f6a767f2..55a02f1e73 100644
--- a/packages/client/src/components/note-detailed.vue
+++ b/packages/client/src/components/note-detailed.vue
@@ -649,7 +649,7 @@ export default defineComponent({
 					text: this.$ts.pin,
 					action: () => this.togglePin(true)
 				} : undefined,
-				...(this.$i.isModerator || this.$i.isAdmin ? [
+				/*...(this.$i.isModerator || this.$i.isAdmin ? [
 					null,
 					{
 						icon: 'fas fa-bullhorn',
@@ -657,7 +657,7 @@ export default defineComponent({
 						action: this.promote
 					}]
 					: []
-				),
+				),*/
 				...(this.appearNote.userId != this.$i.id ? [
 					null,
 					{
diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue
index 25d4b48147..c4040388a9 100644
--- a/packages/client/src/components/note.vue
+++ b/packages/client/src/components/note.vue
@@ -623,6 +623,7 @@ export default defineComponent({
 					text: this.$ts.pin,
 					action: () => this.togglePin(true)
 				} : undefined,
+				/*
 				...(this.$i.isModerator || this.$i.isAdmin ? [
 					null,
 					{
@@ -631,7 +632,7 @@ export default defineComponent({
 						action: this.promote
 					}]
 					: []
-				),
+				),*/
 				...(this.appearNote.userId != this.$i.id ? [
 					null,
 					{
diff --git a/packages/client/src/ui/chat/note.vue b/packages/client/src/ui/chat/note.vue
index 512c87a59e..6927dd0eaf 100644
--- a/packages/client/src/ui/chat/note.vue
+++ b/packages/client/src/ui/chat/note.vue
@@ -632,6 +632,7 @@ export default defineComponent({
 					text: this.$ts.pin,
 					action: () => this.togglePin(true)
 				} : undefined,
+				/*
 				...(this.$i.isModerator || this.$i.isAdmin ? [
 					null,
 					{
@@ -640,7 +641,7 @@ export default defineComponent({
 						action: this.promote
 					}]
 					: []
-				),
+				),*/
 				...(this.appearNote.userId != this.$i.id ? [
 					null,
 					{

From a82ff360c6b97f1db9f05181eae1d7f7d51de480 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 2 Dec 2021 20:58:23 +0900
Subject: [PATCH 08/29] add todo

---
 packages/client/src/directives/tooltip.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
index b96671be35..0e36322cd9 100644
--- a/packages/client/src/directives/tooltip.ts
+++ b/packages/client/src/directives/tooltip.ts
@@ -1,3 +1,6 @@
+// TODO: useTooltip関数使うようにしたい
+// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明
+
 import { Directive, ref } from 'vue';
 import { isDeviceTouch } from '@/scripts/is-device-touch';
 import { popup, alert } from '@/os';

From f33ded310751dd1b4bfd6fb792eec9adfde7019e Mon Sep 17 00:00:00 2001
From: nullobsi <me@nullob.si>
Date: Thu, 2 Dec 2021 18:14:44 -0800
Subject: [PATCH 09/29] feat: Undo Accept (#7980)

* allow breaking of follow

* send undo

* delete by using reject follow
---
 locales/ja-JP.yml                             |  1 +
 .../remote/activitypub/kernel/undo/accept.ts  | 27 ++++++
 .../remote/activitypub/kernel/undo/index.ts   |  4 +-
 .../api/endpoints/following/invalidate.ts     | 82 +++++++++++++++++++
 .../backend/src/services/following/delete.ts  |  7 ++
 packages/client/src/scripts/get-user-menu.ts  | 16 ++++
 6 files changed, 136 insertions(+), 1 deletion(-)
 create mode 100644 packages/backend/src/remote/activitypub/kernel/undo/accept.ts
 create mode 100644 packages/backend/src/server/api/endpoints/following/invalidate.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index d5fcd2d406..d5c009bbcf 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -792,6 +792,7 @@ pubSub: "Pub/Subのアカウント"
 lastCommunication: "直近の通信"
 resolved: "解決済み"
 unresolved: "未解決"
+breakFollow: "フォロワーを解除"
 itsOn: "オンになっています"
 itsOff: "オフになっています"
 emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts
new file mode 100644
index 0000000000..5112d1d4ea
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts
@@ -0,0 +1,27 @@
+import unfollow from '@/services/following/delete';
+import cancelRequest from '@/services/following/requests/cancel';
+import {IAccept} from '../../type';
+import { IRemoteUser } from '@/models/entities/user';
+import { Followings } from '@/models/index';
+import DbResolver from '../../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
+	const dbResolver = new DbResolver();
+
+	const follower = await dbResolver.getUserFromApId(activity.object);
+	if (follower == null) {
+		return `skip: follower not found`;
+	}
+
+	const following = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: actor.id
+	});
+
+	if (following) {
+		await unfollow(follower, actor);
+		return `ok: unfollowed`;
+	}
+
+	return `skip: フォローされていない`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts
index 14b1add152..8de78420e3 100644
--- a/packages/backend/src/remote/activitypub/kernel/undo/index.ts
+++ b/packages/backend/src/remote/activitypub/kernel/undo/index.ts
@@ -1,8 +1,9 @@
 import { IRemoteUser } from '@/models/entities/user';
-import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type';
+import {IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept} from '../../type';
 import unfollow from './follow';
 import unblock from './block';
 import undoLike from './like';
+import undoAccept from './accept';
 import { undoAnnounce } from './announce';
 import Resolver from '../../resolver';
 import { apLogger } from '../../logger';
@@ -29,6 +30,7 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
 	if (isBlock(object)) return await unblock(actor, object);
 	if (isLike(object)) return await undoLike(actor, object);
 	if (isAnnounce(object)) return await undoAnnounce(actor, object);
+	if (isAccept(object)) return await undoAccept(actor, object);
 
 	return `skip: unknown object type ${getApType(object)}`;
 };
diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts
new file mode 100644
index 0000000000..c0e9df3652
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts
@@ -0,0 +1,82 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import * as ms from 'ms';
+import deleteFollowing from '@/services/following/delete';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { Followings, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['following', 'users'],
+
+	limit: {
+		duration: ms('1hour'),
+		max: 100
+	},
+
+	requireCredential: true as const,
+
+	kind: 'write:following',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8'
+		},
+
+		followerIsYourself: {
+			message: 'Follower is yourself.',
+			code: 'FOLLOWER_IS_YOURSELF',
+			id: '07dc03b9-03da-422d-885b-438313707662'
+		},
+
+		notFollowing: {
+			message: 'The other use is not following you.',
+			code: 'NOT_FOLLOWING',
+			id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const followee = user;
+
+	// Check if the follower is yourself
+	if (user.id === ps.userId) {
+		throw new ApiError(meta.errors.followerIsYourself);
+	}
+
+	// Get follower
+	const follower = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Check not following
+	const exist = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
+	});
+
+	if (exist == null) {
+		throw new ApiError(meta.errors.notFollowing);
+	}
+
+	await deleteFollowing(follower, followee);
+
+	return await Users.pack(followee.id, user);
+});
diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts
index 29e3372b6a..ea612147df 100644
--- a/packages/backend/src/services/following/delete.ts
+++ b/packages/backend/src/services/following/delete.ts
@@ -2,6 +2,7 @@ import { publishMainStream, publishUserEvent } from '@/services/stream';
 import { renderActivity } from '@/remote/activitypub/renderer/index';
 import renderFollow from '@/remote/activitypub/renderer/follow';
 import renderUndo from '@/remote/activitypub/renderer/undo';
+import renderReject from '@/remote/activitypub/renderer/reject';
 import { deliver } from '@/queue/index';
 import Logger from '../logger';
 import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
@@ -40,6 +41,12 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
 		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
 		deliver(follower, content, followee.inbox);
 	}
+
+	if (Users.isLocalUser(followee) && Users.isRemoteUser(follower)) {
+		// local user has null host
+		const content = renderActivity(renderReject(renderFollow(follower, followee), followee));
+		deliver(followee, content, follower.inbox);
+	}
 }
 
 export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) {
diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts
index 0c04547101..ebe101bc0f 100644
--- a/packages/client/src/scripts/get-user-menu.ts
+++ b/packages/client/src/scripts/get-user-menu.ts
@@ -109,6 +109,14 @@ export function getUserMenu(user) {
 		return !confirm.canceled;
 	}
 
+	async function invalidateFollow() {
+		os.apiWithDialog('following/invalidate', {
+			userId: user.id
+		}).then(() => {
+			user.isFollowed = !user.isFollowed;
+		})
+	}
+
 	let menu = [{
 		icon: 'fas fa-at',
 		text: i18n.locale.copyUsername,
@@ -153,6 +161,14 @@ export function getUserMenu(user) {
 			action: toggleBlock
 		}]);
 
+		if (user.isFollowed) {
+			menu = menu.concat([{
+				icon: 'fas fa-unlink',
+				text: i18n.locale.breakFollow,
+				action: invalidateFollow
+			}]);
+		}
+
 		menu = menu.concat([null, {
 			icon: 'fas fa-exclamation-circle',
 			text: i18n.locale.reportAbuse,

From 22464c434eb7faf3af4c8c44389996925c04eca6 Mon Sep 17 00:00:00 2001
From: xianon <xianon@hotmail.co.jp>
Date: Fri, 3 Dec 2021 11:19:28 +0900
Subject: [PATCH 10/29] =?UTF-8?q?fix:=20=E7=94=BB=E5=83=8F=E3=83=95?=
 =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE=E7=B8=A6=E6=A8=AA=E3=82=B5?=
 =?UTF-8?q?=E3=82=A4=E3=82=BA=E3=81=AE=E5=8F=96=E5=BE=97=E3=81=A7=20Exif?=
 =?UTF-8?q?=20Orientation=20=E3=82=92=E8=80=83=E6=85=AE=E3=81=99=E3=82=8B?=
 =?UTF-8?q?=20(#8014)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 画像ファイルの縦横サイズの取得で Exif Orientation を考慮する

* test: Add rotate.jpg test

* Webpublic 画像を返す時のみ Exif Orientation を考慮して縦横サイズを返す

* test: Support orientation
---
 packages/backend/src/misc/get-file-info.ts    |   5 ++++
 .../backend/src/models/entities/drive-file.ts |   2 +-
 .../src/models/repositories/drive-file.ts     |  20 +++++++++++++-
 .../backend/src/services/drive/add-file.ts    |   4 +++
 packages/backend/test/get-file-info.ts        |  26 ++++++++++++++++++
 packages/backend/test/resources/rotate.jpg    | Bin 0 -> 12624 bytes
 packages/client/src/components/media-list.vue |  21 ++++++++++----
 7 files changed, 70 insertions(+), 8 deletions(-)
 create mode 100644 packages/backend/test/resources/rotate.jpg

diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts
index 39ba541395..8d7f6b1bf9 100644
--- a/packages/backend/src/misc/get-file-info.ts
+++ b/packages/backend/src/misc/get-file-info.ts
@@ -19,6 +19,7 @@ export type FileInfo = {
 	};
 	width?: number;
 	height?: number;
+	orientation?: number;
 	blurhash?: string;
 	warnings: string[];
 };
@@ -47,6 +48,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
 	// image dimensions
 	let width: number | undefined;
 	let height: number | undefined;
+	let orientation: number | undefined;
 
 	if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) {
 		const imageSize = await detectImageSize(path).catch(e => {
@@ -61,6 +63,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
 		} else if (imageSize.wUnits === 'px') {
 			width = imageSize.width;
 			height = imageSize.height;
+			orientation = imageSize.orientation;
 
 			// 制限を超えている画像は octet-stream にする
 			if (imageSize.width > 16383 || imageSize.height > 16383) {
@@ -87,6 +90,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
 		type,
 		width,
 		height,
+		orientation,
 		blurhash,
 		warnings,
 	};
@@ -163,6 +167,7 @@ async function detectImageSize(path: string): Promise<{
 	height: number;
 	wUnits: string;
 	hUnits: string;
+	orientation?: number;
 }> {
 	const readable = fs.createReadStream(path);
 	const imageSize = await probeImageSize(readable);
diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
index 698dfac222..4ec7b94ed2 100644
--- a/packages/backend/src/models/entities/drive-file.ts
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -77,7 +77,7 @@ export class DriveFile {
 		default: {},
 		comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
 	})
-	public properties: { width?: number; height?: number; avgColor?: string };
+	public properties: { width?: number; height?: number; orientation?: number; avgColor?: string };
 
 	@Index()
 	@Column('boolean')
diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts
index ddf9a46afd..f2f0308dc0 100644
--- a/packages/backend/src/models/repositories/drive-file.ts
+++ b/packages/backend/src/models/repositories/drive-file.ts
@@ -28,6 +28,19 @@ export class DriveFileRepository extends Repository<DriveFile> {
 		);
 	}
 
+	public getPublicProperties(file: DriveFile): DriveFile['properties'] {
+		if (file.properties.orientation != null) {
+			const properties = JSON.parse(JSON.stringify(file.properties));
+			if (file.properties.orientation >= 5) {
+				[properties.width, properties.height] = [properties.height, properties.width];
+			}
+			properties.orientation = undefined;
+			return properties;
+		}
+
+		return file.properties;
+	}
+
 	public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
 		// リモートかつメディアプロキシ
 		if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
@@ -122,7 +135,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
 			size: file.size,
 			isSensitive: file.isSensitive,
 			blurhash: file.blurhash,
-			properties: file.properties,
+			properties: opts.self ? file.properties : this.getPublicProperties(file),
 			url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
 			thumbnailUrl: this.getPublicUrl(file, true, meta),
 			comment: file.comment,
@@ -202,6 +215,11 @@ export const packedDriveFileSchema = {
 					optional: true as const, nullable: false as const,
 					example: 720
 				},
+				orientation: {
+					type: 'number' as const,
+					optional: true as const, nullable: false as const,
+					example: 8
+				},
 				avgColor: {
 					type: 'string' as const,
 					optional: true as const, nullable: false as const,
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index 6c5fefd4ad..a57f9cf068 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -372,12 +372,16 @@ export default async function(
 	const properties: {
 		width?: number;
 		height?: number;
+		orientation?: number;
 	} = {};
 
 	if (info.width) {
 		properties['width'] = info.width;
 		properties['height'] = info.height;
 	}
+	if (info.orientation != null) {
+		properties['orientation'] = info.orientation;
+	}
 
 	const profile = user ? await UserProfiles.findOne(user.id) : null;
 
diff --git a/packages/backend/test/get-file-info.ts b/packages/backend/test/get-file-info.ts
index cc9eefbfc6..a0146bd815 100644
--- a/packages/backend/test/get-file-info.ts
+++ b/packages/backend/test/get-file-info.ts
@@ -17,6 +17,7 @@ describe('Get file info', () => {
 			},
 			width: undefined,
 			height: undefined,
+			orientation: undefined,
 		});
 	}));
 
@@ -34,6 +35,7 @@ describe('Get file info', () => {
 			},
 			width: 512,
 			height: 512,
+			orientation: undefined,
 		});
 	}));
 
@@ -51,6 +53,7 @@ describe('Get file info', () => {
 			},
 			width: 256,
 			height: 256,
+			orientation: undefined,
 		});
 	}));
 
@@ -68,6 +71,7 @@ describe('Get file info', () => {
 			},
 			width: 256,
 			height: 256,
+			orientation: undefined,
 		});
 	}));
 
@@ -85,6 +89,7 @@ describe('Get file info', () => {
 			},
 			width: 256,
 			height: 256,
+			orientation: undefined,
 		});
 	}));
 
@@ -102,6 +107,7 @@ describe('Get file info', () => {
 			},
 			width: 256,
 			height: 256,
+			orientation: undefined,
 		});
 	}));
 
@@ -120,6 +126,7 @@ describe('Get file info', () => {
 			},
 			width: 256,
 			height: 256,
+			orientation: undefined,
 		});
 	}));
 
@@ -137,6 +144,25 @@ describe('Get file info', () => {
 			},
 			width: 25000,
 			height: 25000,
+			orientation: undefined,
+		});
+	}));
+
+	it('Rotate JPEG', async (async () => {
+		const path = `${__dirname}/resources/rotate.jpg`;
+		const info = await getFileInfo(path) as any;
+		delete info.warnings;
+		delete info.blurhash;
+		assert.deepStrictEqual(info, {
+			size: 12624,
+			md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
+			type: {
+				mime: 'image/jpeg',
+				ext: 'jpg'
+			},
+			width: 512,
+			height: 256,
+			orientation: 8,
 		});
 	}));
 });
diff --git a/packages/backend/test/resources/rotate.jpg b/packages/backend/test/resources/rotate.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..477c2baf5bbd0039dc08fa36e3d4b3cd5d39ecb9
GIT binary patch
literal 12624
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

literal 0
HcmV?d00001

diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue
index 38f0f70662..344ad8784e 100644
--- a/packages/client/src/components/media-list.vue
+++ b/packages/client/src/components/media-list.vue
@@ -44,12 +44,18 @@ export default defineComponent({
 
 		onMounted(() => {
 			const lightbox = new PhotoSwipeLightbox({
-				dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({
-					src: media.url,
-					w: media.properties.width,
-					h: media.properties.height,
-					alt: media.name,
-				})),
+				dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => {
+					const item = {
+						src: media.url,
+						w: media.properties.width,
+						h: media.properties.height,
+						alt: media.name,
+					};
+					if (media.properties.orientation != null && media.properties.orientation >= 5) {
+						[item.w, item.h] = [item.h, item.w];
+					}
+					return item;
+				}),
 				gallery: gallery.value,
 				children: '.image',
 				thumbSelector: '.image',
@@ -77,6 +83,9 @@ export default defineComponent({
 				itemData.src = file.url;
 				itemData.w = Number(file.properties.width);
 				itemData.h = Number(file.properties.height);
+				if (file.properties.orientation != null && file.properties.orientation >= 5) {
+					[itemData.w, itemData.h] = [itemData.h, itemData.w];
+				}
 				itemData.msrc = file.thumbnailUrl;
 				itemData.thumbCropped = true;
 			});

From e617ced1d35bbc68d685ebdb3be09894621237ec Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 11:43:05 +0900
Subject: [PATCH 11/29] refactoring

https: //github.com/misskey-dev/misskey/pull/7901
Co-Authored-By: MeiMei <30769358+mei23@users.noreply.github.com>
---
 .../activitypub/kernel/reject/follow.ts       |   7 +-
 .../endpoints/following/requests/reject.ts    |   2 +-
 .../backend/src/services/following/reject.ts  | 105 ++++++++++++++++++
 .../src/services/following/requests/reject.ts |  46 --------
 4 files changed, 110 insertions(+), 50 deletions(-)
 create mode 100644 packages/backend/src/services/following/reject.ts
 delete mode 100644 packages/backend/src/services/following/requests/reject.ts

diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts
index 356547440f..049437b18f 100644
--- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts
+++ b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts
@@ -1,8 +1,9 @@
 import { IRemoteUser } from '@/models/entities/user';
-import reject from '@/services/following/requests/reject';
+import { remoteReject } from '@/services/following/reject';
 import { IFollow } from '../../type';
 import DbResolver from '../../db-resolver';
 import { relayRejected } from '@/services/relay';
+import { Users } from '@/models';
 
 export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
 	// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
@@ -14,7 +15,7 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
 		return `skip: follower not found`;
 	}
 
-	if (follower.host != null) {
+	if (!Users.isLocalUser(follower)) {
 		return `skip: follower is not a local user`;
 	}
 
@@ -24,6 +25,6 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
 		return await relayRejected(match[1]);
 	}
 
-	await reject(actor, follower);
+	await remoteReject(actor, follower);
 	return `ok`;
 };
diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts
index 620324361f..30d0e094c3 100644
--- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts
+++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import { ID } from '@/misc/cafy-id';
-import rejectFollowRequest from '@/services/following/requests/reject';
+import { rejectFollowRequest } from '@/services/following/reject';
 import define from '../../../define';
 import { ApiError } from '../../../error';
 import { getUser } from '../../../common/getters';
diff --git a/packages/backend/src/services/following/reject.ts b/packages/backend/src/services/following/reject.ts
new file mode 100644
index 0000000000..0ec4d7d00c
--- /dev/null
+++ b/packages/backend/src/services/following/reject.ts
@@ -0,0 +1,105 @@
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderFollow from '@/remote/activitypub/renderer/follow';
+import renderReject from '@/remote/activitypub/renderer/reject';
+import { deliver } from '@/queue/index';
+import { publishMainStream, publishUserEvent } from '@/services/stream';
+import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
+import { Users, FollowRequests, Followings } from '@/models/index';
+import { decrementFollowing } from './delete';
+
+type Local = ILocalUser | { id: User['id']; host: User['host']; uri: User['host'] };
+type Remote = IRemoteUser;
+type Both = Local | Remote;
+
+/**
+ * API following/request/reject
+ */
+export async function rejectFollowRequest(user: Local, follower: Both) {
+	if (Users.isRemoteUser(follower)) {
+		deliverReject(user, follower);
+	}
+
+	await removeFollowRequest(user, follower);
+
+	if (Users.isLocalUser(follower)) {
+		publishUnfollow(user, follower);
+	}
+}
+
+/**
+ * API following/reject
+ */
+export async function rejectFollow(user: Local, follower: Both) {
+	if (Users.isRemoteUser(follower)) {
+		deliverReject(user, follower);
+	}
+
+	await removeFollow(user, follower);
+
+	if (Users.isLocalUser(follower)) {
+		publishUnfollow(user, follower);
+	}
+}
+
+/**
+ * AP Reject/Follow
+ */
+export async function remoteReject(actor: Remote, follower: Local) {
+	await removeFollowRequest(actor, follower);
+	await removeFollow(actor, follower);
+	publishUnfollow(actor, follower);
+}
+
+/**
+ * Remove follow request record
+ */
+async function removeFollowRequest(followee: Both, follower: Both) {
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	if (!request) return;
+
+	await FollowRequests.delete(request.id);
+}
+
+/**
+ * Remove follow record
+ */
+async function removeFollow(followee: Both, follower: Both) {
+	const following = await Followings.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	if (!following) return;
+
+	await Followings.delete(following.id);
+	decrementFollowing(follower, followee);
+}
+
+/**
+ * Deliver Reject to remote
+ */
+async function deliverReject(followee: Local, follower: Remote) {
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	const content = renderActivity(renderReject(renderFollow(follower, followee, request?.requestId || undefined), followee));
+	deliver(followee, content, follower.inbox);
+}
+
+/**
+ * Publish unfollow to local
+ */
+async function publishUnfollow(followee: Both, follower: Local) {
+	const packedFollowee = await Users.pack(followee.id, follower, {
+		detail: true
+	});
+
+	publishUserEvent(follower.id, 'unfollow', packedFollowee);
+	publishMainStream(follower.id, 'unfollow', packedFollowee);
+}
diff --git a/packages/backend/src/services/following/requests/reject.ts b/packages/backend/src/services/following/requests/reject.ts
deleted file mode 100644
index 41cebd9e41..0000000000
--- a/packages/backend/src/services/following/requests/reject.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderFollow from '@/remote/activitypub/renderer/follow';
-import renderReject from '@/remote/activitypub/renderer/reject';
-import { deliver } from '@/queue/index';
-import { publishMainStream, publishUserEvent } from '@/services/stream';
-import { User, ILocalUser } from '@/models/entities/user';
-import { Users, FollowRequests, Followings } from '@/models/index';
-import { decrementFollowing } from '../delete';
-
-export default async function(followee: { id: User['id']; host: User['host']; uri: User['host'] }, follower: User) {
-	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
-		const request = await FollowRequests.findOne({
-			followeeId: followee.id,
-			followerId: follower.id
-		});
-
-		const content = renderActivity(renderReject(renderFollow(follower, followee, request!.requestId!), followee));
-		deliver(followee, content, follower.inbox);
-	}
-
-	const request = await FollowRequests.findOne({
-		followeeId: followee.id,
-		followerId: follower.id
-	});
-
-	if (request) {
-		await FollowRequests.delete(request.id);
-	} else {
-		const following = await Followings.findOne({
-			followeeId: followee.id,
-			followerId: follower.id
-		});
-
-		if (following) {
-			await Followings.delete(following.id);
-			decrementFollowing(follower, followee);
-		}
-	}
-
-	Users.pack(followee.id, follower, {
-		detail: true
-	}).then(packed => {
-		publishUserEvent(follower.id, 'unfollow', packed);
-		publishMainStream(follower.id, 'unfollow', packed);
-	});
-}

From 902bed4db32c0e5c966891810e418d9f9086aa37 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 12:00:11 +0900
Subject: [PATCH 12/29] client: tweak ui

---
 packages/client/src/components/media-list.vue | 7 ++++++-
 packages/client/src/components/ui/menu.vue    | 1 +
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue
index 344ad8784e..79fe36b540 100644
--- a/packages/client/src/components/media-list.vue
+++ b/packages/client/src/components/media-list.vue
@@ -60,11 +60,16 @@ export default defineComponent({
 				children: '.image',
 				thumbSelector: '.image',
 				loop: false,
-				padding: {
+				padding: window.innerWidth > 500 ? {
 					top: 32,
 					bottom: 32,
 					left: 32,
 					right: 32,
+				} : {
+					top: 0,
+					bottom: 0,
+					left: 0,
+					right: 0,
 				},
 				imageClickAction: 'close',
 				tapAction: 'toggle-controls',
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index 6ca5e32555..687ce5e548 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -153,6 +153,7 @@ export default defineComponent({
 	box-sizing: border-box;
 	min-width: 200px;
 	overflow: auto;
+	overscroll-behavior: contain;
 
 	&.center {
 		> .item {

From 584ceb37144e17de7b1523e218f846c9802d2c14 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 12:01:31 +0900
Subject: [PATCH 13/29] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d500a3e634..296f8ec291 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
 - クライアント: アンケートに投票する際に確認ダイアログを出すように
 - クライアント: Renoteなノート詳細ページから元のノートページに遷移できるように
 - クライアント: 画像ポップアップでクリックで閉じられるように
+- フォロワーを解除できる機能
 
 ### Bugfixes
 - クライアント: LTLやGTLが無効になっている場合でもUI上にタブが表示される問題を修正
@@ -23,6 +24,7 @@
 - クライアント: 一部環境において通知が表示されると操作不能になる問題を修正
 - クライアント: モバイルでタップしたときにツールチップが表示される問題を修正
 - クライアント: リモートインスタンスのノートに返信するとき、対象のノートにそのリモートインスタンス内のユーザーへのメンションが含まれていると、返信テキスト内にローカルユーザーへのメンションとして引き継がれてしまう場合がある問題を修正
+- クライアント: 画像ビューワーで全体表示した時に上側の一部しか表示されない画像がある問題を修正
 
 ### Changes
 - クライアント: ノートにモデレーターバッジを表示するのを廃止

From c722225c805aab6bbf9ae45e96079817ca4f611e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 12:41:30 +0900
Subject: [PATCH 14/29] client: tweak ui

---
 packages/client/src/pages/page.vue | 13 +++----------
 1 file changed, 3 insertions(+), 10 deletions(-)

diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue
index bcc09b0235..3a4803c3a3 100644
--- a/packages/client/src/pages/page.vue
+++ b/packages/client/src/pages/page.vue
@@ -1,5 +1,5 @@
 <template>
-<div>
+<MkSpacer :content-max="700">
 	<transition name="fade" mode="out-in">
 		<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
 			<div class="_block main">
@@ -56,7 +56,7 @@
 		<MkError v-else-if="error" @retry="fetch()"/>
 		<MkLoading v-else/>
 	</transition>
-</div>
+</MkSpacer>
 </template>
 
 <script lang="ts">
@@ -201,14 +201,7 @@ export default defineComponent({
 }
 
 .xcukqgmh {
-	--padding: 32px;
-
-	&.max-width_450px {
-		--padding: 16px;
-	}
-
 	> .main {
-		padding: var(--padding);
 
 		> .header {
 			padding: 16px;
@@ -302,7 +295,7 @@ export default defineComponent({
 	}
 
 	> .footer {
-		margin: var(--padding);
+		margin: var(--margin) 0 var(--margin) 0;
 		font-size: 85%;
 		opacity: 0.75;
 	}

From 5fe2e8a59a7b45a76fb4e87ea306732301b5c36f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 13:52:57 +0900
Subject: [PATCH 15/29] client: tweak ui

---
 .../client/src/components/global/spacer.vue   |  8 +++-
 .../client/src/components/poll-editor.vue     |  2 -
 packages/client/src/pages/timeline.vue        | 37 ++++++++-----------
 packages/client/src/ui/classic.vue            |  1 +
 packages/client/src/ui/deck.vue               | 13 ++++---
 5 files changed, 32 insertions(+), 29 deletions(-)

diff --git a/packages/client/src/components/global/spacer.vue b/packages/client/src/components/global/spacer.vue
index 417282ad12..45481a2c8d 100644
--- a/packages/client/src/components/global/spacer.vue
+++ b/packages/client/src/components/global/spacer.vue
@@ -7,7 +7,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+import { defineComponent, inject, onMounted, onUnmounted, ref } from 'vue';
 
 export default defineComponent({
 	props: {
@@ -33,7 +33,13 @@ export default defineComponent({
 		const root = ref<HTMLElement>();
 		const content = ref<HTMLElement>();
 		const margin = ref(0);
+		const shouldSpacerMin = inject('shouldSpacerMin', false);
 		const adjust = (rect: { width: number; height: number; }) => {
+			if (shouldSpacerMin) {
+				margin.value = props.marginMin;
+				return;
+			}
+
 			if (rect.width > props.contentMax || rect.width > 500) {
 				margin.value = props.marginMax;
 			} else {
diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue
index c2f760acbd..70ffb35d42 100644
--- a/packages/client/src/components/poll-editor.vue
+++ b/packages/client/src/components/poll-editor.vue
@@ -206,8 +206,6 @@ export default defineComponent({
 
 			> .input {
 				flex: 1;
-				margin-top: 16px;
-				margin-bottom: 0;
 			}
 
 			> button {
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index b0a02d17a1..494932c602 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -1,20 +1,22 @@
 <template>
-<div v-size="{ min: [800] }" v-hotkey.global="keymap" class="cmuxhskf">
-	<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
-	<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
+<MkSpacer :content-max="800">
+	<div v-hotkey.global="keymap" class="cmuxhskf">
+		<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
+		<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
 
-	<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
-	<div class="tl _block">
-		<XTimeline ref="tl" :key="src"
-			class="tl"
-			:src="src"
-			:sound="true"
-			@before="before()"
-			@after="after()"
-			@queue="queueUpdated"
-		/>
+		<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
+		<div class="tl _block">
+			<XTimeline ref="tl" :key="src"
+				class="tl"
+				:src="src"
+				:sound="true"
+				@before="before()"
+				@after="after()"
+				@queue="queueUpdated"
+			/>
+		</div>
 	</div>
-</div>
+</MkSpacer>
 </template>
 
 <script lang="ts">
@@ -188,8 +190,6 @@ export default defineComponent({
 
 <style lang="scss" scoped>
 .cmuxhskf {
-	padding: var(--margin);
-
 	> .new {
 		position: sticky;
 		top: calc(var(--stickyTop, 0px) + 16px);
@@ -213,10 +213,5 @@ export default defineComponent({
 		border-radius: var(--radius);
 		overflow: clip;
 	}
-
-	&.min-width_800px {
-		max-width: 800px;
-		margin: 0 auto;
-	}
 }
 </style>
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index fe533662d0..684a075c04 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -86,6 +86,7 @@ export default defineComponent({
 	provide() {
 		return {
 			shouldHeaderThin: this.showMenuOnTop,
+			shouldSpacerMin: true,
 		};
 	},
 
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 329716664e..4f1efb0a4c 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -49,11 +49,14 @@ export default defineComponent({
 	},
 
 	provide() {
-		return deckStore.state.navWindow ? {
-			navHook: (url) => {
-				os.pageWindow(url);
-			}
-		} : {};
+		return {
+			shouldSpacerMin: true,
+			...deckStore.state.navWindow ? {
+				navHook: (url) => {
+					os.pageWindow(url);
+				}
+			} : {}
+		};
 	},
 
 	data() {

From 5c8561c9038351c88009a816d6ae0efd543a4dfc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 13:55:30 +0900
Subject: [PATCH 16/29] client: tweak ui

---
 CHANGELOG.md                                   | 1 +
 packages/client/src/components/poll-editor.vue | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 296f8ec291..2164ecdbdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
 - クライアント: アンケートに投票する際に確認ダイアログを出すように
 - クライアント: Renoteなノート詳細ページから元のノートページに遷移できるように
 - クライアント: 画像ポップアップでクリックで閉じられるように
+- クライアント: デザインの調整
 - フォロワーを解除できる機能
 
 ### Bugfixes
diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue
index 70ffb35d42..fad0cf1593 100644
--- a/packages/client/src/components/poll-editor.vue
+++ b/packages/client/src/components/poll-editor.vue
@@ -221,7 +221,7 @@ export default defineComponent({
 	}
 
 	> section {
-		margin: 16px 0 -16px 0;
+		margin: 16px 0 0 0;
 
 		> div {
 			margin: 0 8px;

From e46e88344ca2b50bb131c2e2e7503a91bff82232 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 14:14:58 +0900
Subject: [PATCH 17/29] New Crowdin updates (#8007)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

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

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Indonesian)
---
 locales/ar-SA.yml |  25 ++++++
 locales/de-DE.yml |   4 +
 locales/en-US.yml |   4 +-
 locales/eo-UY.yml | 201 +++++++++++++++++++++++++++++++---------------
 locales/es-ES.yml |   2 +
 locales/fr-FR.yml |   1 +
 locales/id-ID.yml |   6 ++
 locales/it-IT.yml |   1 +
 locales/ja-KS.yml |   1 +
 locales/ko-KR.yml |   1 +
 locales/nl-NL.yml | 188 +++++++++++++++++++++++++++++++++++++++++++
 locales/pl-PL.yml |   1 +
 locales/pt-PT.yml |  11 +++
 locales/ru-RU.yml |   1 +
 locales/uk-UA.yml |   1 +
 locales/zh-CN.yml |   4 +
 locales/zh-TW.yml |   1 +
 17 files changed, 389 insertions(+), 64 deletions(-)

diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index cd69d921b8..c831fafff1 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -734,7 +734,10 @@ translate: "ترجم"
 translatedFrom: "تُرجم من {x}"
 accountDeletionInProgress: "حذف الحساب جارٍ"
 usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله."
+keepCw: "أبقِ على تحذيرات المحتوى"
 lastCommunication: "آخر تواصل"
+resolved: "عولج"
+unresolved: "لم يعالج"
 itsOn: "مفعّل"
 itsOff: "معطّل"
 emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
@@ -747,6 +750,16 @@ makeReactionsPublicDescription: "هذا سيجعل قائمة تفاعلاتك 
 classic: "تقليدي"
 muteThread: "اكتم النقاش"
 unmuteThread: "ارفع الكتم عن النقاش"
+deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟"
+incorrectPassword: "كلمة السر خاطئة."
+_emailUnavailable:
+  used: "هذا البريد الإلكتروني مستخدم"
+  format: "صيغة البريد الإلكتروني غير صالحة"
+  mx: "خادم البريد الإلكتروني غير صالح"
+  smtp: "خادم البريد الإلكتروتي لا يستجيب"
+_ffVisibility:
+  public: "علني"
+  private: "خاص"
 _signup:
   almostThere: "كدت تنتهي"
   emailAddressInfo: "رجاءً أدخل بريدك الإلكتروني."
@@ -829,6 +842,7 @@ _mfm:
   font: "الخط"
   rainbow: "قوس قزح"
   rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف"
+  rotate: "تدوير"
 _reversi:
   gameSettings: "إعدادات اللعبة"
   chooseBoard: "اختر اللوح"
@@ -980,9 +994,13 @@ _tutorial:
   step7_2: "إذا أردت معرفة المزيد عن ميسكي زر {help}."
   step7_3: "حظًا سعيدًا واستمتع بوقتك مع ميسكي! 🚀"
 _2fa:
+  alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين."
   registerDevice: "سجّل جهازًا جديدًا"
   registerKey: "تسجيل مفتاح أمان جديد"
   step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})."
+  step2: "امسح رمز الاستجابة السريعة الموجد على الشاشة."
+  step3: "أدخل الرمز الموجود في تطبيقك لإكمال التثبيت."
+  step4: "من هذه اللحظة أثناء ولوجك سيُطلب منك الرمز."
 _permissions:
   "read:account": "اعرض معلومات حسابك"
   "write:account": "تعديل معلومات حسابك"
@@ -993,6 +1011,7 @@ _permissions:
   "read:favorites": "اعرض المفضلة"
   "write:favorites": "عدّل المفضلة"
   "read:following": "اعرض معلومات متابَعيك"
+  "write:following": "تابع أو ألغ متابعة حسابات"
   "read:messaging": "اعرض المحادثات"
   "write:messaging": "اكتب أو احذف رسائل محادثة"
   "read:mutes": "اعرض قائمة المستخدمين المكتومين"
@@ -1005,11 +1024,14 @@ _permissions:
   "write:votes": "صوّت"
   "read:pages": "اعرض صفحاتك"
   "write:pages": "عدّل أو احذف صفحاتك"
+  "read:page-likes": "يعرض ما أعجبك من ملاحظات في صفحات"
   "read:user-groups": "اعرض فِرق المستخدمين"
   "write:user-groups": "عدّل أو احذف فِرق المستخدمين"
+  "read:channels": "طالع قنواتك"
   "write:channels": "عدّل القنوات"
   "read:gallery": "اعرض المعرض"
   "write:gallery": "عدّل المعرض"
+  "read:gallery-likes": "يعرض ما أعجبك من مشاركات المعرض"
 _auth:
   shareAccess: "أتريد التفويض لـ \"{name}\" بالوصول لحسابك؟"
   shareAccessAsk: "هل تخول لهذا التطبيق الوصول لحسابك؟"
@@ -1173,6 +1195,7 @@ _rooms:
     tv: "تلفاز"
     pinguin: "بطريق"
     sofa: "أريكة"
+    bin: "سلة مهملات"
     banknote: "أوراق نقدية"
 _pages:
   newPage: "أنشئ صفحة جديدة"
@@ -1212,6 +1235,7 @@ _pages:
       name: "اسم المتغير"
       text: "العنوان"
       default: "القيمة الافتراضية"
+    textareaInput: "مدخل نصي متعدد الأسطر"
     _textareaInput:
       name: "اسم المتغير"
       text: "العنوان"
@@ -1227,6 +1251,7 @@ _pages:
     note: "ملاحظة مضمّنة"
     _note:
       id: "معرّف الملاحظة"
+      idDescription: "كبديل يمكنك إدخال رابك الملاحظة هنا"
       detailed: "عرض مفصّل"
     switch: "بدّل"
     _switch:
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 39748e2b23..030d13bec4 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -808,6 +808,8 @@ ffVisibility: "Sichtbarkeit von Gefolgten/Followern"
 ffVisibilityDescription: "Konfiguriere wer sehen kann, wem du folgst sowie wer dir folgt."
 continueThread: "Weiteren Threadverlauf anzeigen"
 deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzdem fortfahren?"
+incorrectPassword: "Falsches Passwort."
+voteConfirm: "Wirklich für \"{choice}\" abstimmen?"
 _emailUnavailable:
   used: "Diese Email-Adresse wird bereits verwendet"
   format: "Das Format dieser Email-Adresse ist ungültig"
@@ -931,6 +933,8 @@ _mfm:
   rainbowDescription: "Lässt den Inhalt in Regenbogenfarben erscheinen."
   sparkle: "Glitzer"
   sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt."
+  rotate: "Drehen"
+  rotateDescription: "Dreht den Inhalt um einen angegebenen Winkel"
 _reversi:
   reversi: "Reversi"
   gameSettings: "Spieleinstellungen"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index c5c8afb50f..5388a7a636 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -808,6 +808,8 @@ ffVisibility: "Follows/Followers Visibility"
 ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you."
 continueThread: "View thread continuation"
 deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
+incorrectPassword: "Incorrect password."
+voteConfirm: "Confirm your vote for \"{choice}\"?"
 _emailUnavailable:
   used: "This email address is already being used"
   format: "The format of this email address is invalid"
@@ -932,7 +934,7 @@ _mfm:
   sparkle: "Sparkle"
   sparkleDescription: "Gives content a sparkling particle effect."
   rotate: "Rotate"
-  rotateDescription: "Rotates the content by 90 degrees"
+  rotateDescription: "Turns content by a specified angle."
 _reversi:
   reversi: "Reversi"
   gameSettings: "Game settings"
diff --git a/locales/eo-UY.yml b/locales/eo-UY.yml
index 7b641303b1..a57206e090 100644
--- a/locales/eo-UY.yml
+++ b/locales/eo-UY.yml
@@ -2,18 +2,18 @@
 _lang_: "Esperanto"
 headlineMisskey: "Jen la reto konektata de notoj"
 introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por paroli vian penson al iuj ĉirkaŭ vi. 📡\nLa funkcion \"reago\" ebligas esprimi rapide vian senton pri ies noto en Fediverso. 👍\nBonvole esploru novan mondon. 🚀"
-monthAndDay: "La {day}a de la {month}a"
+monthAndDay: "la {day}a de la {month}a"
 search: "Serĉi"
 notifications: "Sciigoj"
 username: "Uzantnomo"
 password: "Pasvorto"
 forgotPassword: "Ĉu vi forgesis pasvorton?"
-fetchingAsApObject: "Informpetado de kunfederaĵo…"
-ok: "Akcepteble"
-gotIt: "Mi komprenas"
+fetchingAsApObject: "Informpetado de la Fediverso…"
+ok: "OK"
+gotIt: "Kompreni"
 cancel: "Nuligi"
 enterUsername: "Entajpu uzantnomon"
-renotedBy: "Noto plusendita de {user}"
+renotedBy: "Plusendita de {user}"
 noNotes: "Neniu noto!"
 noNotifications: "Vi ne havas sciigojn."
 instance: "Nodo"
@@ -35,22 +35,22 @@ addUser: "Aldoni uzanton"
 favorite: "Preferi"
 favorites: "Preferaĵoj"
 unfavorite: "Malpreferi"
-favorited: "Aldonita al via listo de preferaĵoj."
-alreadyFavorited: "Jam aldonita al via listo de preferaĵoj."
-cantFavorite: "Ĝi ne povis esti aldonita al via listo de preferaĵoj."
+favorited: "Aldonita al viaj preferaĵoj."
+alreadyFavorited: "Jam aldonita al viaj preferaĵoj."
+cantFavorite: "Oni ne povis aldoni al viaj preferaĵoj."
 pin: "Alpingli"
 unpin: "Depingli"
 copyContent: "Kopii enhavon"
 copyLink: "Kopii ligilon"
 delete: "Forviŝi"
 deleteAndEdit: "Forviŝi kaj redakti"
-deleteAndEditConfirm: "Ĉu vi certas ke vi volas redakti forviŝinte la noton? Tio forviŝos ankaŭ ĉiujn reagojn, plusendojn, kaj respondojn apartenantajn al ĝi."
+deleteAndEditConfirm: "Ĉu vi certas ke vi volas redakti foriginte la noton? Tio forviŝos reagojn, plusendojn, kaj respondojn ĉiujn apartenantajn al ĝi."
 addToList: "Aldoni al listo"
 sendMessage: "Sendi mesaĝon"
 copyUsername: "Kopii uzantnomon"
 searchUser: "Serĉi uzanton"
 reply: "Respondi"
-loadMore: "Vidu pli"
+loadMore: "Vidi pli"
 showMore: "Vidi pli"
 youGotNewFollower: "eksekvis vin"
 receiveFollowRequest: "Peto de sekvado estas ricevita"
@@ -77,10 +77,11 @@ manageLists: "Administri liston"
 error: "Eraro"
 somethingHappened: "Problemo okazis"
 retry: "Provi denove"
+serverIsDead: "La servilo ne respondas. Vole atendu iom kaj penu denove."
 enterListName: "Entajpu nomon de la listo"
 privacy: "Privateco"
 makeFollowManuallyApprove: "Eksekvi vin devas peti al vi"
-defaultNoteVisibility: "Implicitaĵo de videbleco"
+defaultNoteVisibility: "Implicita videbleco de la noto"
 follow: "Sekvi"
 followRequest: "Peti de sekvado"
 followRequests: "Petoj de sekvado"
@@ -88,10 +89,10 @@ unfollow: "Ne plu sekvi"
 followRequestPending: "Atendado akcepti vian peton de eksekvado"
 enterEmoji: "Entajpu emoĵion"
 renote: "Plusendi la noton"
-unrenote: "Malfari plusendadon"
+unrenote: "Malfari plusendon"
 renoted: "Sukcese plusendita"
 cantRenote: "Oni ne povas plusendi la noton."
-cantReRenote: "Plusendo de noto ne estas plusendebla."
+cantReRenote: "Plusendo ne estas plusendebla."
 quote: "Citi"
 pinnedNote: "Alpinglita noto"
 pinned: "Alpingli"
@@ -101,7 +102,7 @@ sensitive: "Enhavo ne estas deca por laborejo (NSFW)"
 add: "Aldoni"
 reaction: "Reagoj"
 reactionSettingDescription: "Agordi la reagojn kiujn vi volas prefere montrigi ĉe la elektilo de reagoj"
-rememberNoteVisibility: "Rememori la agordon de videbleco de la noto laste sendita "
+rememberNoteVisibility: "Rememori la agordon de videbleco de la laste sendita"
 attachCancel: "Deigi aldonaĵon"
 markAsSensitive: "Troviĝi NSFW"
 unmarkAsSensitive: "Ne troviĝi NSFW"
@@ -121,16 +122,16 @@ selectAntenna: "Elekti antenon"
 selectWidget: "Elekti enestraĵon"
 editWidgets: "Redakti fenestraĵon"
 editWidgetsExit: "Fini la redaktadon"
-customEmojis: "Personecigitaj emoĵioj"
 emoji: "Emoĵio"
 emojis: "Emoĵio"
-emojiName: "Nomo de emoĵio"
+emojiName: "Nomo de la emoĵio"
 emojiUrl: "URL de la emoĵio"
 addEmoji: "Aldoni emoĵion"
 settingGuide: "Agordaj rekomendoj"
-cacheRemoteFiles: "Stapli transajn dosierojn"
-flagAsBot: "Agordo por robota uzanto"
-flagAsCat: "Agi kat-iĝon"
+cacheRemoteFiles: "Stapli forajn dosierojn"
+flagAsBot: "Fari la flagon por robota uzanto"
+flagAsCat: "Fari la flagon por kat-iĝi"
+autoAcceptFollowed: "Aŭtomate akcepti la peton de sekvado far uzantoj kiujn vi sekvas"
 addAccount: "Aldoni konton"
 showOnRemote: "Vidi ĉe la surloka nodo"
 general: "Ĝenerala"
@@ -140,7 +141,7 @@ removeWallpaper: "Forviŝi ekranfonon. "
 searchWith: "Serĉi: {q}"
 youHaveNoLists: "Vi ne havas listojn."
 followConfirm: "Ĉu vi certas ke vi volas sekvi {name}'(o)n?"
-host: "Gastigo"
+host: "Nodo"
 selectUser: "Elekti uzanton"
 recipient: "Ricevonto"
 annotation: "Komentarioj"
@@ -164,8 +165,9 @@ disk: "Disko"
 instanceInfo: "Informoj pri la nodo"
 statistics: "Statistikoj"
 clearCachedFiles: "Malplenigi la staplon"
-clearCachedFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn transajn dosierojn en la staplo?"
+clearCachedFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn forajn dosierojn en la staplo?"
 blockedInstances: "Blokitaj nodoj"
+muteAndBlock: "Silentigi / Bloki"
 mutedUsers: "Silentigitaj uzantoj"
 blockedUsers: "Blokitaj uzantoj"
 noUsers: "Neniu uzanto"
@@ -175,7 +177,7 @@ pinLimitExceeded: "Vi ne povas alpingli pli"
 done: "Fini"
 processing: "Prilaborado…"
 preview: "Antaŭmontro"
-default: "Defaŭlta"
+default: "Implicitaĵo"
 noCustomEmojis: "Neniu emoĵio"
 noJobs: "Neniu laboro"
 federating: "Federantaj"
@@ -195,7 +197,7 @@ currentPassword: "Aktuala pasvorto"
 newPassword: "Nova pasvorto"
 newPasswordRetype: "Reentajpu la novan pasvorton"
 attachFile: "Aldoni dosieron"
-more: "Plu!"
+more: "Pli!"
 featured: "Maksimumi"
 usernameOrUserId: "Uzantnomo aŭ identigilo de uzanto"
 noSuchUser: "Neniuj uzantoj trovitaj"
@@ -204,8 +206,8 @@ announcements: "Novaĵoj"
 imageUrl: "URL de la bildo"
 remove: "Forigi"
 removed: "Forigita"
-removeAreYouSure: "Ĉu vi certas ke vi volas forigi \"{x}\"'(o)n?"
-deleteAreYouSure: "Ĉu vi certas ke vi volas forviŝi \"{x}\"'(o)n?"
+removeAreYouSure: "Ĉu vi certas ke vi volas forigi \"{x}\"n?"
+deleteAreYouSure: "Ĉu vi certas ke vi volas forviŝi \"{x}\"'?"
 resetAreYouSure: "Ĉu vi certas restarigi?"
 saved: "Konservita"
 messaging: "Retbabili"
@@ -225,13 +227,13 @@ agreeTo: "Mi akceptas {0}'(o)n"
 tos: "Kondiĉoj de uzado"
 start: "Komenciĝi"
 home: "Hejma"
-remoteUserCaution: "Ĉi tiuj infomoj estas ne tute ekzaktaj pro transa uzanto."
+remoteUserCaution: "Ĉi tiuj infomoj de la uzanto el fora nodo, ne estas tute ekzaktaj."
 activity: "Aktiveco"
 images: "Bildoj"
 birthday: "Naskiĝdato"
 yearsOld: "{age} jaroj aĝa"
 registeredDate: "Dato de registriĝo"
-location: "Loko"
+location: "Kie"
 theme: "Koloraro"
 themeForLightMode: "Luma kolararo en la luma modo"
 themeForDarkMode: "Malluma kolararo en la malluma modo"
@@ -253,7 +255,7 @@ deleteFolder: "Forviŝi dosierujon"
 addFile: "Aldoni dosieron"
 emptyDrive: "La disko malplenas"
 emptyFolder: "La dosierujo malplenas"
-unableToDelete: "Ne forigebla"
+unableToDelete: "Ne forviŝebla"
 inputNewFileName: "Entajpu novan nomon de la dosiero"
 inputNewDescription: "Entajpu novan priskribon"
 inputNewFolderName: "Entajpu novan nomon de la dosierujo"
@@ -266,9 +268,11 @@ nsfw: "Enhavo ne estas deca por laborejo (NSFW)"
 disconnectedFromServer: "Malkonektita de servilo"
 reload: "Reŝargi"
 doNothing: "Ignori"
+reloadConfirm: "Ĉu vi volas reŝargi?"
 watch: "Observi"
 unwatch: "Malobservi"
 accept: "Permesi"
+reject: "Malakcepti"
 normal: "Normala"
 instanceName: "Nomo de la nodo"
 instanceDescription: "Priskribo de la nodo "
@@ -291,20 +295,22 @@ registration: "Registri"
 enableRegistration: "Ebligi novan uzanton registriĝon"
 invite: "Inviti"
 driveCapacityPerLocalAccount: "Volumo de disko po unu loka uzanto"
-driveCapacityPerRemoteAccount: "Volumo de disko po unu transa uzanto"
+driveCapacityPerRemoteAccount: "Volumo de disko po unu fora uzanto"
 iconUrl: "URL de la ikono (retpaĝsimbolo, ktp)"
 bannerUrl: "URL de standardo"
 backgroundImageUrl: "URL de fona bildo"
 basicInfo: "Baza informo"
 pinnedUsers: "Alpinglita uzanto"
+pinnedUsersDescription: "Listigu uzantnomojn apartige en ĉiu linio por alpingli al la paĝoj ekz \"Esplori\"."
 pinnedPages: "Alpinglitaj paĝoj"
+pinnedPagesDescription: "Listigu dosierindiko apartige en ĉiu linio por alpingli al la ĉefpaĝo de la nodo."
 pinnedNotes: "Alpinglita noto"
 hcaptcha: "hCaptcha"
 enableHcaptcha: "Ebligi hCaptcha"
 hcaptchaSiteKey: "Reteja ŝlosilo"
 hcaptchaSecretKey: "Sekreta ŝlosilo"
 recaptcha: "reCAPTCHA"
-enableRecaptcha: "Ebligi reCAPTCHA'on"
+enableRecaptcha: "Ebligi reCAPTCHA"
 recaptchaSiteKey: "Reteja ŝlosilo"
 recaptchaSecretKey: "Sekreta ŝlosilo"
 antennas: "Antenoj"
@@ -338,15 +344,17 @@ moderator: "Kontrolisto"
 nUsersMentioned: "{n} uzanto(j) menciis"
 securityKey: "Sekureca ŝlosilo"
 securityKeyName: "Nomo de la ŝlosilo"
+registerSecurityKey: "Registri ŝlosilon de sekureco"
 lastUsed: "Plej malnove uzita"
 unregister: "Malregistriĝi"
 passwordLessLogin: "Ensaluti sen pasvorto"
 resetPassword: "Restarigi pasvorton"
 newPasswordIs: "La nova pasvorto estas {password}."
-share: "Diskonigi"
+reduceUiAnimation: "Redukti la animacioj de la fasado"
+share: "Kundividi"
 notFound: "Ne trovita"
 cacheClear: "Malplenigi staplon"
-markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legito"
+markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legita"
 help: "Manlibro de uzado"
 inputMessageHere: "Entajpu masaĝo tie ĉi"
 close: "Fermi"
@@ -354,10 +362,11 @@ group: "Grupo"
 groups: "Grupoj"
 createGroup: "Krei grupon"
 ownedGroups: "Administrataj grupoj"
-joinedGroups: "La grupoj kiujn la uzanto aliĝis"
+joinedGroups: "Al grupoj kiuj vi aliĝis"
 invites: "Inviti"
 groupName: "Grupa nomo"
 members: "Membroj"
+transfer: "Movi"
 messagingWithUser: "Babili private"
 messagingWithGroup: "Babili grupe"
 title: "Titolo"
@@ -366,6 +375,7 @@ enable: "Ebligi"
 next: "Sekve"
 retype: "Retajpu"
 noteOf: "Noto de {user}"
+inviteToGroup: "Inviti al grupo"
 quoteAttached: "Kun citaĵo"
 quoteQuestion: "Ĉu vi aldonas citaĵon?"
 noMessagesYet: "Ankoraŭ neniu mesaĝo"
@@ -374,27 +384,38 @@ onlyOneFileCanBeAttached: "Oni povas aldoni nur unu dosieron po mesaĝo."
 signinRequired: "Bonvolu ensaluti"
 invitations: "Inviti"
 invitationCode: "Invita kodo"
+available: "Disposabla"
 unavailable: "Ne disponebla"
+usernameInvalidFormat: "La uzantnomo povas enhavi minusklajn kaj majusklajn literojn, numerojn, nur kaj '_'."
+tooShort: "Tro mallonga"
+tooLong: "Tro longa"
+weakPassword: "Malforta pasvorto"
+normalPassword: "Normala pasvorto"
+strongPassword: "Forta pasvorto"
 passwordMatched: "Konforma"
 passwordNotMatched: "Nekonforma"
+signinWith: "Ensaluti kun {x}"
 or: "Aŭ"
 language: "Lingvo"
 uiLanguage: "Lingvo de fasado"
 aboutX: "Pri {x}"
-useOsNativeEmojis: "Oni uzas la emoĵioj de la denaska sistemo"
+useOsNativeEmojis: "Uzi la emoĵiojn implicitan de la operaciumo"
 youHaveNoGroups: "Neniuj grupoj"
+noHistory: "Neniom historio"
+signinHistory: "Historio de aliroj al la konto"
 doing: "Traktado..."
 category: "Kategorio"
 tags: "Etikedoj"
+docSource: "Fonto de la dokumento"
 createAccount: "Krei konton"
 existingAccount: "Ekzista konto"
 regenerate: "Regeneri"
 fontSize: "Tipara grando"
 noFollowRequests: "Vi ne havas peto de sekvado"
-openImageInNewTab: "Fermi la bildon en nova tablo"
+openImageInNewTab: "Malfermi la bildojn en nova tablo"
 dashboard: "Stirpanelo"
 local: "Loka"
-remote: "Transa"
+remote: "Fora"
 total: "Entute"
 appearance: "Eksteraĵo"
 clientSettings: "Agordoj de kliento"
@@ -402,6 +423,7 @@ accountSettings: "Agordoj de konto"
 numberOfDays: "Nombro de tagoj"
 hideThisNote: "Kaŝi la noton"
 objectStorageBaseUrl: "Baza URL"
+objectStoragePrefix: "Prefix"
 objectStorageRegion: "Regiono"
 objectStorageUseSSL: "Oni uzas SSL"
 serverLogs: "Servila protokolo"
@@ -416,7 +438,7 @@ volume: "Laŭteco"
 masterVolume: "Baza laŭteco"
 details: "Detaloj"
 chooseEmoji: "Elekti emoĵion"
-recentUsed: "Lastatempaj uzitaj"
+recentUsed: "Lastatempe uzitaj"
 install: "Instali"
 uninstall: "Malinstali"
 installedApps: "Instalita programo"
@@ -425,10 +447,12 @@ installedDate: "Dato de instalado"
 lastUsedDate: "Lastfoje uzita je"
 state: "Stato"
 sort: "Ordigado"
+ascendingOrder: "Kreski"
+descendingOrder: "Malkreski"
 scratchpad: "Malneta redaktilo"
 output: "Elmeto"
 script: "Skripto"
-disablePagesScript: "Malebligi AiScripto en la paĝoj"
+disablePagesScript: "Malebligi AiScript en la paĝoj"
 deleteAllFiles: "Forviŝi ĉiujn dosierojn"
 deleteAllFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn dosierojn?"
 removeAllFollowing: "Ĉesi sekvi ĉiujn sekvatojn"
@@ -438,7 +462,8 @@ menu: "Menuo"
 addItem: "Aldoni novaĵon"
 rooms: "Ĉambro"
 deletedNote: "Forviŝita noto"
-invisibleNote: "Malpublika noto"
+invisibleNote: "Malpublikigita noto"
+enableInfiniteScroll: "Ebligi infinitan rulumon"
 visibility: "Videbleco"
 poll: "Balotujo"
 useCw: "Kaŝi enhavo"
@@ -453,16 +478,23 @@ author: "Aŭtoro"
 manage: "Administro"
 plugins: "Kromaĵoj"
 deck: "Kartaro"
+useFullReactionPicker: "Uzi la tuton de la elektilon de reagoj"
 width: "Larĝeco"
 height: "Alteco"
+large: "Granda"
 medium: "Meza"
 small: "Malgranda"
+generateAccessToken: "Generi ĵetonon de aliro"
+permission: "Permesoj"
+enableAll: "Ebligi ĉiujn"
+disableAll: "Malebligi ĉiujn"
+notificationType: "Tipo de sciigoj"
 edit: "Redakti"
 emailServer: "Retpoŝta servilo"
 email: "Retpoŝto"
 emailAddress: "Retpoŝta adreso"
 smtpConfig: "Agordoj de SMTP servilo"
-smtpHost: "Gastigo"
+smtpHost: "Transa servilo"
 smtpPort: "Pordo"
 smtpUser: "Uzantnomo"
 smtpPass: "Pasvorto"
@@ -471,13 +503,20 @@ userSaysSomething: "{name} parolis ion"
 makeActive: "Aktivigi"
 display: "Vidi"
 copy: "Kopii"
+metrics: "mezurciferoj"
 overview: "Resumo"
+logs: "Protokoloj"
+delayed: "Prokrasto "
 database: "Datumbazo"
 channel: "Kanalo"
 create: "Krei"
 notificationSetting: "Agordoj de sciigoj"
 useGlobalSetting: "Oni uzas malloka agordo"
+other: "Aliaj"
+regenerateLoginToken: "Regeneri la ĵetonon de aliro"
 fileIdOrUrl: "Dosiera identigilo aŭ URL"
+chatOpenBehavior: "Konduto por malfermi la fenestron de babilejo"
+behavior: "Konduto"
 sample: "Ekzemplo"
 abuseReports: "Signaloj"
 reportAbuse: "Signalo"
@@ -485,20 +524,21 @@ reportAbuseOf: "Signali kontraŭ {name}'(o)"
 send: "Sendi"
 openInNewTab: "Malfermi en nova langeto"
 editTheseSettingsMayBreakAccount: "Redakti ĉi tiujn agordojn povas damaĝi vian konton."
-instanceTicker: "Informoj pri la nodo kiu dissendas la noton"
+instanceTicker: "Nomo de la nodo sendinta notojn"
+waitingFor: "Atendado pro {x}"
 random: "Hazarde"
 system: "Sistemo"
 desktop: "Labortablo"
 createNew: "Krei novan"
 optional: "Opciaj"
 public: "Publika"
-i18nInfo: "Misskey estas tradukata en diversaj lingvoj far volontuloj. Oni povas kontribui por la tradukado ĉe {link}."
+i18nInfo: "Misskey estas tradukata en diversaj lingvoj de volontuloj. Oni povas kontribui ĉe {link}."
 accountInfo: "Kontaj Informoj"
 notesCount: "La nombro de notoj"
 repliesCount: "La nombro de respondoj senditaj"
-renotesCount: "La nombro de notoj kiujn la uzanto plusendis"
+renotesCount: "La nombro de notoj plusenditaj de la uzanto"
 repliedCount: "La nombro de respondoj ricevitaj"
-renotedCount: "La nombro de uzantulaj notoj plusenditaj"
+renotedCount: "La nombro de plusendoj de la notoj skribitaj de la uzanto"
 followingCount: "La nombro de sekvatoj"
 followersCount: "La nombro de sekvantoj"
 sentReactionsCount: "La nombro de la reagoj senditaj"
@@ -512,10 +552,15 @@ noteFavoritesCount: "La nombro de notoj preferataj"
 pageLikesCount: "La nombro de paĝoj kiun la uzanto preferas"
 pageLikedCount: "La nombro de uzantoj, kiuj preferas paĝon de ĉi tiu uzanto"
 contact: "Kontakto"
+useSystemFont: "Uzi la tiparon implicitan de la sistemo"
+developer: "Evoluiganto"
 makeExplorable: "Videbligi konton sur la paĝo \"Esplori\""
+makeExplorableDescription: "Se vi elŝaltas tiun, via konto ne montros en la paĝo \"Esplori\"."
 duplicate: "Duobligi"
 left: "Maldekstra"
 center: "Centra"
+wide: "Vasta"
+narrow: "Malvasta"
 showTitlebar: "Videbligi titolan stangon"
 clearCache: "Malplenigi staplon"
 onlineUsersCount: "{n} uzanto(j) estas surlinea"
@@ -525,9 +570,11 @@ myTheme: "Miaj koloraroj"
 backgroundColor: "Fona koloro"
 textColor: "Teksto"
 saveAs: "Konservi kiel…"
+advanced: "Altnivela"
 value: "Valoro"
 createdAt: "Kreita je"
 updatedAt: "Laste ĝisdatigita"
+saveConfirm: "Ĉu vi konservas la ŝanĝon?"
 deleteConfirm: "Ĉu certas forviŝi?"
 closeAccount: "Forigi konton"
 currentVersion: "Nuna versio"
@@ -538,9 +585,10 @@ inUse: "Uzata"
 editCode: "Redakti kodon"
 emailNotification: "Sciigoj per retpoŝto"
 inChannelSearch: "Serĉi en kanalo"
-useReactionPickerForContextMenu: "Malfermi reago-elektilon per dekstro-klaki"
+useReactionPickerForContextMenu: "Dekstre-klaki por malfermi la elektilon de reagoj"
 typingUsers: "{users} nun skribas…"
 clear: "Vakigi"
+markAllAsRead: "Marki ĉiujn kiel legito"
 goBack: "Reiri antaŭ"
 addDescription: "Priskribi"
 info: "Informoj"
@@ -559,7 +607,7 @@ memo: "Memorigilo"
 high: "Alta"
 middle: "Meza"
 low: "Malalta"
-customCss: "Uzantula CSS"
+customCss: "Personecigita CSS"
 global: "Malloka"
 sent: "Sendi"
 received: "Ricevita"
@@ -569,10 +617,27 @@ troubleshooting: "Problemsolvi"
 learnMore: "Lernu pli"
 translate: "Traduki"
 translatedFrom: "Tradukita el {x}"
+itsOn: "Ŝaltita"
+unread: "Nelegita"
 controlPanel: "Ŝaltpodio"
 classic: "Klasika"
+ffVisibility: "Videbleco pri viaj sekvataro/sekvantaro\n"
+ffVisibilityDescription: "Agordi la videblecon kiu povas vidi tiujn kiujn vi sekvas kaj tiujn kiuj sekvas vin."
+continueThread: "Vidi pli mesaĝarojn"
+incorrectPassword: "Nevalida pasvorto"
+_emailUnavailable:
+  used: "La retpoŝto jam estas uzita."
+  format: "Nevalida formato."
+  disposable: "Dumtempa retpoŝto ne estas uzebla."
+  smtp: "Tiu retpoŝta servilo ne respondas"
+_ffVisibility:
+  public: "Publika"
+  followers: "Afiŝi nur al sekvantoj"
+  private: "Malpublikigita"
 _signup:
   emailAddressInfo: "Entajpu vian retpoŝton"
+_accountDelete:
+  accountDelete: "Forigi konton"
 _ad:
   back: "Nuligi"
 _forgotPassword:
@@ -598,7 +663,7 @@ _aboutMisskey:
   contributors: "Precipaj kontribuantoj"
   allContributors: "Ĉiuj kontribuantoj"
   source: "Fontkodo"
-  translation: "Traduki Misskey'on"
+  translation: "Traduki Misskey"
   patrons: "Mecenatoj"
 _mfm:
   dummy: "Misskey evoluigas la mondon de Fediverso"
@@ -614,19 +679,21 @@ _mfm:
   inlineMath: "Formulo (en linio)"
   blockMath: "Formulo (bloko)"
   quote: "Citi"
-  emoji: "Personecigitaj emoĵioj"
   search: "Serĉi"
   flip: "Inversa"
   x2: "Granda"
   x3: "Grandega"
   x4: "Pli grandega"
   font: "Presliteraro"
+  rotate: "Orientiĝo"
 _reversi:
   total: "Entute"
 _instanceTicker:
   none: "Ne montri"
-  remote: "Montri al transaj uzantoj"
+  remote: "Montri al foraj uzantoj"
   always: "Ĉiam montri"
+_serverDisconnectedBehavior:
+  reload: "Aŭtomate reŝargi"
 _channel:
   create: "Krei kanalon"
   edit: "Redakti kanalon"
@@ -640,13 +707,14 @@ _menuDisplay:
   hide: "Kaŝi"
 _wordMute:
   muteWords: "Silentigitaj vortoj"
-  soft: "En kliento"
-  hard: "En servilo"
+  soft: "Per la kliento"
+  hard: "Per la servilo"
   mutedNotes: "Silentigitaj notoj"
 _theme:
   manage: "Administri kolorarojn"
   code: "Kolorara kodo"
   description: "Priskribo"
+  defaultValue: "Implicitaĵa valoro"
   color: "Koloro"
   darken: "Malbrileco"
   lighten: "Brileco"
@@ -657,7 +725,7 @@ _theme:
     hashtag: "Kradvorto"
     mention: "Mencioj"
     mentionMe: "Mencio al vi"
-    renote: "Noto plusendita"
+    renote: "Plusendita"
     buttonBg: "Fono de butono"
     driveFolderBg: "Fono de dosierujo de la disko"
     messageBg: "Fono de retbabilejo"
@@ -688,7 +756,7 @@ _tutorial:
   title: "Uzado de Misskey"
   step1_1: "Bonvenon."
   step7_2: "Se vi volas scii pli pri Misskey, rigardu la fakon {help}."
-  step7_3: "Do, bonvolu amuziĝi Misskey'on🚀"
+  step7_3: "Do, bonvolu amuziĝi sur Misskey🚀"
 _2fa:
   registerKey: "Nove registri ŝlosilon"
 _permissions:
@@ -732,10 +800,10 @@ _widgets:
   federation: "Federaĵo"
   slideshow: "Bildoprezento"
   button: "Butono"
-  onlineUsers: "Surkonektita uzanto"
+  onlineUsers: "Surkonektitaj uzantoj"
   aichan: "Ai"
 _cw:
-  show: "Vidu pli"
+  show: "Vidi pli"
   files: "{count} dosiero(j)"
 _poll:
   choiceN: "Balotilo {n}"
@@ -747,15 +815,15 @@ _poll:
   closed: "Oni jam balotis ĝin"
 _visibility:
   public: "Publika"
-  publicDescription: "Via noto estos videbla de ĉiuj uzantoj"
+  publicDescription: "Afiŝi al ĉiuj en la Fediverso"
   home: "Hejma"
   homeDescription: "Dissendi nur sur hejma templinio"
   followers: "Nur al sekvantoj"
-  followersDescription: "Publiki nur al viaj sekvantoj"
+  followersDescription: "Afiŝi nur al sekvantoj"
   specified: "Rekte"
-  specifiedDescription: "Montri nur al specifaj uzantoj"
+  specifiedDescription: "Afiŝi nur al specifaj uzantoj"
   localOnly: "Nur loka"
-  localOnlyDescription: "Ne montri al transaj uzantoj"
+  localOnlyDescription: "Ne afiŝi al foraj uzantoj"
 _postForm:
   replyPlaceholder: "Respondi la noton…"
   quotePlaceholder: "Citi la noton…"
@@ -789,7 +857,7 @@ _rooms:
   translate: "Movi"
   chooseImage: "Elekti bildon"
   _roomType:
-    default: "Defaŭlta"
+    default: "Implicitaĵo"
   _furnitures:
     bed: "Lito"
     low-table: "Malaltotablo"
@@ -835,18 +903,22 @@ _pages:
     textInput: "Enmeto el teksto"
     _textInput:
       text: "Titolo"
+      default: "Implicitaĵa valoro"
     textareaInput: "Enmeto el teksto en multaj linioj"
     _textareaInput:
       text: "Titolo"
+      default: "Implicitaĵa valoro"
     numberInput: "Nombra enmeto"
     _numberInput:
       text: "Titolo"
+      default: "Implicitaĵa valoro"
     _canvas:
       id: "Kanvasa identigilo"
     _note:
       id: "Identigilo de noto"
     _switch:
       text: "Titolo"
+      default: "Implicitaĵa valoro"
     _counter:
       text: "Titolo"
     _button:
@@ -856,6 +928,7 @@ _pages:
           event: "Nomo de la evento"
     _radioButton:
       title: "Titolo"
+      default: "Implicitaĵa valoro"
   script:
     categories:
       text: "Manipulo de teksto"
@@ -874,6 +947,7 @@ _pages:
         arg1: "Teksto"
       _join:
         arg1: "Listoj"
+        arg2: "apartigilo"
       _randomPick:
         arg1: "Listoj"
       _dailyRandomPick:
@@ -904,6 +978,7 @@ _pages:
 _relayStatus:
   requesting: "Atendado de aprobon"
   accepted: "Konfirmita"
+  rejected: "Malakceptita"
 _notification:
   fileUploaded: "La dosiero sukcese alŝutiĝis."
   youGotMention: "{name} mencis"
@@ -918,13 +993,13 @@ _notification:
   yourFollowRequestAccepted: "Via peto de sekvado estis akceptita."
   _types:
     all: "Ĉio"
-    follow: "Nova sekvatoj"
+    follow: "Novaj sekvatoj"
     mention: "Mencioj"
     reply: "Respondoj"
-    renote: "Notoj plusenditaj"
+    renote: "Plusendoj"
     quote: "Citi"
     reaction: "Reagoj"
-    receiveFollowRequest: "Ricevita peton de sekvado"
+    receiveFollowRequest: "Ricevi peton de sekvado"
     followRequestAccepted: "Akceptita peto por sekvado"
 _deck:
   profile: "Agordaro"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 3421c64389..f81c3772aa 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -737,6 +737,7 @@ pubSub: "Cuentas Pub/Sub"
 lastCommunication: "Última comunicación"
 resolved: "Resuelto"
 unresolved: "Sin resolver"
+controlPanel: "Panel de control"
 _accountDelete:
   accountDelete: "Eliminar Cuenta"
 _ad:
@@ -767,6 +768,7 @@ _mfm:
   flip: "Echar de un capirotazo"
   flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda / derecha."
   font: "Fuente"
+  rotate: "Rotar"
 _reversi:
   reversi: "Reversi"
   gameSettings: "Configuración del juego"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 5d67b5269a..cf5e2238b1 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -919,6 +919,7 @@ _mfm:
   rainbowDescription: "Permet d'afficher le contenu en couleurs arc-en-ciel."
   sparkle: "Paillettes"
   sparkleDescription: "Ajoute un effet scintillant au contenu."
+  rotate: "Pivoter"
 _reversi:
   reversi: "Reversi"
   gameSettings: "Réglages de la partie"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index f4997e3a64..d9e6368c3a 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -806,6 +806,10 @@ muteThread: "Bisukan thread"
 unmuteThread: "Suarakan thread"
 ffVisibility: "Visibilitas Mengikuti/Pengikut"
 ffVisibilityDescription: "Mengatur siapa yang dapat melihat pengikutmu dan yang kamu ikuti."
+continueThread: "Lihat lanjutan thread"
+deleteAccountConfirm: "Akun akan dihapus. Apakah kamu yakin?"
+incorrectPassword: "Kata sandi salah."
+voteConfirm: "Konfirmasi suara kamu untuk ({choice})?"
 _emailUnavailable:
   used: "Alamat surel ini telah digunakan"
   format: "Format tidak valid."
@@ -929,6 +933,8 @@ _mfm:
   rainbowDescription: "Membuat konten muncul dalam warna pelangi."
   sparkle: "Kelap-kelip"
   sparkleDescription: "Memberikan konten efek partikel kelap-kelip."
+  rotate: "Putar"
+  rotateDescription: "Putar konten sesuai sudut yang ditentukan."
 _reversi:
   reversi: "Reversi"
   gameSettings: "Pengaturan permainan"
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index fc032e068c..d650f44357 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -806,6 +806,7 @@ _mfm:
   font: "Tipo di carattere"
   fontDescription: "Puoi scegliere il tipo di carattere per il contenuto."
   rainbow: "Arcobaleno"
+  rotate: "Ruota"
 _reversi:
   reversi: "Reversi"
   gameSettings: "Impostazioni di gioco"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index b73be17035..49ef286a59 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -700,6 +700,7 @@ _mfm:
   spin: "アニメーション(回転)"
   blur: "ぼかし"
   font: "フォント"
+  rotate: "回転"
 _reversi:
   reversi: "リバーシ"
   gameSettings: "対局の設定"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 10a9b6e3e1..dde60c4c81 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -899,6 +899,7 @@ _mfm:
   rainbowDescription: "내용을 무지개로 표시합니다."
   sparkle: "반짝반짝"
   sparkleDescription: "반짝이는 파티클 효과를 추가합니다."
+  rotate: "회전"
 _reversi:
   reversi: "리버시"
   gameSettings: "대국 설정"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
index 7a3c568f6f..0393d94303 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -1,5 +1,193 @@
 ---
 _lang_: "Nederlands"
 headlineMisskey: "Netwerk verbonden door notities"
+introMisskey: "Welkom! Misskey is een open source, gedecentraliseerde microblogdienst.\nMaak \"notities\" om je gedachten te delen met iedereen om je heen. 📡\nMet \"reacties\" kun je ook snel je mening geven over berichten van anderen. 👍\nLaten we een nieuwe wereld verkennen! 🚀"
+monthAndDay: "{day} {month}"
+search: "Zoeken"
+notifications: "Meldingen"
+username: "Gebruikersnaam"
+password: "Wachtwoord"
+forgotPassword: "Wachtwoord vergeten"
+fetchingAsApObject: "Ophalen vanuit de Fediverse"
+ok: "Ok"
+gotIt: "Begrepen"
+cancel: "Annuleren"
+enterUsername: "Voer een gebruikersnaam in"
+renotedBy: "Hergedeeld door {user}"
+noNotes: "Geen notities"
+noNotifications: "Geen meldingen"
+instance: "Server"
+settings: "Instellingen"
+basicSettings: "Basisinstellingen"
+otherSettings: "Overige instellingen"
+openInWindow: "In een venster openen"
+profile: "Profiel"
+timeline: "Tijdlijn"
+noAccountDescription: "Deze gebruiker heeft nog geen bio geschreven"
+login: "Inloggen"
+loggingIn: "Aan het inloggen"
+logout: "Afmelden"
+signup: "Registreren"
+uploading: "Bezig met uploaden"
+save: "Opslaan"
+users: "Gebruikers"
+addUser: "Toevoegen gebruiker"
+favorite: "Favorieten"
+favorites: "Toevoegen aan favorieten"
+unfavorite: "Verwijderen uit favorieten"
+favorited: "Toegevoegd aan favorieten."
+alreadyFavorited: "Al toegevoegd aan favorieten"
+cantFavorite: "Kon niet toevoegen aan favorieten"
+pin: "Vastmaken aan profielpagina"
+unpin: "Losmaken van profielpagina"
+copyContent: "Kopiëren inhoud"
+copyLink: "Kopiëren link"
+delete: "Verwijderen"
+deleteAndEdit: "Verwijderen en bewerken"
+deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop."
+addToList: "Aan lijst toevoegen"
+sendMessage: "Verstuur bericht"
+copyUsername: "Kopiëren gebruikersnaam "
+searchUser: "Zoeken een gebruiker"
+reply: "Antwoord"
+loadMore: "Laad meer"
+showMore: "Toon meer"
+youGotNewFollower: "volgde jou"
+receiveFollowRequest: "Volgverzoek ontvangen"
+followRequestAccepted: "Volgverzoek geaccepteerd"
+mention: "Vermelding"
+mentions: "Vermeldingen"
+directNotes: "Directe notities"
+importAndExport: "Import / export"
+import: "Import"
+export: "Export"
+files: "Bestanden"
+download: "Downloaden"
+driveFileDeleteConfirm: "Weet je zeker dat je het bestand \"{name}\" wilt verwijderen? Notities met dit bestand als bijlage worden ook verwijderd."
+unfollowConfirm: "Weet je zeker dat je {name} wilt ontvolgen?"
+exportRequested: "Je hebt een export aangevraagd. Dit kan een tijdje duren. Het wordt toegevoegd aan je Drive zodra het is voltooid."
+importRequested: "Je hebt een import aangevraagd. Dit kan even duren."
+lists: "Lijsten"
+noLists: "Je hebt geen lijsten"
+note: "Notitie"
+notes: "Notities"
+following: "Volgend"
+followers: "Volgers"
+followsYou: "Volgt jou"
+createList: "Creëer lijst"
+manageLists: "Beheren lijsten"
+error: "Fout"
+somethingHappened: "Er is iets misgegaan."
+retry: "Probeer opnieuw"
+pageLoadError: "Pagina laden mislukt"
+pageLoadErrorDescription: "Dit wordt normaal gesproken veroorzaakt door netwerkfouten of door de cache van de browser. Probeer de cache te wissen en probeer het na een tijdje wachten opnieuw."
+serverIsDead: "De server reageert niet. Wacht even en probeer het opnieuw."
+youShouldUpgradeClient: "Werk je client bij om deze pagina te zien."
+enterListName: "Voer de naam van de lijst in"
+privacy: "Privacy"
+makeFollowManuallyApprove: "Volgverzoeken vergen een goedkeuring"
+defaultNoteVisibility: "Standaard zichtbaarheid"
+follow: "Volgen"
+followRequest: "Verzoek om te mogen volgen"
+followRequests: "Volgverzoeken"
+unfollow: "Ontvolgen"
+followRequestPending: "Wachten op goedkeuring volgverzoek"
+enterEmoji: "Voer een emoji in"
+renote: "Herdelen"
+unrenote: "Stop herdelen"
+renoted: "Herdeeld"
+cantRenote: "Dit bericht kan niet worden herdeeld"
+cantReRenote: "Een herdeling kan niet worden herdeeld"
+quote: "Quote"
+pinnedNote: "Vastgemaakte notitie"
+pinned: "Vastmaken aan profielpagina"
+you: "Jij"
+clickToShow: "Klik om te bekijken"
+sensitive: "NSFW"
+add: "Toevoegen"
+reaction: "Reacties"
+reactionSettingDescription: "Configureer welke reacties je wilt weergeven in de reactiekiezer."
+reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen"
+rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen"
+attachCancel: "Verwijder bijlage"
+markAsSensitive: "Markeren als NSFW"
+unmarkAsSensitive: "Geen NSFW"
+enterFileName: "Invoeren bestandsnaam"
+mute: "Dempen"
+unmute: "Stop dempen"
+block: "Blokkeren"
+unblock: "Deblokkeren"
+suspend: "Opschorten"
+unsuspend: "Heractiveren"
+blockConfirm: "Weet je zeker dat je dit account wil blokkeren?"
+instances: "Server"
+remove: "Verwijderen"
+nsfw: "NSFW"
+pinnedNotes: "Vastgemaakte notitie"
+userList: "Lijsten"
+smtpUser: "Gebruikersnaam"
+smtpPass: "Wachtwoord"
+user: "Gebruikers"
 muteThread: "Discussies dempen "
 unmuteThread: "Dempen van discussie ongedaan maken"
+_email:
+  _follow:
+    title: "volgde jou"
+_mfm:
+  mention: "Vermelding"
+  quote: "Quote"
+  search: "Zoeken"
+_theme:
+  keys:
+    mention: "Vermelding"
+    renote: "Herdelen"
+_sfx:
+  note: "Notities"
+  notification: "Meldingen"
+_widgets:
+  notifications: "Meldingen"
+  timeline: "Tijdlijn"
+_cw:
+  show: "Laad meer"
+_visibility:
+  followers: "Volgers"
+_profile:
+  username: "Gebruikersnaam"
+_exportOrImport:
+  followingList: "Volgend"
+  muteList: "Dempen"
+  blockingList: "Blokkeren"
+  userLists: "Lijsten"
+_pages:
+  script:
+    categories:
+      list: "Lijsten"
+    blocks:
+      _join:
+        arg1: "Lijsten"
+      _randomPick:
+        arg1: "Lijsten"
+      _dailyRandomPick:
+        arg1: "Lijsten"
+      _seedRandomPick:
+        arg2: "Lijsten"
+      _pick:
+        arg1: "Lijsten"
+      _listLen:
+        arg1: "Lijsten"
+    types:
+      array: "Lijsten"
+_notification:
+  youWereFollowed: "volgde jou"
+  _types:
+    follow: "Volgend"
+    mention: "Vermelding"
+    renote: "Herdelen"
+    quote: "Quote"
+    reaction: "Reacties"
+_deck:
+  _columns:
+    notifications: "Meldingen"
+    tl: "Tijdlijn"
+    list: "Lijsten"
+    mentions: "Vermeldingen"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index 5e1fba8382..1a52f35235 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -815,6 +815,7 @@ _mfm:
   blur: "Rozmycie"
   font: "Czcionka"
   fontDescription: "Wybiera czcionkę do wyświetlania treści."
+  rotate: "Obróć"
 _reversi:
   reversi: "Reversi"
   gameSettings: "Ustawienia gry"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index c46f02f102..d095887bdc 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -1,22 +1,33 @@
 ---
 _lang_: "Português"
+headlineMisskey: "Rede conectada por notas"
 monthAndDay: "{day}/{month}"
 search: "Pesquisar"
 notifications: "Notificações"
 username: "Nome de usuário"
 password: "Senha"
+forgotPassword: "Esqueci a senha"
+fetchingAsApObject: "Buscando no Fediverso"
 ok: "OK"
 gotIt: "Entendi"
 cancel: "Cancelar"
 enterUsername: "Digite o nome de usuário"
 renotedBy: "Repostado por {user}"
 noNotes: "Sem posts"
+noNotifications: "Sem notificações"
+instance: "Instância"
 settings: "Configurações"
 basicSettings: "Configurações básicas"
 otherSettings: "Outras configurações"
+openInWindow: "Abrir numa janela"
 profile: "Perfil"
 timeline: "Timeline"
+login: "Iniciar sessão"
+loggingIn: "Iniciando sessão…"
 logout: "Sair"
+signup: "Registrar-se"
+uploading: "Enviando…"
+save: "Guardar"
 users: "Usuários"
 favorite: "Favoritar"
 favorites: "Favoritar"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 1eb6dff0ef..1d889866db 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -922,6 +922,7 @@ _mfm:
   rainbowDescription: "Заставлять содержимое отображаться в цветах радуги."
   sparkle: "Блеск"
   sparkleDescription: "Добавьте эффект искрящихся частиц."
+  rotate: "Повернуть"
 _reversi:
   reversi: "Реверси"
   gameSettings: "Настройки игры"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 73f43669af..9104b0839d 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -771,6 +771,7 @@ _mfm:
   blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші."
   font: "Шрифт"
   fontDescription: "Встановлює шрифт для контенту."
+  rotate: "Обертати"
 _reversi:
   reversi: "Реверсі"
   gameSettings: "Налаштування гри"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 604f1e74d8..3fdbbc63ac 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -808,6 +808,8 @@ ffVisibility: "连接的可见范围"
 ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围"
 continueThread: "查看更多帖子"
 deleteAccountConfirm: "将要删除账户。是否确认?"
+incorrectPassword: "密码错误"
+voteConfirm: "确定投给“{choice}” ?"
 _emailUnavailable:
   used: "已经被使用过"
   format: "无效的格式"
@@ -931,6 +933,8 @@ _mfm:
   rainbowDescription: "用彩虹色来显示内容。"
   sparkle: "闪光"
   sparkleDescription: "添加发光粒子效果。"
+  rotate: "旋转"
+  rotateDescription: "旋转指定的角度。"
 _reversi:
   reversi: "黑白棋"
   gameSettings: "对局设置"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 888e12490a..56eaaa0f0d 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -840,6 +840,7 @@ _mfm:
   blur: "模糊"
   font: "字型"
   fontDescription: "您可以設定顯示內容的字型"
+  rotate: "旋轉"
 _reversi:
   reversi: "黑白棋"
   gameSettings: "對弈設定"

From 705b46b3a0415c4c6a665afe65c3836a7b2bc8aa Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Dec 2021 14:18:49 +0900
Subject: [PATCH 18/29] Bump cypress from 9.0.0 to 9.1.0 (#8022)

Bumps [cypress](https://github.com/cypress-io/cypress) from 9.0.0 to 9.1.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/.releaserc.base.js)
- [Commits](https://github.com/cypress-io/cypress/compare/v9.0.0...v9.1.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json |  2 +-
 yarn.lock    | 12 ++++++------
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/package.json b/package.json
index 343e3f36cc..49da8fda93 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
 		"@types/fluent-ffmpeg": "2.1.17",
 		"@typescript-eslint/parser": "5.4.0",
 		"cross-env": "7.0.3",
-		"cypress": "9.0.0",
+		"cypress": "9.1.0",
 		"start-server-and-test": "1.14.0",
 		"typescript": "4.5.2"
 	}
diff --git a/yarn.lock b/yarn.lock
index fb93b79758..7f74114102 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -631,7 +631,7 @@ blob-util@^2.0.2:
   resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
   integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
 
-bluebird@3.7.2, bluebird@^3.7.2:
+bluebird@3.7.2:
   version "3.7.2"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@@ -1115,10 +1115,10 @@ csso@~2.3.1:
     clap "^1.0.9"
     source-map "^0.5.3"
 
-cypress@9.0.0:
-  version "9.0.0"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.0.0.tgz#8c496f7f350e611604cc2f77b663fb81d0c235d2"
-  integrity sha512-/93SWBZTw7BjFZ+I9S8SqkFYZx7VhedDjTtRBmXO0VzTeDbmxgK/snMJm/VFjrqk/caWbI+XY4Qr80myDMQvYg==
+cypress@9.1.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.1.0.tgz#5d23c1b363b7d4853009c74a422a083a8ad2601c"
+  integrity sha512-fyXcCN51vixkPrz/vO/Qy6WL3hKYJzCQFeWofOpGOFewVVXrGfmfSOGFntXpzWBXsIwPn3wzW0HOFw51jZajNQ==
   dependencies:
     "@cypress/request" "^2.88.7"
     "@cypress/xvfb" "^1.2.4"
@@ -1127,7 +1127,7 @@ cypress@9.0.0:
     "@types/sizzle" "^2.3.2"
     arch "^2.2.0"
     blob-util "^2.0.2"
-    bluebird "^3.7.2"
+    bluebird "3.7.2"
     cachedir "^2.3.0"
     chalk "^4.1.0"
     check-more-types "^2.24.0"

From 8de8de76696ca2627dd17d7da5ccd1067729eebc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 16:07:50 +0900
Subject: [PATCH 19/29] client: tweak ui

---
 .../client/src/components/global/spacer.vue   |   2 +-
 packages/client/src/components/poll.vue       | 137 ++++++++----------
 2 files changed, 64 insertions(+), 75 deletions(-)

diff --git a/packages/client/src/components/global/spacer.vue b/packages/client/src/components/global/spacer.vue
index 45481a2c8d..e2f1d1aec7 100644
--- a/packages/client/src/components/global/spacer.vue
+++ b/packages/client/src/components/global/spacer.vue
@@ -24,7 +24,7 @@ export default defineComponent({
 		marginMax: {
 			type: Number,
 			required: false,
-			default: 32,
+			default: 24,
 		},
 	},
 
diff --git a/packages/client/src/components/poll.vue b/packages/client/src/components/poll.vue
index 20a9900258..171b4a4770 100644
--- a/packages/client/src/components/poll.vue
+++ b/packages/client/src/components/poll.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="tivcixzd" :class="{ done: closed || isVoted }">
 	<ul>
-		<li v-for="(choice, i) in poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
+		<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
 			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
 			<span>
 				<template v-if="choice.isVoted"><i class="fas fa-check"></i></template>
@@ -13,7 +13,7 @@
 	<p v-if="!readOnly">
 		<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
 		<span> · </span>
-		<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a>
+		<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a>
 		<span v-if="isVoted">{{ $ts._poll.voted }}</span>
 		<span v-else-if="closed">{{ $ts._poll.closed }}</span>
 		<span v-if="remaining > 0"> · {{ timer }}</span>
@@ -22,9 +22,10 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent, onUnmounted, ref, toRef } from 'vue';
 import { sum } from '@/scripts/array';
 import * as os from '@/os';
+import { i18n } from '@/i18n';
 
 export default defineComponent({
 	props: {
@@ -38,71 +39,67 @@ export default defineComponent({
 			default: false,
 		}
 	},
-	data() {
-		return {
-			remaining: -1,
-			showResult: false,
-		};
-	},
-	computed: {
-		poll(): any {
-			return this.note.poll;
-		},
-		total(): number {
-			return sum(this.poll.choices.map(x => x.votes));
-		},
-		closed(): boolean {
-			return !this.remaining;
-		},
-		timer(): string {
-			return this.$t(
-				this.remaining >= 86400 ? '_poll.remainingDays' :
-				this.remaining >= 3600 ? '_poll.remainingHours' :
-				this.remaining >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
-					s: Math.floor(this.remaining % 60),
-					m: Math.floor(this.remaining / 60) % 60,
-					h: Math.floor(this.remaining / 3600) % 24,
-					d: Math.floor(this.remaining / 86400)
-				});
-		},
-		isVoted(): boolean {
-			return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
-		}
-	},
-	created() {
-		this.showResult = this.readOnly || this.isVoted;
 
-		if (this.note.poll.expiresAt) {
-			const update = () => {
-				if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
-					requestAnimationFrame(update);
-				else
-					this.showResult = true;
+	setup(props) {
+		const remaining = ref(-1);
+
+		const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
+		const closed = computed(() => remaining.value === 0);
+		const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
+		const timer = computed(() => i18n.t(
+			remaining.value >= 86400 ? '_poll.remainingDays' :
+			remaining.value >= 3600 ? '_poll.remainingHours' :
+			remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
+				s: Math.floor(remaining.value % 60),
+				m: Math.floor(remaining.value / 60) % 60,
+				h: Math.floor(remaining.value / 3600) % 24,
+				d: Math.floor(remaining.value / 86400)
+			}));
+
+		const showResult = ref(props.readOnly || isVoted.value);
+
+		// 期限付きアンケート
+		if (props.note.poll.expiresAt) {
+			const tick = () => {
+				remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000);
+				if (remaining.value === 0) {
+					showResult.value = true;
+				}
 			};
 
-			update();
+			tick();
+			const intevalId = window.setInterval(tick, 3000);
+			onUnmounted(() => {
+				window.clearInterval(intevalId);
+			});
 		}
-	},
-	methods: {
-		toggleShowResult() {
-			this.showResult = !this.showResult;
-		},
-		async vote(id) {
-			if (this.readOnly || this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
+
+		const vote = async (id) => {
+			if (props.readOnly || closed.value || isVoted.value) return;
 
 			const { canceled } = await os.confirm({
 				type: 'question',
-				text: this.$t('voteConfirm', { choice: this.poll.choices[id].text }),
+				text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
 			});
 			if (canceled) return;
 
 			await os.api('notes/polls/vote', {
-				noteId: this.note.id,
-				choice: id
+				noteId: props.note.id,
+				choice: id,
 			});
-			if (!this.showResult) this.showResult = !this.poll.multiple;
-		}
-	}
+			if (!showResult.value) showResult.value = !props.note.poll.multiple;
+		};
+
+		return {
+			remaining,
+			showResult,
+			total,
+			isVoted,
+			closed,
+			timer,
+			vote,
+		};
+	},
 });
 </script>
 
@@ -118,38 +115,38 @@ export default defineComponent({
 			display: block;
 			position: relative;
 			margin: 4px 0;
-			padding: 4px 8px;
-			border: solid 0.5px var(--divider);
+			padding: 4px;
+			//border: solid 0.5px var(--divider);
+			background: var(--accentedBg);
 			border-radius: 4px;
 			overflow: hidden;
 			cursor: pointer;
 
-			&:hover {
-				background: rgba(#000, 0.05);
-			}
-
-			&:active {
-				background: rgba(#000, 0.1);
-			}
-
 			> .backdrop {
 				position: absolute;
 				top: 0;
 				left: 0;
 				height: 100%;
 				background: var(--accent);
+				background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
 				transition: width 1s ease;
 			}
 
 			> span {
 				position: relative;
+				display: inline-block;
+				padding: 3px 5px;
+				background: var(--panel);
+				border-radius: 3px;
 
 				> i {
 					margin-right: 4px;
+					color: var(--accent);
 				}
 
 				> .votes {
 					margin-left: 4px;
+					opacity: 0.7;
 				}
 			}
 		}
@@ -166,14 +163,6 @@ export default defineComponent({
 	&.done {
 		> ul > li {
 			cursor: default;
-
-			&:hover {
-				background: transparent;
-			}
-
-			&:active {
-				background: transparent;
-			}
 		}
 	}
 }

From 8223a069fefe16c479a821aad8fc3063befe9487 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 17:47:44 +0900
Subject: [PATCH 20/29] fix(server): Fix #8032

---
 CHANGELOG.md                                     | 1 +
 packages/backend/src/models/repositories/user.ts | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2164ecdbdf..01c0adecfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@
 - クライアント: モバイルでタップしたときにツールチップが表示される問題を修正
 - クライアント: リモートインスタンスのノートに返信するとき、対象のノートにそのリモートインスタンス内のユーザーへのメンションが含まれていると、返信テキスト内にローカルユーザーへのメンションとして引き継がれてしまう場合がある問題を修正
 - クライアント: 画像ビューワーで全体表示した時に上側の一部しか表示されない画像がある問題を修正
+- API: ユーザーを取得時に条件によっては内部エラーになる問題を修正
 
 ### Changes
 - クライアント: ノートにモデレーターバッジを表示するのを廃止
diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts
index fc0860970c..81468d6de2 100644
--- a/packages/backend/src/models/repositories/user.ts
+++ b/packages/backend/src/models/repositories/user.ts
@@ -189,12 +189,12 @@ export class UserRepository extends Repository<User> {
 
 		const followingCount = profile == null ? null :
 			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount :
-			(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followingCount :
+			(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
 			null;
 
 		const followersCount = profile == null ? null :
 			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount :
-			(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followersCount :
+			(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
 			null;
 
 		const falsy = opts.detail ? false : undefined;

From fa36b88af41cf96bd975189f30ca5354d14679d9 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 22:09:40 +0900
Subject: [PATCH 21/29] refactor(client): refactor ui components

---
 packages/client/src/menu.ts                   |   6 +-
 packages/client/src/os.ts                     |   2 +-
 .../src/ui/_common_/sidebar-for-mobile.vue    | 205 +++++++
 packages/client/src/ui/_common_/sidebar.vue   | 565 ++++++++----------
 packages/client/src/ui/classic.vue            | 141 +----
 packages/client/src/ui/deck.vue               | 289 +++++----
 packages/client/src/ui/universal.vue          | 295 +++++----
 7 files changed, 814 insertions(+), 689 deletions(-)
 create mode 100644 packages/client/src/ui/_common_/sidebar-for-mobile.vue

diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
index ae74740bb8..bd155ba16d 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -1,4 +1,4 @@
-import { computed, ref } from 'vue';
+import { computed, ref, reactive } from 'vue';
 import { search } from '@/scripts/search';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
@@ -7,7 +7,7 @@ import { $i } from './account';
 import { unisonReload } from '@/scripts/unison-reload';
 import { router } from './router';
 
-export const menuDef = {
+export const menuDef = reactive({
 	notifications: {
 		title: 'notifications',
 		icon: 'fas fa-bell',
@@ -221,4 +221,4 @@ export const menuDef = {
 			}*/], ev.currentTarget || ev.target);
 		},
 	},
-};
+});
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 30f6b35964..37b57557c3 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -556,7 +556,7 @@ export function contextMenu(items: any[], ev: MouseEvent) {
 	});
 }
 
-export function post(props: Record<string, any>) {
+export function post(props: Record<string, any> = {}) {
 	return new Promise((resolve, reject) => {
 		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
 		// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
diff --git a/packages/client/src/ui/_common_/sidebar-for-mobile.vue b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
new file mode 100644
index 0000000000..5babdb98a8
--- /dev/null
+++ b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
@@ -0,0 +1,205 @@
+<template>
+<div class="kmwsukvl">
+	<div>
+		<button v-click-anime class="item _button account" @click="openAccountMenu">
+			<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
+		</button>
+		<MkA v-click-anime class="item index" active-class="active" to="/" exact>
+			<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
+		</MkA>
+		<template v-for="item in menu">
+			<div v-if="item === '-'" class="divider"></div>
+			<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
+				<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
+				<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+			</component>
+		</template>
+		<div class="divider"></div>
+		<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
+			<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
+		</MkA>
+		<button v-click-anime class="item _button" @click="more">
+			<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
+			<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
+		</button>
+		<MkA v-click-anime class="item" active-class="active" to="/settings">
+			<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
+		</MkA>
+		<button class="item _button post" data-cy-open-post-form @click="post">
+			<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
+		</button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, ref, toRef, watch } from 'vue';
+import { host } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import { openAccountMenu } from '@/account';
+import { defaultStore } from '@/store';
+
+export default defineComponent({
+	setup(props, context) {
+		const menu = toRef(defaultStore.state, 'menu');
+		const otherMenuItemIndicated = computed(() => {
+			for (const def in menuDef) {
+				if (menu.value.includes(def)) continue;
+				if (menuDef[def].indicated) return true;
+			}
+			return false;
+		});
+
+		return {
+			host: host,
+			accounts: [],
+			connection: null,
+			menu,
+			menuDef: menuDef,
+			otherMenuItemIndicated,
+			post: os.post,
+			search,
+			openAccountMenu,
+			more: () => {
+				os.popup(import('@/components/launch-pad.vue'), {}, {
+				}, 'closed');
+			},
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.kmwsukvl {
+	$ui-font-size: 1em; // TODO: どこかに集約したい
+	$avatar-size: 32px;
+	$avatar-margin: 8px;
+
+	> div {
+
+		> .divider {
+			margin: 16px 16px;
+			border-top: solid 0.5px var(--divider);
+		}
+
+		> .item {
+			position: relative;
+			display: block;
+			padding-left: 24px;
+			font-size: $ui-font-size;
+			line-height: 2.85rem;
+			text-overflow: ellipsis;
+			overflow: hidden;
+			white-space: nowrap;
+			width: 100%;
+			text-align: left;
+			box-sizing: border-box;
+			color: var(--navFg);
+
+			> i {
+				position: relative;
+				width: 32px;
+			}
+
+			> i,
+			> .avatar {
+				margin-right: $avatar-margin;
+			}
+
+			> .avatar {
+				width: $avatar-size;
+				height: $avatar-size;
+				vertical-align: middle;
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 0;
+				left: 20px;
+				color: var(--navIndicator);
+				font-size: 8px;
+				animation: blink 1s infinite;
+			}
+
+			> .text {
+				position: relative;
+				font-size: 0.9em;
+			}
+
+			&:hover {
+				text-decoration: none;
+				color: var(--navHoverFg);
+			}
+
+			&.active {
+				color: var(--navActive);
+			}
+
+			&:hover, &.active {
+				&:before {
+					content: "";
+					display: block;
+					width: calc(100% - 24px);
+					height: 100%;
+					margin: auto;
+					position: absolute;
+					top: 0;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-radius: 999px;
+					background: var(--accentedBg);
+				}
+			}
+
+			&:first-child, &:last-child {
+				position: sticky;
+				z-index: 1;
+				padding-top: 8px;
+				padding-bottom: 8px;
+				background: var(--X14);
+				-webkit-backdrop-filter: var(--blur, blur(8px));
+				backdrop-filter: var(--blur, blur(8px));
+			}
+
+			&:first-child {
+				top: 0;
+
+				&:hover, &.active {
+					&:before {
+						content: none;
+					}
+				}
+			}
+
+			&:last-child {
+				bottom: 0;
+				color: var(--fgOnAccent);
+
+				&:before {
+					content: "";
+					display: block;
+					width: calc(100% - 20px);
+					height: calc(100% - 20px);
+					margin: auto;
+					position: absolute;
+					top: 0;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-radius: 999px;
+					background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+				}
+				
+				&:hover, &.active {
+					&:before {
+						background: var(--accentLighten);
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue
index 6abb21d963..00e95d3663 100644
--- a/packages/client/src/ui/_common_/sidebar.vue
+++ b/packages/client/src/ui/_common_/sidebar.vue
@@ -1,385 +1,300 @@
 <template>
-<div class="mvcprjjd">
-	<transition name="nav-back">
-		<div v-if="showing"
-			class="nav-back _modalBg"
-			@click="showing = false"
-			@touchstart.passive="showing = false"
-		></div>
-	</transition>
-
-	<transition name="nav">
-		<nav v-show="showing" class="nav" :class="{ iconOnly, hidden }">
-			<div>
-				<button v-click-anime class="item _button account" @click="openAccountMenu">
-					<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
-				</button>
-				<MkA v-click-anime class="item index" active-class="active" to="/" exact>
-					<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
-				</MkA>
-				<template v-for="item in menu">
-					<div v-if="item === '-'" class="divider"></div>
-					<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
-						<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
-						<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
-					</component>
-				</template>
-				<div class="divider"></div>
-				<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
-					<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
-				</MkA>
-				<button v-click-anime class="item _button" @click="more">
-					<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
-					<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
-				</button>
-				<MkA v-click-anime class="item" active-class="active" to="/settings">
-					<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
-				</MkA>
-				<button class="item _button post" data-cy-open-post-form @click="post">
-					<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
-				</button>
-			</div>
-		</nav>
-	</transition>
+<div class="mvcprjjd" :class="{ iconOnly }">
+	<div>
+		<button v-click-anime class="item _button account" @click="openAccountMenu">
+			<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
+		</button>
+		<MkA v-click-anime class="item index" active-class="active" to="/" exact>
+			<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
+		</MkA>
+		<template v-for="item in menu">
+			<div v-if="item === '-'" class="divider"></div>
+			<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
+				<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
+				<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+			</component>
+		</template>
+		<div class="divider"></div>
+		<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
+			<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
+		</MkA>
+		<button v-click-anime class="item _button" @click="more">
+			<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
+			<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
+		</button>
+		<MkA v-click-anime class="item" active-class="active" to="/settings">
+			<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
+		</MkA>
+		<button class="item _button post" data-cy-open-post-form @click="post">
+			<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
+		</button>
+	</div>
 </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent, ref, watch } from 'vue';
 import { host } from '@/config';
 import { search } from '@/scripts/search';
 import * as os from '@/os';
 import { menuDef } from '@/menu';
 import { openAccountMenu } from '@/account';
+import { defaultStore } from '@/store';
 
 export default defineComponent({
-	props: {
-		defaultHidden: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
+	setup(props, context) {
+		const iconOnly = ref(false);
 
-	data() {
-		return {
-			host: host,
-			showing: false,
-			accounts: [],
-			connection: null,
-			menuDef: menuDef,
-			iconOnly: false,
-			hidden: this.defaultHidden,
-		};
-	},
-
-	computed: {
-		menu(): string[] {
-			return this.$store.state.menu;
-		},
-
-		otherNavItemIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (this.menu.includes(def)) continue;
-				if (this.menuDef[def].indicated) return true;
+		const menu = computed(() => defaultStore.state.menu);
+		const otherMenuItemIndicated = computed(() => {
+			for (const def in menuDef) {
+				if (menu.value.includes(def)) continue;
+				if (menuDef[def].indicated) return true;
 			}
 			return false;
-		},
+		});
+
+		const calcViewState = () => {
+			iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
+		};
+
+		calcViewState();
+
+		window.addEventListener('resize', calcViewState);
+
+		watch(defaultStore.reactiveState.menuDisplay, () => {
+			calcViewState();
+		});
+
+		return {
+			host: host,
+			accounts: [],
+			connection: null,
+			menu,
+			menuDef: menuDef,
+			otherMenuItemIndicated,
+			iconOnly,
+			post: os.post,
+			search,
+			openAccountMenu,
+			more: () => {
+				os.popup(import('@/components/launch-pad.vue'), {}, {
+				}, 'closed');
+			},
+		};
 	},
-
-	watch: {
-		$route(to, from) {
-			this.showing = false;
-		},
-
-		'$store.reactiveState.menuDisplay.value'() {
-			this.calcViewState();
-		},
-
-		iconOnly() {
-			this.$nextTick(() => {
-				this.$emit('change-view-mode');
-			});
-		},
-
-		hidden() {
-			this.$nextTick(() => {
-				this.$emit('change-view-mode');
-			});
-		}
-	},
-
-	created() {
-		window.addEventListener('resize', this.calcViewState);
-		this.calcViewState();
-	},
-
-	methods: {
-		calcViewState() {
-			this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.menuDisplay === 'sideIcon');
-			if (!this.defaultHidden) {
-				this.hidden = (window.innerWidth <= 650);
-			}
-		},
-
-		show() {
-			this.showing = true;
-		},
-
-		post() {
-			os.post();
-		},
-
-		search() {
-			search();
-		},
-
-		more(ev) {
-			os.popup(import('@/components/launch-pad.vue'), {}, {
-			}, 'closed');
-		},
-
-		openAccountMenu,
-	}
 });
 </script>
 
 <style lang="scss" scoped>
-.nav-enter-active,
-.nav-leave-active {
-	opacity: 1;
-	transform: translateX(0);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.nav-enter-from,
-.nav-leave-active {
-	opacity: 0;
-	transform: translateX(-240px);
-}
-
-.nav-back-enter-active,
-.nav-back-leave-active {
-	opacity: 1;
-	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.nav-back-enter-from,
-.nav-back-leave-active {
-	opacity: 0;
-}
-
 .mvcprjjd {
 	$ui-font-size: 1em; // TODO: どこかに集約したい
 	$nav-width: 250px;
 	$nav-icon-only-width: 86px;
+	$avatar-size: 32px;
+	$avatar-margin: 8px;
 
-	> .nav-back {
+	flex: 0 0 $nav-width;
+	width: $nav-width;
+	box-sizing: border-box;
+
+	> div {
+		position: fixed;
+		top: 0;
+		left: 0;
 		z-index: 1001;
-	}
-
-	> .nav {
-		$avatar-size: 32px;
-		$avatar-margin: 8px;
-
-		flex: 0 0 $nav-width;
 		width: $nav-width;
+		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+		height: calc(var(--vh, 1vh) * 100);
 		box-sizing: border-box;
+		overflow: auto;
+		overflow-x: clip;
+		background: var(--navBg);
 
-		&.iconOnly {
-			flex: 0 0 $nav-icon-only-width;
-			width: $nav-icon-only-width;
-
-			&:not(.hidden) {
-				> div {
-					width: $nav-icon-only-width;
-
-					> .divider {
-						margin: 8px auto;
-						width: calc(100% - 32px);
-					}
-
-					> .item {
-						padding-left: 0;
-						padding: 18px 0;
-						width: 100%;
-						text-align: center;
-						font-size: $ui-font-size * 1.1;
-						line-height: initial;
-
-						> i,
-						> .avatar {
-							display: block;
-							margin: 0 auto;
-						}
-
-						> i {
-							opacity: 0.7;
-						}
-
-						> .text {
-							display: none;
-						}
-
-						&:hover, &.active {
-							> i, > .text {
-								opacity: 1;
-							}
-						}
-
-						&:first-child {
-							margin-bottom: 8px;
-						}
-
-						&:last-child {
-							margin-top: 8px;
-						}
-					}
-				}
-			}
+		> .divider {
+			margin: 16px 16px;
+			border-top: solid 0.5px var(--divider);
 		}
 
-		&.hidden {
-			position: fixed;
-			top: 0;
-			left: 0;
-			z-index: 1001;
-		}
-
-		&:not(.hidden) {
-			display: block !important;
-		}
-
-		> div {
-			position: fixed;
-			top: 0;
-			left: 0;
-			z-index: 1001;
-			width: $nav-width;
-			// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-			height: calc(var(--vh, 1vh) * 100);
+		> .item {
+			position: relative;
+			display: block;
+			padding-left: 24px;
+			font-size: $ui-font-size;
+			line-height: 2.85rem;
+			text-overflow: ellipsis;
+			overflow: hidden;
+			white-space: nowrap;
+			width: 100%;
+			text-align: left;
 			box-sizing: border-box;
-			overflow: auto;
-			overflow-x: clip;
-			background: var(--navBg);
+			color: var(--navFg);
 
-			> .divider {
-				margin: 16px 16px;
-				border-top: solid 0.5px var(--divider);
+			> i {
+				position: relative;
+				width: 32px;
 			}
 
-			> .item {
+			> i,
+			> .avatar {
+				margin-right: $avatar-margin;
+			}
+
+			> .avatar {
+				width: $avatar-size;
+				height: $avatar-size;
+				vertical-align: middle;
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 0;
+				left: 20px;
+				color: var(--navIndicator);
+				font-size: 8px;
+				animation: blink 1s infinite;
+			}
+
+			> .text {
 				position: relative;
-				display: block;
-				padding-left: 24px;
-				font-size: $ui-font-size;
-				line-height: 2.85rem;
-				text-overflow: ellipsis;
-				overflow: hidden;
-				white-space: nowrap;
-				width: 100%;
-				text-align: left;
-				box-sizing: border-box;
-				color: var(--navFg);
+				font-size: 0.9em;
+			}
 
-				> i {
-					position: relative;
-					width: 32px;
-				}
+			&:hover {
+				text-decoration: none;
+				color: var(--navHoverFg);
+			}
 
-				> i,
-				> .avatar {
-					margin-right: $avatar-margin;
-				}
+			&.active {
+				color: var(--navActive);
+			}
 
-				> .avatar {
-					width: $avatar-size;
-					height: $avatar-size;
-					vertical-align: middle;
-				}
-
-				> .indicator {
+			&:hover, &.active {
+				&:before {
+					content: "";
+					display: block;
+					width: calc(100% - 24px);
+					height: 100%;
+					margin: auto;
 					position: absolute;
 					top: 0;
-					left: 20px;
-					color: var(--navIndicator);
-					font-size: 8px;
-					animation: blink 1s infinite;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-radius: 999px;
+					background: var(--accentedBg);
 				}
+			}
 
-				> .text {
-					position: relative;
-					font-size: 0.9em;
-				}
+			&:first-child, &:last-child {
+				position: sticky;
+				z-index: 1;
+				padding-top: 8px;
+				padding-bottom: 8px;
+				background: var(--X14);
+				-webkit-backdrop-filter: var(--blur, blur(8px));
+				backdrop-filter: var(--blur, blur(8px));
+			}
 
-				&:hover {
-					text-decoration: none;
-					color: var(--navHoverFg);
-				}
-
-				&.active {
-					color: var(--navActive);
-				}
+			&:first-child {
+				top: 0;
 
 				&:hover, &.active {
 					&:before {
-						content: "";
-						display: block;
-						width: calc(100% - 24px);
-						height: 100%;
-						margin: auto;
-						position: absolute;
-						top: 0;
-						left: 0;
-						right: 0;
-						bottom: 0;
-						border-radius: 999px;
-						background: var(--accentedBg);
+						content: none;
 					}
 				}
+			}
 
-				&:first-child, &:last-child {
-					position: sticky;
-					z-index: 1;
-					padding-top: 8px;
-					padding-bottom: 8px;
-					background: var(--X14);
-					-webkit-backdrop-filter: var(--blur, blur(8px));
-					backdrop-filter: var(--blur, blur(8px));
+			&:last-child {
+				bottom: 0;
+				color: var(--fgOnAccent);
+
+				&:before {
+					content: "";
+					display: block;
+					width: calc(100% - 20px);
+					height: calc(100% - 20px);
+					margin: auto;
+					position: absolute;
+					top: 0;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-radius: 999px;
+					background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+				}
+				
+				&:hover, &.active {
+					&:before {
+						background: var(--accentLighten);
+					}
+				}
+			}
+		}
+	}
+
+	&.iconOnly {
+		flex: 0 0 $nav-icon-only-width;
+		width: $nav-icon-only-width;
+
+		> div {
+			width: $nav-icon-only-width;
+
+			> .divider {
+				margin: 8px auto;
+				width: calc(100% - 32px);
+			}
+
+			> .item {
+				padding-left: 0;
+				padding: 18px 0;
+				width: 100%;
+				text-align: center;
+				font-size: $ui-font-size * 1.1;
+				line-height: initial;
+
+				> i,
+				> .avatar {
+					display: block;
+					margin: 0 auto;
+				}
+
+				> i {
+					opacity: 0.7;
+				}
+
+				> .text {
+					display: none;
+				}
+
+				&:hover, &.active {
+					> i, > .text {
+						opacity: 1;
+					}
 				}
 
 				&:first-child {
-					top: 0;
-
-					&:hover, &.active {
-						&:before {
-							content: none;
-						}
-					}
+					margin-bottom: 8px;
 				}
 
 				&:last-child {
-					bottom: 0;
-					color: var(--fgOnAccent);
+					margin-top: 8px;
+				}
 
-					&:before {
-						content: "";
-						display: block;
-						width: calc(100% - 20px);
-						height: calc(100% - 20px);
-						margin: auto;
-						position: absolute;
-						top: 0;
-						left: 0;
-						right: 0;
-						bottom: 0;
-						border-radius: 999px;
-						background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-					}
-					
-					&:hover, &.active {
-						&:before {
-							background: var(--accentLighten);
-						}
-					}
+				&:before {
+					width: 100%;
+					border-radius: 0;
+				}
+
+				&.post {
+					height: $nav-icon-only-width;
+				}
+
+				&.post:before {
+					width: calc(100% - 32px);
+					height: calc(100% - 32px);
+					border-radius: 100%;
 				}
 			}
 		}
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 684a075c04..91dbe2462d 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -1,16 +1,14 @@
 <template>
-<div class="mk-app" :class="{ wallpaper, isMobile }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
+<div class="gbhvwtnk" :class="{ wallpaper }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
 	<XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/>
 
 	<div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }">
-		<template v-if="!isMobile">
-			<div v-if="!showMenuOnTop" class="sidebar">
-				<XSidebar/>
-			</div>
-			<div v-else ref="widgetsLeft" class="widgets left">
-				<XWidgets :place="'left'" @mounted="attachSticky('widgetsLeft')"/>
-			</div>
-		</template>
+		<div v-if="!showMenuOnTop" class="sidebar">
+			<XSidebar/>
+		</div>
+		<div v-else ref="widgetsLeft" class="widgets left">
+			<XWidgets :place="'left'" @mounted="attachSticky('widgetsLeft')"/>
+		</div>
 
 		<main class="main" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
 			<div class="content">
@@ -32,16 +30,6 @@
 		</div>
 	</div>
 
-	<div v-if="isMobile" class="buttons">
-		<button ref="navButton" class="button nav _button" @click="showDrawerNav"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
-		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
-		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
-		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
-		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
-	</div>
-
-	<XDrawerSidebar v-if="isMobile" ref="drawerNav" class="sidebar"/>
-
 	<transition name="tray-back">
 		<div v-if="widgetsShowing"
 			class="tray-back _modalBg"
@@ -65,20 +53,17 @@ import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
 import { instanceName } from '@/config';
 import { StickySidebar } from '@/scripts/sticky-sidebar';
 import XSidebar from './classic.sidebar.vue';
-import XDrawerSidebar from '@/ui/_common_/sidebar.vue';
 import XCommon from './_common_/common.vue';
 import * as os from '@/os';
 import { menuDef } from '@/menu';
 import * as symbols from '@/symbols';
 
 const DESKTOP_THRESHOLD = 1100;
-const MOBILE_THRESHOLD = 600;
 
 export default defineComponent({
 	components: {
 		XCommon,
 		XSidebar,
-		XDrawerSidebar,
 		XHeaderMenu: defineAsyncComponent(() => import('./classic.header.vue')),
 		XWidgets: defineAsyncComponent(() => import('./classic.widgets.vue')),
 	},
@@ -95,7 +80,6 @@ export default defineComponent({
 			pageInfo: null,
 			menuDef: menuDef,
 			globalHeaderHeight: 0,
-			isMobile: window.innerWidth <= MOBILE_THRESHOLD,
 			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
 			widgetsShowing: false,
 			fullView: false,
@@ -104,16 +88,8 @@ export default defineComponent({
 	},
 
 	computed: {
-		navIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
-				if (this.menuDef[def].indicated) return true;
-			}
-			return false;
-		},
-
 		showMenuOnTop(): boolean {
-			return !this.isMobile && this.$store.state.menuDisplay === 'top';
+			return this.$store.state.menuDisplay === 'top';
 		}
 	},
 
@@ -136,7 +112,6 @@ export default defineComponent({
 
 	mounted() {
 		window.addEventListener('resize', () => {
-			this.isMobile = (window.innerWidth <= MOBILE_THRESHOLD);
 			this.isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD);
 		}, { passive: true });
 
@@ -179,22 +154,10 @@ export default defineComponent({
 			}, { passive: true });
 		},
 
-		post() {
-			os.post();
-		},
-
 		top() {
 			window.scroll({ top: 0, behavior: 'smooth' });
 		},
 
-		back() {
-			history.back();
-		},
-
-		showDrawerNav() {
-			this.$refs.drawerNav.show();
-		},
-
 		onTransition() {
 			if (window._scroll) window._scroll();
 		},
@@ -258,10 +221,9 @@ export default defineComponent({
 	opacity: 0;
 }
 
-.mk-app {
+.gbhvwtnk {
 	$ui-font-size: 1em;
 	$widgets-hide-threshold: 1200px;
-	$nav-icon-only-width: 78px; // TODO: どこかに集約したい
 
 	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 	min-height: calc(var(--vh, 1vh) * 100);
@@ -272,21 +234,6 @@ export default defineComponent({
 		//backdrop-filter: var(--blur, blur(4px));
 	}
 
-	&.isMobile {
-		> .columns {
-			display: block;
-			margin: 0;
-
-			> .main {
-				margin: 0;
-				padding-bottom: 92px;
-				border: none;
-				width: 100%;
-				border-radius: 0;
-			}
-		}
-	}
-
 	> .columns {
 		display: flex;
 		justify-content: center;
@@ -372,76 +319,6 @@ export default defineComponent({
 		}
 	}
 
-	> .buttons {
-		position: fixed;
-		z-index: 1000;
-		bottom: 0;
-		padding: 16px;
-		display: flex;
-		width: 100%;
-		box-sizing: border-box;
-		-webkit-backdrop-filter: var(--blur, blur(32px));
-		backdrop-filter: var(--blur, blur(32px));
-		background-color: var(--header);
-		border-top: solid 0.5px var(--divider);
-
-		> .button {
-			position: relative;
-			flex: 1;
-			padding: 0;
-			margin: auto;
-			height: 64px;
-			border-radius: 8px;
-			background: var(--panel);
-			color: var(--fg);
-
-			&:not(:last-child) {
-				margin-right: 12px;
-			}
-
-			@media (max-width: 400px) {
-				height: 60px;
-
-				&:not(:last-child) {
-					margin-right: 8px;
-				}
-			}
-
-			&:hover {
-				background: var(--X2);
-			}
-
-			> .indicator {
-				position: absolute;
-				top: 0;
-				left: 0;
-				color: var(--indicator);
-				font-size: 16px;
-				animation: blink 1s infinite;
-			}
-
-			&:first-child {
-				margin-left: 0;
-			}
-
-			&:last-child {
-				margin-right: 0;
-			}
-
-			> * {
-				font-size: 22px;
-			}
-
-			&:disabled {
-				cursor: default;
-
-				> * {
-					opacity: 0.5;
-				}
-			}
-		}
-	}
-
 	> .tray-back {
 		z-index: 1001;
 	}
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 4f1efb0a4c..fb8f953625 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -1,8 +1,8 @@
 <template>
-<div class="mk-deck" :class="`${deckStore.reactiveState.columnAlign.value}`" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
+<div class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
 	@contextmenu.self.prevent="onContextmenu"
 >
-	<XSidebar ref="nav"/>
+	<XSidebar v-if="!isMobile"/>
 
 	<template v-for="ids in layout">
 		<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
@@ -22,94 +22,76 @@
 		/>
 	</template>
 
-	<button v-if="$i" class="nav _button" @click="showNav()"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
-	<button v-if="$i" class="post _buttonPrimary" @click="post()"><i class="fas fa-pencil-alt"></i></button>
+	<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>
+		<button class="button home _button" @click="$router.push('/')"><i class="fas fa-home"></i></button>
+		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
+		<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
+	</div>
+
+	<transition name="menu-back">
+		<div v-if="drawerMenuShowing"
+			class="menu-back _modalBg"
+			@click="drawerMenuShowing = false"
+			@touchstart.passive="drawerMenuShowing = false"
+		></div>
+	</transition>
+
+	<transition name="menu">
+		<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
+	</transition>
 
 	<XCommon/>
 </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent, provide, ref, watch } from 'vue';
 import { v4 as uuid } from 'uuid';
-import { host } from '@/config';
 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 { getScrollContainer } from '@/scripts/scroll';
 import * as os from '@/os';
 import { menuDef } from '@/menu';
 import XCommon from './_common_/common.vue';
-import { deckStore, addColumn, loadDeck } from './deck/deck-store';
+import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
+import { useRoute } from 'vue-router';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
 
 export default defineComponent({
 	components: {
 		XCommon,
 		XSidebar,
+		XDrawerMenu,
 		DeckColumnCore,
 	},
 
-	provide() {
-		return {
-			shouldSpacerMin: true,
-			...deckStore.state.navWindow ? {
-				navHook: (url) => {
-					os.pageWindow(url);
-				}
-			} : {}
-		};
-	},
+	setup() {
+		const isMobile = ref(window.innerWidth <= 500);
+		window.addEventListener('resize', () => {
+			isMobile.value = window.innerWidth <= 500;
+		});
 
-	data() {
-		return {
-			deckStore,
-			host: host,
-			menuDef: menuDef,
-			wallpaper: localStorage.getItem('wallpaper') != null,
-		};
-	},
+		const drawerMenuShowing = ref(false);
 
-	computed: {
-		columns() {
-			return deckStore.reactiveState.columns.value;
-		},
-		layout() {
-			return deckStore.reactiveState.layout.value;
-		},
-		navIndicated(): boolean {
-			if (!this.$i) return false;
-			for (const def in this.menuDef) {
-				if (this.menuDef[def].indicated) return true;
+		const route = useRoute();
+		watch(route, () => {
+			drawerMenuShowing.value = false;
+		});
+
+		const columns = deckStore.reactiveState.columns;
+		const layout = deckStore.reactiveState.layout.value;
+		const menuIndicated = computed(() => {
+			if ($i == null) return false;
+			for (const def in menuDef) {
+				if (menuDef[def].indicated) return true;
 			}
 			return false;
-		},
-	},
+		});
 
-	created() {
-		document.documentElement.style.overflowY = 'hidden';
-		document.documentElement.style.scrollBehavior = 'auto';
-		window.addEventListener('wheel', this.onWheel);
-		loadDeck();
-	},
-
-	mounted() {
-	},
-
-	methods: {
-		onWheel(e) {
-			if (getScrollContainer(e.target) == null) {
-				document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
-			}
-		},
-
-		showNav() {
-			this.$refs.nav.show();
-		},
-
-		post() {
-			os.post();
-		},
-
-		async addColumn(ev) {
+		const addColumn = async (ev) => {
 			const columns = [
 				'main',
 				'widgets',
@@ -122,33 +104,83 @@ export default defineComponent({
 			];
 
 			const { canceled, result: column } = await os.select({
-				title: this.$ts._deck.addColumn,
+				title: i18n.locale._deck.addColumn,
 				items: columns.map(column => ({
-					value: column, text: this.$t('_deck._columns.' + column)
+					value: column, text: i18n.t('_deck._columns.' + column)
 				}))
 			});
 			if (canceled) return;
 
-			addColumn({
+			addColumnToStore({
 				type: column,
 				id: uuid(),
-				name: this.$t('_deck._columns.' + column),
+				name: i18n.t('_deck._columns.' + column),
 				width: 330,
 			});
-		},
+		};
 
-		onContextmenu(e) {
+		const onContextmenu = (ev) => {
 			os.contextMenu([{
-				text: this.$ts._deck.addColumn,
+				text: i18n.locale._deck.addColumn,
 				icon: null,
-				action: this.addColumn
-			}], e);
-		},
-	}
+				action: addColumn
+			}], ev);
+		};
+
+		provide('shouldSpacerMin', true);
+		if (deckStore.state.navWindow) {
+			provide('navHook', (url) => {
+				os.pageWindow(url);
+			});
+		}
+
+		document.documentElement.style.overflowY = 'hidden';
+		document.documentElement.style.scrollBehavior = 'auto';
+		window.addEventListener('wheel', (ev) => {
+			if (getScrollContainer(ev.target) == null) {
+				document.documentElement.scrollLeft += ev.deltaY > 0 ? 96 : -96;
+			}
+		});
+		loadDeck();
+
+		return {
+			isMobile,
+			deckStore,
+			drawerMenuShowing,
+			columns,
+			layout,
+			menuIndicated,
+			onContextmenu,
+			wallpaper: localStorage.getItem('wallpaper') != null,
+			post: os.post,
+		};
+	},
 });
 </script>
 
 <style lang="scss" scoped>
+.menu-enter-active,
+.menu-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.menu-enter-from,
+.menu-leave-active {
+	opacity: 0;
+	transform: translateX(-240px);
+}
+
+.menu-back-enter-active,
+.menu-back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.menu-back-enter-from,
+.menu-back-leave-active {
+	opacity: 0;
+}
+
 .mk-deck {
 	$nav-hide-threshold: 650px; // TODO: どこかに集約したい
 
@@ -172,6 +204,10 @@ export default defineComponent({
 		}
 	}
 
+	&.isMobile {
+		padding-bottom: 100px;
+	}
+
 	> .column {
 		flex-shrink: 0;
 		margin-right: var(--deckMargin);
@@ -186,43 +222,88 @@ export default defineComponent({
 		}
 	}
 
-	> .post,
-	> .nav {
+	> .buttons {
 		position: fixed;
 		z-index: 1000;
-		bottom: 32px;
-		width: 64px;
-		height: 64px;
-		border-radius: 100%;
-		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
-		font-size: 22px;
+		bottom: 0;
+		left: 0;
+		padding: 16px;
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
 
-		@media (min-width: ($nav-hide-threshold + 1px)) {
-			display: none;
+		> .button {
+			position: relative;
+			flex: 1;
+			padding: 0;
+			margin: auto;
+			height: 64px;
+			border-radius: 8px;
+			background: var(--panel);
+			color: var(--fg);
+
+			&:not(:last-child) {
+				margin-right: 12px;
+			}
+
+			@media (max-width: 400px) {
+				height: 60px;
+
+				&:not(:last-child) {
+					margin-right: 8px;
+				}
+			}
+
+			&:hover {
+				background: var(--X2);
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 0;
+				left: 0;
+				color: var(--indicator);
+				font-size: 16px;
+				animation: blink 1s infinite;
+			}
+
+			&:first-child {
+				margin-left: 0;
+			}
+
+			&:last-child {
+				margin-right: 0;
+			}
+
+			> * {
+				font-size: 22px;
+			}
+
+			&:disabled {
+				cursor: default;
+
+				> * {
+					opacity: 0.5;
+				}
+			}
 		}
 	}
 
-	> .post {
-		right: 32px;
+	> .menu-back {
+		z-index: 1001;
 	}
 
-	> .nav {
-		left: 32px;
-		background: var(--panel);
-		color: var(--fg);
-
-		&:hover {
-			background: var(--X2);
-		}
-
-		> .indicator {
-			position: absolute;
-			top: 0;
-			left: 0;
-			color: var(--indicator);
-			font-size: 16px;
-			animation: blink 1s infinite;
-		}
+	> .menu {
+		position: fixed;
+		top: 0;
+		left: 0;
+		z-index: 1001;
+		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+		height: calc(var(--vh, 1vh) * 100);
+		width: 240px;
+		box-sizing: border-box;
+		overflow: auto;
+		background: var(--bg);
 	}
 }
 </style>
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 011370f7f1..352163d050 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -1,9 +1,9 @@
 <template>
-<div class="mk-app" :class="{ wallpaper }">
-	<XSidebar ref="nav" class="sidebar"/>
+<div class="dkgtipfy" :class="{ wallpaper }">
+	<XSidebar v-if="!isMobile" class="sidebar"/>
 
-	<div ref="contents" class="contents" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
-		<main ref="main">
+	<div class="contents" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
+		<main>
 			<div class="content">
 				<MkStickyContainer>
 					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
@@ -20,32 +20,44 @@
 		</main>
 	</div>
 
-	<XSide v-if="isDesktop" ref="side" class="side"/>
+	<XSideView v-if="isDesktop" ref="side" class="side"/>
 
-	<div v-if="isDesktop" ref="widgets" class="widgets">
+	<div v-if="isDesktop" ref="widgetsEl" class="widgets">
 		<XWidgets @mounted="attachSticky"/>
 	</div>
 
-	<div class="buttons" :class="{ navHidden }">
-		<button ref="navButton" class="button nav _button" @click="showNav"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
+	<button class="widgetButton _button" :class="{ show: true }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
+
+	<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>
 		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
 		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
 		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
-		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
+		<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
 	</div>
 
-	<button class="widgetButton _button" :class="{ navHidden }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
+	<transition name="menuDrawer-back">
+		<div v-if="drawerMenuShowing"
+			class="menuDrawer-back _modalBg"
+			@click="drawerMenuShowing = false"
+			@touchstart.passive="drawerMenuShowing = false"
+		></div>
+	</transition>
 
-	<transition name="tray-back">
+	<transition name="menuDrawer">
+		<XDrawerMenu v-if="drawerMenuShowing" class="menuDrawer"/>
+	</transition>
+
+	<transition name="widgetsDrawer-back">
 		<div v-if="widgetsShowing"
-			class="tray-back _modalBg"
+			class="widgetsDrawer-back _modalBg"
 			@click="widgetsShowing = false"
 			@touchstart.passive="widgetsShowing = false"
 		></div>
 	</transition>
 
-	<transition name="tray">
-		<XWidgets v-if="widgetsShowing" class="tray"/>
+	<transition name="widgetsDrawer">
+		<XWidgets v-if="widgetsShowing" class="widgetsDrawer"/>
 	</transition>
 
 	<XCommon/>
@@ -53,60 +65,69 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+import { defineComponent, defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue';
 import { instanceName } from '@/config';
 import { StickySidebar } from '@/scripts/sticky-sidebar';
 import XSidebar from '@/ui/_common_/sidebar.vue';
+import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
 import XCommon from './_common_/common.vue';
-import XSide from './classic.side.vue';
+import XSideView from './classic.side.vue';
 import * as os from '@/os';
-import { menuDef } from '@/menu';
 import * as symbols from '@/symbols';
+import { defaultStore } from '@/store';
+import * as EventEmitter from 'eventemitter3';
+import { menuDef } from '@/menu';
+import { useRoute } from 'vue-router';
+import { i18n } from '@/i18n';
 
 const DESKTOP_THRESHOLD = 1100;
+const MOBILE_THRESHOLD = 500;
 
 export default defineComponent({
 	components: {
 		XCommon,
 		XSidebar,
+		XDrawerMenu,
 		XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
-		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
+		XSideView, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
 	},
 
-	provide() {
-		return {
-			sideViewHook: this.isDesktop ? (url) => {
-				this.$refs.side.navigate(url);
-			} : null
-		};
-	},
+	setup() {
+		const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
+		const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD);
+		window.addEventListener('resize', () => {
+			isMobile.value = window.innerWidth <= MOBILE_THRESHOLD;
+		});
 
-	data() {
-		return {
-			pageInfo: null,
-			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
-			menuDef: menuDef,
-			navHidden: false,
-			widgetsShowing: false,
-			wallpaper: localStorage.getItem('wallpaper') != null,
-		};
-	},
+		const pageInfo = ref();
+		const widgetsEl = ref<HTMLElement>();
+		const widgetsShowing = ref(false);
 
-	computed: {
-		navIndicated(): boolean {
-			for (const def in this.menuDef) {
+		const sideViewController = new EventEmitter();
+
+		provide('sideViewHook', isDesktop.value ? (url) => {
+			sideViewController.emit('navigate', url);
+		} : null);
+
+		const menuIndicated = computed(() => {
+			for (const def in menuDef) {
 				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
-				if (this.menuDef[def].indicated) return true;
+				if (menuDef[def].indicated) return true;
 			}
 			return false;
-		}
-	},
+		});
+
+		const drawerMenuShowing = ref(false);
+
+		const route = useRoute();
+		watch(route, () => {
+			drawerMenuShowing.value = false;
+		});
 
-	created() {
 		document.documentElement.style.overflowY = 'scroll';
 
-		if (this.$store.state.widgets.length === 0) {
-			this.$store.set('widgets', [{
+		if (defaultStore.state.widgets.length === 0) {
+			defaultStore.set('widgets', [{
 				name: 'calendar',
 				id: 'a', place: 'right', data: {}
 			}, {
@@ -117,123 +138,129 @@ export default defineComponent({
 				id: 'c', place: 'right', data: {}
 			}]);
 		}
-	},
 
-	mounted() {
-		this.adjustUI();
-
-		const ro = new ResizeObserver((entries, observer) => {
-			this.adjustUI();
+		onMounted(() => {
+			if (!isDesktop.value) {
+				window.addEventListener('resize', () => {
+					if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
+				}, { passive: true });
+			}
 		});
 
-		ro.observe(this.$refs.contents);
-
-		window.addEventListener('resize', this.adjustUI, { passive: true });
-
-		if (!this.isDesktop) {
-			window.addEventListener('resize', () => {
-				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
-			}, { passive: true });
-		}
-	},
-
-	methods: {
-		changePage(page) {
+		const changePage = (page) => {
 			if (page == null) return;
 			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-				document.title = `${this.pageInfo.title} | ${instanceName}`;
+				pageInfo.value = page[symbols.PAGE_INFO];
+				document.title = `${pageInfo.value.title} | ${instanceName}`;
 			}
-		},
+		};
 
-		adjustUI() {
-			const navWidth = this.$refs.nav.$el.offsetWidth;
-			this.navHidden = navWidth === 0;
-		},
-
-		showNav() {
-			this.$refs.nav.show();
-		},
-
-		attachSticky(el) {
-			const sticky = new StickySidebar(this.$refs.widgets);
-			window.addEventListener('scroll', () => {
-				sticky.calc(window.scrollY);
-			}, { passive: true });
-		},
-
-		post() {
-			os.post();
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		back() {
-			history.back();
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-
-		onContextmenu(e) {
+		const onContextmenu = (ev) => {
 			const isLink = (el: HTMLElement) => {
 				if (el.tagName === 'A') return true;
 				if (el.parentElement) {
 					return isLink(el.parentElement);
 				}
 			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+			if (isLink(ev.target)) return;
+			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
 			if (window.getSelection().toString() !== '') return;
-			const path = this.$route.path;
+			const path = route.path;
 			os.contextMenu([{
 				type: 'label',
 				text: path,
 			}, {
 				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
+				text: i18n.locale.openInSideView,
 				action: () => {
 					this.$refs.side.navigate(path);
 				}
 			}, {
 				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
+				text: i18n.locale.openInWindow,
 				action: () => {
 					os.pageWindow(path);
 				}
-			}], e);
-		},
-	}
+			}], ev);
+		};
+
+		const attachSticky = (el) => {
+			const sticky = new StickySidebar(widgetsEl.value);
+			window.addEventListener('scroll', () => {
+				sticky.calc(window.scrollY);
+			}, { passive: true });
+		};
+
+		return {
+			pageInfo,
+			isDesktop,
+			isMobile,
+			widgetsEl,
+			widgetsShowing,
+			drawerMenuShowing,
+			menuIndicated,
+			wallpaper: localStorage.getItem('wallpaper') != null,
+			changePage,
+			top: () => {
+				window.scroll({ top: 0, behavior: 'smooth' });
+			},
+			onTransition: () => {
+				if (window._scroll) window._scroll();
+			},
+			post: os.post,
+			onContextmenu,
+			attachSticky,
+		};
+	},
 });
 </script>
 
 <style lang="scss" scoped>
-.tray-enter-active,
-.tray-leave-active {
+.widgetsDrawer-enter-active,
+.widgetsDrawer-leave-active {
 	opacity: 1;
 	transform: translateX(0);
 	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 }
-.tray-enter-from,
-.tray-leave-active {
+.widgetsDrawer-enter-from,
+.widgetsDrawer-leave-active {
 	opacity: 0;
 	transform: translateX(240px);
 }
 
-.tray-back-enter-active,
-.tray-back-leave-active {
+.widgetsDrawer-back-enter-active,
+.widgetsDrawer-back-leave-active {
 	opacity: 1;
 	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 }
-.tray-back-enter-from,
-.tray-back-leave-active {
+.widgetsDrawer-back-enter-from,
+.widgetsDrawer-back-leave-active {
 	opacity: 0;
 }
 
-.mk-app {
+.menuDrawer-enter-active,
+.menuDrawer-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.menuDrawer-enter-from,
+.menuDrawer-leave-active {
+	opacity: 0;
+	transform: translateX(-240px);
+}
+
+.menuDrawer-back-enter-active,
+.menuDrawer-back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.menuDrawer-back-enter-from,
+.menuDrawer-back-leave-active {
+	opacity: 0;
+}
+
+.dkgtipfy {
 	$ui-font-size: 1em; // TODO: どこかに集約したい
 	$widgets-hide-threshold: 1090px;
 
@@ -285,6 +312,7 @@ export default defineComponent({
 		}
 	}
 
+/*
 	> .widgetButton {
 		display: block;
 		position: fixed;
@@ -305,12 +333,34 @@ export default defineComponent({
 		@media (min-width: ($widgets-hide-threshold + 1px)) {
 			display: none;
 		}
+	}*/
+
+	> .widgetButton {
+		display: none;
+	}
+
+	> .widgetsDrawer-back {
+		z-index: 1001;
+	}
+
+	> .widgetsDrawer {
+		position: fixed;
+		top: 0;
+		right: 0;
+		z-index: 1001;
+		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+		height: calc(var(--vh, 1vh) * 100);
+		padding: var(--margin);
+		box-sizing: border-box;
+		overflow: auto;
+		background: var(--bg);
 	}
 
 	> .buttons {
 		position: fixed;
 		z-index: 1000;
 		bottom: 0;
+		left: 0;
 		padding: 16px;
 		display: flex;
 		width: 100%;
@@ -319,10 +369,6 @@ export default defineComponent({
 		backdrop-filter: var(--blur, blur(32px));
 		background-color: var(--header);
 
-		&:not(.navHidden) {
-			display: none;
-		}
-
 		> .button {
 			position: relative;
 			flex: 1;
@@ -380,22 +426,23 @@ export default defineComponent({
 		}
 	}
 
-	> .tray-back {
+	> .menuDrawer-back {
 		z-index: 1001;
 	}
 
-	> .tray {
+	> .menuDrawer {
 		position: fixed;
 		top: 0;
-		right: 0;
+		left: 0;
 		z-index: 1001;
 		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 		height: calc(var(--vh, 1vh) * 100);
-		padding: var(--margin);
+		width: 240px;
 		box-sizing: border-box;
 		overflow: auto;
 		background: var(--bg);
 	}
+
 }
 </style>
 

From 4f208b99ff64fbe26fab0e003d73401757beaace Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 22:22:08 +0900
Subject: [PATCH 22/29] enhance(client): improve usability

---
 packages/client/src/ui/deck.vue        | 1 +
 packages/client/src/ui/deck/column.vue | 1 +
 packages/client/src/ui/universal.vue   | 2 ++
 3 files changed, 4 insertions(+)

diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index fb8f953625..e1b2887bb2 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -303,6 +303,7 @@ export default defineComponent({
 		width: 240px;
 		box-sizing: border-box;
 		overflow: auto;
+		overscroll-behavior: contain;
 		background: var(--bg);
 	}
 }
diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue
index 09d089c528..d3c7cf8213 100644
--- a/packages/client/src/ui/deck/column.vue
+++ b/packages/client/src/ui/deck/column.vue
@@ -401,6 +401,7 @@ export default defineComponent({
 		height: calc(100% - var(--deckColumnHeaderHeight));
 		overflow: auto;
 		overflow-x: hidden;
+		overscroll-behavior: contain;
 		-webkit-overflow-scrolling: touch;
 		box-sizing: border-box;
 	}
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 352163d050..9fc2177ee0 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -353,6 +353,7 @@ export default defineComponent({
 		padding: var(--margin);
 		box-sizing: border-box;
 		overflow: auto;
+		overscroll-behavior: contain;
 		background: var(--bg);
 	}
 
@@ -440,6 +441,7 @@ export default defineComponent({
 		width: 240px;
 		box-sizing: border-box;
 		overflow: auto;
+		overscroll-behavior: contain;
 		background: var(--bg);
 	}
 

From 00982d2742e96fd326e02d998ac0a0750b2143f6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 22:26:09 +0900
Subject: [PATCH 23/29] enhance(client): improve usability

---
 packages/client/src/ui/classic.vue | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 91dbe2462d..41da973152 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -94,6 +94,11 @@ export default defineComponent({
 	},
 
 	created() {
+		if (window.innerWidth < 1024) {
+			localStorage.setItem('ui', 'default');
+			location.reload();
+		}
+
 		document.documentElement.style.overflowY = 'scroll';
 
 		if (this.$store.state.widgets.length === 0) {

From 300785923c368fe9c489ce23f29b48ccb255bdb3 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 22:28:42 +0900
Subject: [PATCH 24/29] New Crowdin updates (#8031)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Simplified)
---
 locales/de-DE.yml | 1 +
 locales/zh-CN.yml | 1 +
 2 files changed, 2 insertions(+)

diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 030d13bec4..69d90c7624 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -792,6 +792,7 @@ pubSub: "Pub/Sub Benutzerkonten"
 lastCommunication: "Letzte Kommunikation"
 resolved: "Gelöst"
 unresolved: "Ungelöst"
+breakFollow: "Follower entfernen"
 itsOn: "Eingeschaltet"
 itsOff: "Ausgeschaltet"
 emailRequiredForSignup: "Angaben einer Email-Adresse als benötigt markieren"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 3fdbbc63ac..829c47e7dc 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -792,6 +792,7 @@ pubSub: "Pub/Sub账户"
 lastCommunication: "最近通信"
 resolved: "已解决"
 unresolved: "未解决"
+breakFollow: "移除关注者"
 itsOn: "已开启"
 itsOff: "已关闭"
 emailRequiredForSignup: "注册账户需要电子邮件地址"

From 75c087b79fbe7aeaf44fa492e26eeea6668658f0 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 22:29:58 +0900
Subject: [PATCH 25/29] 12.98.0

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01c0adecfb..281416fd76 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@
 
 -->
 
-## 12.x.x (unreleased)
+## 12.98.0 (2021/12/03)
 
 ### Improvements
 - API: /antennas/notes API で日付による絞り込みができるように
diff --git a/package.json b/package.json
index 49da8fda93..ef08f2d5e8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.97.1",
+	"version": "12.98.0",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From b65353bc3cd90ccce84c328058818d2bb12380ff Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 4 Dec 2021 18:12:03 +0900
Subject: [PATCH 26/29] =?UTF-8?q?fix(client):=20pages=E3=81=A7=E9=96=A2?=
 =?UTF-8?q?=E6=95=B0=E3=82=92=E5=AE=9A=E7=BE=A9=E3=81=A7=E3=81=8D=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../client/src/pages/page-editor/page-editor.script-block.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/pages/page-editor/page-editor.script-block.vue b/packages/client/src/pages/page-editor/page-editor.script-block.vue
index 1d3dc25d4b..ded9368b89 100644
--- a/packages/client/src/pages/page-editor/page-editor.script-block.vue
+++ b/packages/client/src/pages/page-editor/page-editor.script-block.vue
@@ -45,10 +45,10 @@
 			<template #label>{{ $ts._pages.script.blocks._fn.slots }}</template>
 			<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
 		</MkTextarea>
-		<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/>
+		<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/>
 	</section>
 	<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
-		<XV v-for="(x, i) in modelValue.args" :key="i" v-model="value.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/>
+		<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/>
 	</section>
 	<section v-else class="" style="padding:16px;">
 		<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/>

From e42e9530cb8c0b218a013719de9cae3e804680db Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 4 Dec 2021 18:27:31 +0900
Subject: [PATCH 27/29] fix(client): tweak style

---
 packages/client/src/pages/about.vue           |  5 +--
 packages/client/src/pages/api-console.vue     | 44 ++++++++++---------
 .../src/pages/page-editor/page-editor.vue     | 10 ++---
 packages/client/src/ui/_common_/sidebar.vue   |  4 ++
 4 files changed, 34 insertions(+), 29 deletions(-)

diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index a3a3d3cfb7..04f68b7201 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -24,7 +24,7 @@
 		</FormSection>
 
 		<FormSection>
-			<div class="_inputSplit">
+			<div class="_inputSplit _formBlock">
 				<MkKeyValue class="_formBlock">
 					<template #key>{{ $ts.administrator }}</template>
 					<template #value>{{ $instance.maintainerName }}</template>
@@ -34,10 +34,9 @@
 					<template #value>{{ $instance.maintainerEmail }}</template>
 				</MkKeyValue>
 			</div>
+			<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
 		</FormSection>
 
-		<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" external>{{ $ts.tos }}</FormLink>
-
 		<FormSuspense :p="initStats">
 			<FormSection>
 				<template #label>{{ $ts.statistics }}</template>
diff --git a/packages/client/src/pages/api-console.vue b/packages/client/src/pages/api-console.vue
index 1c41315d21..594778e539 100644
--- a/packages/client/src/pages/api-console.vue
+++ b/packages/client/src/pages/api-console.vue
@@ -1,26 +1,28 @@
 <template>
-<div class="_root">
-	<div class="_block" style="padding: 24px;">
-		<MkInput v-model="endpoint" :datalist="endpoints" class="" @update:modelValue="onEndpointChange()">
-			<template #label>Endpoint</template>
-		</MkInput>
-		<MkTextarea v-model="body" code>
-			<template #label>Params (JSON or JSON5)</template>
-		</MkTextarea>
-		<MkSwitch v-model="withCredential">
-			With credential
-		</MkSwitch>
-		<MkButton primary full :disabled="sending" @click="send">
-			<template v-if="sending"><MkEllipsis/></template>
-			<template v-else><i class="fas fa-paper-plane"></i> Send</template>
-		</MkButton>
+<MkSpacer :content-max="700">
+	<div class="_formRoot">
+		<div class="_formBlock">
+			<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()">
+				<template #label>Endpoint</template>
+			</MkInput>
+			<MkTextarea v-model="body" class="_formBlock" code>
+				<template #label>Params (JSON or JSON5)</template>
+			</MkTextarea>
+			<MkSwitch v-model="withCredential" class="_formBlock">
+				With credential
+			</MkSwitch>
+			<MkButton class="_formBlock" primary :disabled="sending" @click="send">
+				<template v-if="sending"><MkEllipsis/></template>
+				<template v-else><i class="fas fa-paper-plane"></i> Send</template>
+			</MkButton>
+		</div>
+		<div v-if="res" class="_formBlock">
+			<MkTextarea v-model="res" code readonly tall>
+				<template #label>Response</template>
+			</MkTextarea>
+		</div>
 	</div>
-	<div v-if="res" class="_block" style="padding: 24px;">
-		<MkTextarea v-model="res" code readonly tall>
-			<template #label>Response</template>
-		</MkTextarea>
-	</div>
-</div>
+</MkSpacer>
 </template>
 
 <script lang="ts">
diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue
index 4de1121fd3..52f6858b0b 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>
-<div>
-	<div class="jqqmcavi" style="margin: 16px;">
+<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>
 		<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
 		<MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="fas fa-copy"></i> {{ $ts.duplicate }}</MkButton>
@@ -8,7 +8,7 @@
 	</div>
 
 	<div v-if="tab === 'settings'">
-		<div style="padding: 16px;" class="_formRoot">
+		<div class="_formRoot">
 			<MkInput v-model="title" class="_formBlock">
 				<template #label>{{ $ts._pages.title }}</template>
 			</MkInput>
@@ -43,7 +43,7 @@
 	</div>
 
 	<div v-else-if="tab === 'contents'">
-		<div style="padding: 16px;">
+		<div>
 			<XBlocks v-model="content" class="content" :hpml="hpml"/>
 
 			<MkButton v-if="!readonly" @click="add()"><i class="fas fa-plus"></i></MkButton>
@@ -75,7 +75,7 @@
 			<MkTextarea v-model="script" class="_code"/>
 		</div>
 	</div>
-</div>
+</MkSpacer>
 </template>
 
 <script lang="ts">
diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue
index 00e95d3663..e363c3abd9 100644
--- a/packages/client/src/ui/_common_/sidebar.vue
+++ b/packages/client/src/ui/_common_/sidebar.vue
@@ -289,6 +289,10 @@ export default defineComponent({
 
 				&.post {
 					height: $nav-icon-only-width;
+
+					> i {
+						opacity: 1;
+					}
 				}
 
 				&.post:before {

From 8a3f860213a0221383e68ed545712f30757516b6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 4 Dec 2021 20:35:08 +0900
Subject: [PATCH 28/29] fix(client): fix range slider rendering

---
 .../client/src/components/form-dialog.vue     |  2 +-
 packages/client/src/components/form/range.vue | 28 +++++++++++++++----
 packages/client/src/pages/settings/sounds.vue |  1 +
 3 files changed, 24 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue
index fbf49af5d2..efd0da443d 100644
--- a/packages/client/src/components/form-dialog.vue
+++ b/packages/client/src/components/form-dialog.vue
@@ -40,7 +40,7 @@
 					<template #caption><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" class="_formBlock">
+				<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">
 					<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>
diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index 79a83d6a93..3e02cacb9b 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -16,7 +16,7 @@
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, ref, watch } from 'vue';
+import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
 import * as os from '@/os';
 
 export default defineComponent({
@@ -58,6 +58,9 @@ export default defineComponent({
 	},
 
 	setup(props, context) {
+		const containerEl = ref<HTMLElement>();
+		const thumbEl = ref<HTMLElement>();
+
 		const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
 		const steppedValue = computed(() => {
 			if (props.step) {
@@ -78,10 +81,25 @@ export default defineComponent({
 			if (thumbEl.value == null) return 0;
 			return thumbEl.value!.offsetWidth;
 		});
-		const thumbPosition = computed(() => {
-			if (containerEl.value == null) return 0;
-			return (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
+		const thumbPosition = ref(0);
+		const calcThumbPosition = () => {
+			if (containerEl.value == null) {
+				thumbPosition.value = 0;
+			} else {
+				thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
+			}
+		};
+		watch([steppedValue, containerEl], calcThumbPosition);
+		onMounted(() => {
+			const ro = new ResizeObserver((entries, observer) => {
+				calcThumbPosition();
+			});
+			ro.observe(containerEl.value);
+			onUnmounted(() => {
+				ro.disconnect();
+			});
 		});
+
 		const steps = computed(() => {
 			if (props.step) {
 				return (props.max - props.min) / props.step;
@@ -89,8 +107,6 @@ export default defineComponent({
 				return 0;
 			}
 		});
-		const containerEl = ref<HTMLElement>();
-		const thumbEl = ref<HTMLElement>();
 
 		const onMousedown = (ev: MouseEvent | TouchEvent) => {
 			ev.preventDefault();
diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue
index 1492a989a2..0977dd8322 100644
--- a/packages/client/src/pages/settings/sounds.vue
+++ b/packages/client/src/pages/settings/sounds.vue
@@ -119,6 +119,7 @@ export default defineComponent({
 					mim: 0,
 					max: 1,
 					step: 0.05,
+					textConverter: (v) => `${Math.floor(v * 100)}%`,
 					label: this.$ts.volume,
 					default: this.sounds[type].volume
 				},

From b1bd7307bb85ba8ac4d0f2df627b52c3ec5fcc72 Mon Sep 17 00:00:00 2001
From: futchitwo <74236683+futchitwo@users.noreply.github.com>
Date: Sun, 5 Dec 2021 03:01:35 +0900
Subject: [PATCH 29/29] =?UTF-8?q?Fix(client):=20API=E3=82=B3=E3=83=B3?=
 =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=83=AB=E3=81=A7=20with=20credential=20?=
 =?UTF-8?q?=E3=81=8C=E3=82=AA=E3=83=95=E3=81=A0=E3=81=A8i=E3=81=8C?=
 =?UTF-8?q?=E4=BB=98=E4=B8=8E=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(#8038)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/client/src/pages/api-console.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/pages/api-console.vue b/packages/client/src/pages/api-console.vue
index 594778e539..16018be712 100644
--- a/packages/client/src/pages/api-console.vue
+++ b/packages/client/src/pages/api-console.vue
@@ -66,7 +66,8 @@ export default defineComponent({
 	methods: {
 		send() {
 			this.sending = true;
-			os.api(this.endpoint, JSON5.parse(this.body)).then(res => {
+			const body = JSON5.parse(this.body);
+			os.api(this.endpoint, body, body.i || this.withCredential ? undefined : null).then(res => {
 				this.sending = false;
 				this.res = JSON5.stringify(res, null, 2);
 			}, err => {